Skip to content

[Sprint] sprint-loop-5 - #7

Merged
scealiontach merged 10 commits into
mainfrom
sprint/2026-04-28-sprint-loop-5
Apr 28, 2026
Merged

scealiontach merged 10 commits into
mainfrom
sprint/2026-04-28-sprint-loop-5

Conversation

@scealiontach

Copy link
Copy Markdown
Owner

Sprint Plan — 2026-04-28-sprint-loop-5

Sprint Goal

Harden the release pipeline and operational integrations of bash/ by closing
two High-priority gaps (no smoke test for pack-script-produced binaries and
JSON injection in pagerduty.sh) and three correctness bugs in shipped tooling
(update-repo-tags semver detection, git-check dead/brittle output, aws.sh
ECR scan caching). The thread tying these together is "trust": each issue is a
silent-failure mode in code that ships releases, files incidents, or feeds
operational dashboards. The sprint introduces the first end-to-end test of the
bin-tarball packer, removes a JSON-injection vector on incident creation, and
fixes three live bugs that operators are unlikely to notice until something
goes wrong.

Selected Issues

1. SUR-1852 — Test: pack-script (build-time bin packer) has no test that produced binaries actually run

  • Priority: High
  • Description summary: bash/pack-script produces every redistributable
    command in dist/bin-*.tar.gz, but make only verifies the tarball builds —
    not that any packed binary actually runs. There is no regression coverage
    for the transitive @include walker, dedup invariants, the SUR-1817
    BASH_SOURCE[1] sed bug, or the SUR-1818 non-deterministic ordering bug.
    Issue proposes tests/pack-script.bats with four scenarios: end-to-end
    pack-script ... -h execution, no @include/includer.sh leakage in the
    packed output, no BASH_SOURCE[1] regression, and byte-identical repeated
    packs.
  • Rationale: pack-script is the single point of failure for the
    redistributable form of every command. A regression here ships broken
    binaries to consumers of dist/bin-*.tar.gz. This sprint includes the
    bootstrap of a tests/ tree (a coordinated dependency on SUR-1850 — see
    Risks) so subsequent sprints can grow coverage incrementally.
  • Definition of Done:
    • tests/pack-script.bats exists with at least the four cases listed in the
      issue (smoke run, no @include/includer.sh, no BASH_SOURCE[1],
      byte-identical repeat packs).
    • tests/ has a minimal harness so bats tests/ runs locally.
    • make test invokes bats tests/ (or a wrapper) and fails on a failed
      test, replacing the current no-op behaviour.
    • The smoke test passes for at least two distinct command scripts to prove
      coverage isn't accidentally pegged to one input.
    • The packed output assertions correctly fail when temporarily reintroducing
      the SUR-1817 BASH_SOURCE[1] regression (verified manually as part of the
      PR).
    • pre-commit run --all-files is clean.
    • PR description links the issue and explains how the new harness ties into
      Jenkins' make test step.
  • Dependencies / ordering: Soft dependency on SUR-1850 (introduce
    bats-core). To avoid blocking, this sprint will land a minimal bats
    bootstrap inside SUR-1852's PR (just enough to run tests/pack-script.bats)
    rather than waiting on the broader SUR-1850 effort. Should be the first
    issue picked up so subsequent sprints can build on the harness.

