Skip to content

[bug] cut-standards-release.sh dies on bash 3.2 (${out,,}) mid-cut — leaves release published with no channel tag #1119

Description

@don-petry

Summary

scripts/cut-standards-release.sh aborts mid-write on bash 3.2 (the macOS system bash), leaving the release channel in a partially-cut state: the immutable release tag is created, the moving channel tag is not.

Hit on the very first real use of the script — cutting standards/v1.0.0 on 2026-09-13.

Reproduction

$ bash --version
GNU bash, version 3.2.57(1)-release (arm64-apple-darwin25)

$ bash scripts/cut-standards-release.sh cut v1.0.0 --commit 782e005f8359...
repo:      petry-projects/.github
release:   standards/v1.0.0 -> 782e005f8359
channel:   standards/v1-stable -> 782e005f8359
decision:  CREATE
creating immutable release standards/v1.0.0 at 782e005f8359...
moving channel standards/v1-stable onto 782e005f8359...
scripts/cut-standards-release.sh: line 275: ${out,,}: bad substitution

Cause

_gh_move_tag() (line ~275) lowercases the API error with ${out,,}:

out="$(gh api -X PATCH "repos/$repo/git/refs/tags/$tag" -f sha="$sha" -F force=true 2>&1)" && return 0
low="${out,,}"        # <-- bash 4+ only

${var,,} is a bash 4.0 case-modification expansion. On bash 3.2 it is a syntax error, and under set -euo pipefail the script dies immediately.

The expansion is only used for case-insensitive matching of a "not found" / 404 message to decide PATCH-vs-POST — so the failure happens on the create path, which is exactly the path a first-ever cut takes.

Why the partial state is the real problem

The script's stated contract is that cutting is "a scripted, idempotent, clobber-refusing operation instead of a one-off hand action". On bash 3.2 it is not atomic: it created standards/v1.0.0 and then died before creating standards/v1-stable. Observed state afterwards:

standards/v1.0.0     -> annotated tag -> 782e005f8359...   (created)
standards/v1-stable  -> 404                                 (missing)

An immutable release that consumers cannot reach through the channel they pin. And because the release tag is clobber-refusing by design, a naive re-run does not repair it — it correctly reports NOOP on the release while the channel stays absent.

In this instance the channel was completed manually via the same POST /git/refs call the script's own fallback performs, and the result verifies:

$ bash scripts/cut-standards-release.sh resolve
current:  standards/v1.0.0   (channel standards/v1-stable)

Acceptance criteria

  1. Runs on bash 3.2. Replace ${out,,} with a portable lowercase (tr '[:upper:]' '[:lower:]') or a case-insensitive match, and sweep the file for other bash 4+ constructs (${x^^}, declare -A, mapfile/readarray, &>>).
  2. A failed channel move does not leave a published release stranded. Either the cut is ordered/recoverable so a re-run completes the channel (the current NOOP path should still ensure the channel is aligned, which the dry-run text already claims it does), or the failure is reported loudly enough that the operator knows a manual step remains. Silent partial success is the defect.
  3. A regression test runs the script under bash 3.2 semantics, or CI lints for bash 4+ constructs in scripts/**. This script is operator-run from a workstation, so "CI uses newer bash" is not sufficient coverage — macOS is the likely execution environment.
  4. The existing published state is left intact. standards/v1.0.0 and standards/v1-stable both resolve to 782e005f8359... today and must not be clobbered or re-pointed by the fix.
  5. Idempotency is preserved and proven: re-running the cut at the same commit stays NOOP on the release and still converges the channel.

Out of scope

  • Changing the release/channel naming or the N-1 policy (#1448).
  • Automating when a cut happens — a human still decides that.

Dev Notes

  • Same portability class as the sed -i and date -u -d findings raised on #1742 and elsewhere: GNU-only constructs in scripts that are actually run on macOS. Worth checking whether scripts/lib/standards-release.sh (the pure core) has the same issue — the failure here was in the thin I/O glue, which is less well covered by tests/standards_release.bats.
  • canary-rollout.sh has a sibling _gh_move_tag; check whether it carries the same construct, since it is the pattern this script mirrored.

Observed 2026-09-13 cutting the first standards release (#1091 / PR #1094).

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugBug reportsdev-leadFor dev-lead agent pickup

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions