[Sprint] sprint-loop-41#37
Merged
Merged
Conversation
The previous grep '@Package' also matched 'function @Package() {' in doc.sh, yielding '{' as the package name. Combined with the unreachable "$package" != '.' sentinel, the no-package branch never fired — bashadoc bash/doc.sh emitted a '`{`' header with garbage markdown, which is what currently ships in dist/doc-*.tar.gz for doc.md. Anchor extraction on a leading-whitespace '@Package' directive (field 2) and detect the no-package case with [ -n "$package" ] so the filename-derived fallback actually runs. Adds tests/bashadoc.bats covering: a library with @Package, a script without @Package, and bash/doc.sh itself (which defines function @Package and would otherwise emit '{'). Verified by re-tarballing dist/doc-*.tar.gz and inspecting doc/bash/doc.md.
git::project_url previously matched any remote starting with 'origin' (origin-fork, origin-mirror, origin2, ...) and used 'head -1' over the interleaved 'git remote -v' output, making the result non-deterministic when multiple remotes were configured. Switch to 'git config --get remote.origin.url' for an exact-name lookup, with an awk fallback that still scopes to the literal 'origin' fetch URL in the unlikely event 'git config' returns empty. Affects CHANGELOG generation (changelog) and release tagging (update-repo-tags) — both call git::project_url to compose links. Adds a regression to tests/git.bats covering an origin/origin-fork pair.
secret::clear gated removal on [ -n "${SECRET_TMPFILES[0]}" ]. A
sparse array (element 0 unset, later indices populated by 'unset
'SECRET_TMPFILES[0]') makes the guard fail and the cleanup silently
no-op, leaving 0600 tempfiles with decrypted secret material under
$TMPDIR until OS-level /tmp cleanup runs. Direct violation of the
'secrets never touch persistent storage' invariant from SUR-2324.
Switch to [ "${#SECRET_TMPFILES[@]}" -gt 0 ], reset SECRET_TMPFILES=()
after removal, and add a sparse-array regression to tests/secret.bats.
Audit confirmed no caller relies on post-clear array state.
exec::capture installed 'trap rm -f "$tmpout" RETURN' but never cleared it. The trap persisted into the caller's scope and, because $tmpout is a function-local, fired as a no-op 'rm -f ""' on every subsequent return. The real damage: the RETURN trap slot is permanently occupied, so any caller-installed RETURN trap is silently clobbered (last-writer-wins). Latent foot-gun contradicting the chained-trap discipline SUR-2324 established for secret.sh. Clean tmpout inline immediately after reading it; no trap installation. The exit code is already captured into $exit_code from PIPESTATUS[0] before cleanup, so propagation is preserved. Adds two regressions to tests/exec.bats: - after exec::capture returns, 'trap -p RETURN' is empty in the caller. - a caller-installed RETURN trap still fires after exec::capture.
bash/copy-keys passed -n NAMESPACE only to the initial k8s::pod_names_for_label call. The downstream 'k8s::ctl get pod' and 'k8s::ctl cp' invocations omitted the namespace, so the script read pod metadata and copied key material from whatever pod (if any) shared the same name in the current-context default namespace. Worst case: broken symlinks, empty key files, or key material copied from an unrelated pod. Build ns_args=() once at the top, mirroring the fetch-all-pod-logs pattern, and expand it into every downstream k8s::ctl call. Extends tests/copy-keys.bats with two assertions: with -n my-ns every recorded kubectl invocation carries '-n my-ns'; without -n no invocation carries a stray namespace flag.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Sprint Plan — 2026-05-12 (sprint-loop-41)
Sprint goal
Eliminate five high-priority correctness defects in shared
bash/librariesand command scripts that today silently corrupt output, leak resources, or
target the wrong namespace/remote. The selected work is a coherent
"library correctness" theme — each issue is a contained, well-specified bug
fix with concrete suggested fixes already in the issue body, allowing the
sprint to land observable behaviour fixes plus bats regression coverage
without scope creep.
Selected issues
SUR-2827 — Bug: copy-keys ignores -n NAMESPACE on follow-up kubectl get/cp calls
bash/copy-keysthreads-n NAMESPACEinto theinitial
k8s::pod_names_for_labelcall but every downstreamk8s::ctl get podandk8s::ctl cpinvocation (lines ~34–50) omits thenamespace, so the script reads pod metadata and copies key material from
whatever pod (if any) shares that name in the current-context default
namespace. Result: broken symlinks, empty key files, or — worst case —
copying keys from an unrelated pod.
material extraction tool; the fix is well-scoped (one file, mirror the
fetch-all-pod-logsns_args=()pattern).ns_args=()array built once at top ofbash/copy-keysand applied toevery
k8s::ctlcall that operates on a pod by name.-n foo, allkubectloperations target namespacefoo.-n, behaviour is unchanged from today (no spurious-n).tests/copy-keys.batsmocksk8s::ctland asserts thenamespace flag is forwarded on every call.
pre-commit run --all-filesandmake testpass.bash/copy-keysonly.SUR-2828 — Bug: git::project_url matches any remote starting with "origin"
git::project_url(bash/git.shlines 54–68)resolves the origin URL via
git remote -v | grep "^origin" | head -1;this matches
origin-fork,origin-mirror,origin2, etc. Becausegit remote -vlists each remote twice and ordering between distinctremotes is not specified,
head -1is non-deterministic. The CHANGELOGgenerator (
changelog) andupdate-repo-tagsuse this base URL — amisidentified origin silently links every CHANGELOG entry at the wrong
repository.
unless audited, and the suggested fix (
git config --get remote.origin.url)is one line.
git::project_urlresolves the URL viagit config --get remote.origin.url(preferred) or a tightened
awk '$1=="origin" ...'selector.git::projecturl/git::commit_url_basecontinueto forward correctly.
tests/git.batsgains a regression case proving that adding anorigin-forkremote no longer pollutesgit::project_url.pre-commit run --all-filesandmake testpass.SUR-2829 — Bug: bashadoc emits wrong function list when input has no @Package directive
bash/bashadocextracts the package name withgrep "@package" "$1" | awk '{print $NF}' | tail -1. For files withoutan
@packageannotation,$packageis either empty or{(because thegrep matches
function @package() {indoc.sh). The downstreambranch checks
[ "$package" != "." ], where"."is a sentinel that isnever assigned — so the "no package" branch is unreachable. Result:
bashadoc bash/doc.shemits a# `{` packageheader with zerofunctions, and any future helper without
@packagesilently shipsgarbage markdown into
dist/doc-*.tar.gz.the worst failure mode.
awk '/^[[:space:]]*@package /{print $2}'.-n "$package"test, not the unreachable"."sentinel.bashadoc bash/doc.shproduces a header derived from the filename anda non-empty (or correctly empty) function list with no
`{`artifacts.tests/bashadoc.batsspec covers: (a) library with@package,(b) script without
@package, (c)doc.sh(matchesfunction @package).pre-commit run --all-filesandmake testpass.(bashadoc bats coverage), but SUR-2835 is out of scope for this sprint.
SUR-2830 — Bug: secret::clear only checks SECRET_TMPFILES[0]; sparse arrays leak tempfiles
secret::clearinbash/secret.sh(~lines197–202) guards removal with
[ -n "${SECRET_TMPFILES[0]}" ]. A sparsearray (element 0 unset, later indices populated) makes the guard fail and
the cleanup silently no-ops, leaving 0600 tempfiles containing decrypted
secret material under
$TMPDIRuntil OS-level/tmpcleanup.storage" invariant established by SUR-2324; tiny fix; high security
blast radius if the brittle guard ever encounters a sparse array.
secret::clearuses[ "${#SECRET_TMPFILES[@]}" -gt 0 ]and resetsSECRET_TMPFILES=()after removal.tests/secret.batsgains a sparse-array regression: pre-seedSECRET_TMPFILES=("$tmp0" "$tmp1"),unset 'SECRET_TMPFILES[0]', runsecret::clear, assert$tmp1no longer exists.pre-commit run --all-filesandmake testpass.SUR-2831 — Anti-pattern: exec::capture installs a RETURN trap that leaks into the caller's scope
exec::capture(bash/exec.sh~line 34) installstrap 'rm -f "$tmpout"' RETURNand never clears it. The trap fires onevery subsequent function return in the caller's chain and, because
$tmpoutislocal, becomes a no-oprm -f "". But the trap slot ispermanently occupied: any caller that installs its own RETURN trap loses
it (last-writer-wins). Contradicts the chained-trap discipline added in
SUR-2324 for
secret.sh.(inline cleanup instead of trap, matching the no-tempfile branch of the
same function) is small and removes a class of debugging headaches.
exec::capturecleans up$tmpoutinline (noRETURNtrap), orexplicitly
trap - RETURNbefore returning.exec::capture, a caller-installedtrap '...' RETURNstill fires as expected (covered by a bats regression).
tests/exec.bats(or new file if absent) asserts noRETURNtrap isleft installed after
exec::capturereturns.pre-commit run --all-filesandmake testpass.Risks + mitigations
git::project_urlchange togit config --get remote.origin.urlcould differ from
git remote -vparsing in edge cases (insteadOf URLrewriting). Mitigation: keep
awk '$1=="origin" && $3=="(fetch)"'asa fallback if
git configreturns empty; cover both paths in bats.kubectl/gitmay be flaky oncontributor machines with unusual
PATH. Mitigation: usehelpers::source_lib+ isolated$HOME(helpers::isolate_home) anddeclare mock functions before sourcing the library under test.
exec::capturemay need to handle exit codepreservation (
local rc=$?). Mitigation: explicitlocal rc=$?before
rm -f, return$rc; bats spec asserts both stdout capture andexit code propagation.
bashadocregex change might break correctly-annotatedlibraries that put
@packagemid-line. Mitigation: anchor on^[[:space:]]*@packageand add a positive-case bats fixture for everylibrary currently in
bash/.secret::clearreset ofSECRET_TMPFILES=()might break aconsumer that re-reads the array post-clear. Mitigation: audit
callers (
grep -rn SECRET_TMPFILES bash/); none today rely onpost-clear state.
capacity if any one fix uncovers a deeper issue. Mitigation: each
issue is independent — a stretched item can be dropped without
blocking the others.
Out of scope
"testing" theme).
update-repo-tags,make publish).secret.shbeyond thesecret::clearguard fix.Linear Evidence
ce9ebfde-ff2b-4f54-90f1-c388591ca110)a43901a0-b02b-4009-aae1-a6e8903d127d)list_issues(project="shell-scripts", state="Backlog", limit=100)then per-issue
get_issue(includeRelations=true),list_issues(parentId=...),and
list_comments(issueId=...).Sub-issue Status
No selected parent issue has sub-issues. Verified via
list_issues(parentId=<SUR-ID>)for each of SUR-2827, SUR-2828, SUR-2829,SUR-2830, SUR-2831 — all returned empty.
Linear State Transitions