2. SUR-1838 — Anti-pattern: pagerduty.sh builds JSON via string interpolation; injectable on titles/keys

  • Priority: High
  • Description summary: pagerduty.sh send_incident interpolates
    $alert_title, $alert_type, $service_id, and $incident_key directly
    into a heredoc JSON body. Any " or backslash in an operator-supplied title
    produces invalid JSON; a crafted string can inject extra fields like
    "priority":"P1". A second smell: incident_data is conditionally
    defined
    inside an if [ -z "$incident_key" ] block, so subsequent calls
    in the same shell can pick up the wrong body.
  • Rationale: This is a real-world breakage waiting to happen — operators
    routinely paste rich incident titles containing quotes — plus a credible
    injection vector. Fix uses jq -n --arg ..., which is already a dependency
    via _jq elsewhere in the codebase.
  • Definition of Done:
    • bash/pagerduty.sh send_incident no longer interpolates user input into
      a JSON heredoc; body is built with jq -n --arg ....
    • incident_data is defined exactly once at file scope (no conditional
      redefinition).
    • Manual verification: a title containing " and \ produces a valid JSON
      body (capture the payload via a stubbed _curl, parse with jq .).
    • Unit test (or smoke test in the new bats harness from SUR-1852) asserts
      that incident_data invoked with a title containing " produces parseable
      JSON.
    • SUR-1821 is explicitly out of scope (it covers send_event being
      fully commented out and missing -X POST); the PR description links to
      SUR-1821 to make the boundary explicit.
    • pre-commit run --all-files is clean.
  • Dependencies / ordering: Independent of others. Best landed after
    SUR-1852 so its bats harness can host the JSON-validity unit test; can ship
    alongside without if scheduling forces it.

3. SUR-1834 — Bug: update-repo-tags BREAKING CHANGES detection misses feat!: and BREAKING-CHANGE: forms

  • Priority: Medium
  • Description summary: bash/update-repo-tags greps commit history for
    the literal BREAKING CHANGE, missing the equally-valid feat!:,
    feat(scope)!:, fix!: etc. subject form and the hyphenated
    BREAKING-CHANGE: footer variant. Teams using feat!: consistently silently
    get patch bumps for breaking releases, defeating semver. The script is
    invoked by Jenkins on main, so the bug ships incorrect versions
    organisation-wide.
  • Rationale: Direct correctness bug in the release tagger. One-line regex
    fix plus a follow-on for the git describe --abbrev=0 first-run failure
    the issue calls out. Low risk, high downstream value.
  • Definition of Done:
    • The BREAKING_CHANGES count uses git log --pretty=format:'%s%n%b' and a
      regex matching both ^[a-zA-Z]+(\([^)]+\))?!: (subject form) and
      BREAKING[ -]CHANGE: (footer form).
    • Initial-bootstrap path no longer hard-fails when no prior v* tag exists
      — emit a sensible default (e.g. v0.1.0) and continue, or at minimum log
      a clear message before exit.
    • Test fixture in tests/update-repo-tags.bats (or shared with SUR-1852's
      harness) covers: subject-only feat!:, scoped feat(api)!:, footer-only
      BREAKING CHANGE:, hyphenated BREAKING-CHANGE:, mixed, and
      no-breaking-changes baseline.
    • Manual run on a synthetic repo confirms detection.
    • pre-commit run --all-files is clean.
  • Dependencies / ordering: Depends on SUR-1852's bats bootstrap for the
    test fixture path. Land after SUR-1852.

4. SUR-1836 — Bug: git-check echoes ${PULLS} which is never assigned; brittle colon/comma string-state machine

  • Priority: Medium
  • Description summary: bash/git-check builds per-repo state into colon-
    and-comma-delimited strings (UNCOMMITTED, RELEASABLE, DEVELOPMENT),
    then formats them in output(). Line 214 has a leftover
    echo "${PULLS}" referencing a never-assigned variable (dead code from a
    refactor). The encoding is brittle: any field containing : or , would
    silently break parsing. Fix proposes Bash arrays + tab-delimited records.
  • Rationale: Concrete dead-code removal plus a robustness improvement on
    a script run interactively by developers. Modest scope, fits in one sprint.
  • Definition of Done:
    • echo "${PULLS}" is removed.
    • UNCOMMITTED / RELEASABLE / DEVELOPMENT are converted from delimited
      strings to indexed Bash arrays storing tab-delimited records (or another
      explicit structure-preserving encoding).
    • output() reads records via IFS=$'\t' read -r ... (or equivalent) and
      no longer relies on ${var//,/ } / ${token//:/ }.
    • The unused base_name arg in fetch_if_possible is removed or used
      purposefully.
    • Manual run against a directory containing one releasable, one dirty, and
      one development-branch repo produces the same human-readable output as
      before the refactor.
    • pre-commit run --all-files is clean (shellcheck/shfmt pass).
  • Dependencies / ordering: Independent. Can be picked up in parallel with
    SUR-1834.

5. SUR-1814 — Bug: aws.sh — list_findings uses bare jq; refresh_scan integer-compares a float timestamp

  • Priority: Medium
  • Description summary: Two bugs in bash/aws.sh. (1)
    aws::list_findings uses bare jq instead of _jq, bypassing the
    commands::use jq guard so a host without jq silently produces no output
    rather than a clear error. (2) aws::refresh_scan does
    [ "$earliest" -lt "$completedAt" ] against imageScanCompletedAt, which
    ECR returns as a float (1728310921.123); POSIX [ -lt ] is integer-only,
    so the comparison spuriously fails and the cache is defeated — every call
    triggers a fresh aws ecr start-image-scan.
  • Rationale: Two narrow, high-confidence bug fixes in a library that
    costs real AWS API calls when broken. One-line fix for (1); use
    jq ... | floor to extract an integer for (2).
  • Definition of Done:
    • aws::list_findings uses _jq -r ... consistent with the rest of the
      file.
    • aws::refresh_scan extracts an integer timestamp (e.g.
      _jq -r '.imageScanFindings.imageScanCompletedAt | floor') before the
      integer comparison, or otherwise uses a numeric tool that handles floats.
    • Unused local status declarations in aws::scan_status and
      _describe_findings are removed.
    • Manual smoke (or unit test in the new bats harness) confirms a sample
      floating-point timestamp parses correctly and the integer comparison no
      longer errors.
    • pre-commit run --all-files is clean.
  • Dependencies / ordering: Independent. Can land any time after
    SUR-1852's harness if a unit test is added; otherwise standalone.

Risks + Mitigations

  • Risk: SUR-1852 nominally depends on SUR-1850 (introduce bats-core),
    which is not in this sprint. Waiting on a separate full-blown SUR-1850 PR
    would block all five issues.
    Mitigation: Land a minimal bats bootstrap (Makefile make test
    target invoking bats tests/, plus a single tests/test_helper.bash)
    inside the SUR-1852 PR. Document in the PR that SUR-1850's broader
    framework (helpers, fixtures, CI integration) remains a separate ticket.
  • Risk: Adding make test semantics could break Jenkins on main if the
    initial bats run is flaky on the build host.
    Mitigation: Gate Jenkins changes behind a follow-up; in this sprint,
    make test runs the bats suite locally and via .github/workflows/, but
    Jenkins continues to invoke make test exactly as before — the suite must
    be green before the PR merges so the no-op-to-real transition is safe.
  • Risk: The BREAKING CHANGES regex fix in SUR-1834 may catch commits
    that previously slipped through, causing the next make publish on main
    to bump minor/major when teams expected patch.
    Mitigation: Document this in the SUR-1834 PR as an intentional
    behaviour change. Coordinate with stakeholders on main timing; if needed,
    land the fix immediately after a release rather than mid-cycle.
  • Risk: SUR-1838's jq -n rebuild changes the JSON body subtly (e.g.
    field ordering), which could affect any downstream consumer that screen-
    scrapes the request payload from logs.
    Mitigation: PagerDuty's API does not depend on field order; document
    the diff in the PR and verify the new payload still parses with jq . and
    is accepted by a sandbox API token before merging.
  • Risk: SUR-1836's array refactor of git-check could regress the
    human-readable output format that developers rely on.
    Mitigation: Capture the current output on a known set of repos before
    refactor, diff against post-refactor output, and treat any diff as a
    blocking review issue unless explicitly desired.
  • Risk: SUR-1814's floor approach assumes jq ≥ 1.5 (floor was added
    in 1.5).
    Mitigation: Confirm the project's jq baseline; if older versions
    must be supported, fall back to awk '{printf "%d", $0}' or cut -d. -f1
    on the float string.
  • Risk: Sprint scope is five issues, two with non-trivial test
    scaffolding. If SUR-1852's bats bootstrap turns up unexpected friction
    (Makefile portability, CI permissions), it could starve the other four.
    Mitigation: SUR-1852 is the first issue picked up. If the bootstrap
    takes longer than expected, the other four can land without the new bats
    unit tests (using only manual verification described in their DoD).

Out of Scope

  • SUR-1850 itself (full bats-core test framework) — only a minimal bootstrap
    ships with SUR-1852.
  • SUR-1851 (semver test coverage) — testing-themed but separate scope.
  • SUR-1821 (pagerduty send_event commented-out / -X POST) — explicitly
    deferred per SUR-1838's description.
  • SUR-1817 / SUR-1818 (the underlying pack-script bugs SUR-1852's tests
    would catch as regressions) — SUR-1852 only adds tests; the fixes are
    separate tickets if not already done.
  • All other Backlog issues (sub-issue follow-ups SUR-1855/1856, refactor
    SUR-1854, anti-patterns SUR-1840/1841/1842, docs SUR-1844/1845/1846/1847,
    bugs SUR-1824/1827/1830/1831/1833, simplification SUR-1848, CI migration
    SUR-1853).
  • Any change to Jenkinsfile, .github/workflows/pre-commit.yaml, or
    standard_defs.mk build infrastructure beyond the minimal make test
    hook for SUR-1852.

Linear Evidence

  • Linear team verified: Surinis (id ce9ebfde-ff2b-4f54-90f1-c388591ca110)
  • Linear project used: shell-scripts (id a43901a0-b02b-4009-aae1-a6e8903d127d)
  • Query / filter used: list_issues with team="Surinis",
    project="shell-scripts", state="Backlog", limit=100, then per-issue
    get_issue (with includeRelations=true), list_comments, and
    list_issues parentId=<id> for sub-issue dependency checks. Label filter
    applied client-side: labels array must not contain manual
    (case-insensitive).
  • Approx count of Backlog issues reviewed: 25
  • Approx count of manual-labelled Backlog issues skipped: 0

Sub-issue Status

None of the five selected issues have sub-issues. Verified via
list_issues parentId=<id> for each — all returned empty.

Parent Issue Sub-issue Sub-issue Status Eligible?
SUR-1852 (none) n/a Yes
SUR-1838 (none) n/a Yes
SUR-1834 (none) n/a Yes
SUR-1836 (none) n/a Yes
SUR-1814 (none) n/a Yes

No parent was skipped due to incomplete sub-issues.

Linear State Transitions

Issue ID Previous State New State
SUR-1852 Backlog Todo
SUR-1838 Backlog Todo
SUR-1834 Backlog Todo
SUR-1836 Backlog Todo
SUR-1814 Backlog Todo

…R-1852)

Adds tests/sur-1852.sh which packs two distinct command scripts
(bash/changelog and bash/clean-branches) and asserts:

  - pack-script exits 0 and produces a non-empty file
  - the packed body parses with bash -n
  - no @include directives leak into the packed output
  - no includer.sh references survive the pack
  - the packed binary prints SYNTAX when invoked with -h

Companion regressions are already covered by sibling tests:
sur-1817 for BASH_SOURCE[N] index rewriting and sur-1818 for
byte-identical repeated packs, satisfying the four scenarios in the
issue's DoD.

Wires the existing tests/run.sh harness into the Makefile via a new
test target so make test now fails on a failed bats-equivalent test
instead of being a no-op.
Removes the heredoc-based JSON construction in send_incident which
interpolated alert title, type, service id, and incident key directly
into the body. Any operator-supplied title containing a double-quote
or backslash produced invalid JSON, and a crafted title could inject
sibling fields (e.g. "priority":"P1") into the incident object.

incident_data is now defined exactly once at file scope (was previously
redefined inside an if-else for the incident_key branch, which leaked
across calls in the same shell). The optional incident_key is folded
in via jq's `if $key != "" then ... else . end` instead of a second
function definition.

Adds tests/sur-1838.sh covering JSON validity for nasty titles
(quotes, backslashes, newlines), title round-trip through jq, the
absence of incident_key when omitted, key passthrough when supplied,
and the injection-resistance assertion.

SUR-1821 (commented-out send_event / missing -X POST) remains out of
scope per its description.
…1834)

The previous breaking-change detector was `grep -c "BREAKING CHANGE"`
against the default git log output, which matched only the literal
footer with a space and missed:

  - Conventional Commits subject form: feat!:, fix(scope)!:, etc.
  - Hyphenated footer form: BREAKING-CHANGE:.

Teams using `feat!:` consistently silently received patch bumps for
breaking releases, defeating semver. The script is invoked by Jenkins
on main, so the bug shipped incorrect versions organisation-wide.

Detection now uses --pretty=format:'%s%n%b' so subject and body are
both visible, with -E regex `(^[a-zA-Z]+(\([^)]+\))?!:|BREAKING[ -]CHANGE:)`
matching subject ! form and either footer spelling.

Bootstrap path: the prior `git describe --tags --match 'v*' --abbrev=0`
hard-failed with `|| exit 1` when no v* tag existed. It now defaults to
v0.1.0 with a clear notice and the change-detection range falls back to
HEAD instead of $LATEST_TAG..HEAD.

CHANGES counter switched from grep -c "^commit" (relied on default log
output) to a hash-per-commit format, which is robust to log format
changes.

Adds tests/sur-1834.sh with seven scenarios: feat!:, feat(api)!:,
BREAKING CHANGE: footer, BREAKING-CHANGE: footer, mixed, no-breaking
baseline, and the no-prior-tag bootstrap case. Behaviour observed by
parsing the unconditional "Since X N changes M breaks" NOTICE; the
warn message itself is suppressed at default verbosity.

Behavioural note: the next make publish on main may now bump minor
where it previously bumped patch for histories that used `feat!:`.
This is an intentional correctness fix, called out in the sprint plan.
…836)

Replaces the colon/comma string-state machine with indexed Bash arrays
of tab-delimited records. UNCOMMITTED/RELEASABLE/DEVELOPMENT are now
arrays where each element is a `name TAB pr_count TAB branch TAB
version` record. Tabs are forbidden in any of those fields (paths and
branch names use forward-slashes), so the encoding is unambiguous.

The output() function reads each record via `IFS=$'\t' read -r ...`
and no longer relies on `${item//,/ }` / `${token//:/ }` substitution
to chunk fields, which would silently break for any field containing
either delimiter.

Removes the leftover `echo "${PULLS}"` from a previous refactor — the
variable is never assigned anywhere in the script.

Cleans up the unused `local base_name` declaration in
fetch_if_possible by collapsing the declare-then-assign into a single
line that uses the parameter directly.

Behaviour: the human-readable output preserves the same column order
(name, pr_count, branch, version) and the same color rules
(cyan/green for branch by name; red/green/yellow for version by
record-set). Per the DoD, manual verification against a directory of
one releasable, one dirty, and one development-branch repo confirms
identical layout.
…UR-1814)

aws::list_findings called bare `jq` instead of `_jq`, bypassing the
`commands::use jq` guard so a host without jq silently produced empty
output rather than a clear error. Switched to `_jq -r`, consistent
with the rest of the file.

aws::refresh_scan compared imageScanCompletedAt with `[ -lt ]`. ECR
returns that field as a float (e.g. 1728310921.123); POSIX `[ -lt ]`
is integer-only and rejected the comparison as "integer expression
expected", so the cache branch never short-circuited and every call
spuriously kicked off a fresh `aws ecr start-image-scan`. The
extraction now pipes through `floor` (jq >= 1.5) before the integer
comparison.

Removes the unused `local status` declarations in aws::scan_status
and _describe_findings.

Adds tests/sur-1814.sh which stubs _describe_findings,
aws::is_scan_complete, and aws::scan_image to assert:
  - a recent float-timestamp scan short-circuits with no rescan,
  - an old float-timestamp scan triggers exactly one rescan,
  - no "integer expression expected" leaks to stderr,
  - list_findings produces a row per synthetic finding.

The aws::scan_image stub uses a tempfile marker so calls survive the
$(...) subshell that captures refresh_scan's stderr.
@scealiontach
scealiontach marked this pull request as ready for review April 28, 2026 19:35
@scealiontach
scealiontach merged commit aeda714 into main Apr 28, 2026
1 check passed
@scealiontach
scealiontach deleted the sprint/2026-04-28-sprint-loop-5 branch April 28, 2026 19:41
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant