bug(release-prep): DRY_RUN early-check treats non-empty as truthy — DRY_RUN_INPUT='false' silently enables dry-run #70

Closed
opened 2026-06-25 19:12:47 +02:00 by quartermaster · 0 comments

Symptom

When the v0.4.0 _release.yml reusable fires on push:main with mode=update, release-prep.sh runs in DRY-RUN even though the operator never set dry_run=true. No rolling PR is created. Workflow reports success because release-prep.sh exits 0 normally.

Surfaced live during the v0.4.0 dogfood cut (slice 4): fragment-PR #69 merged → workflow fired → release-decide.sh correctly emitted mode=update, bump_level=minor, next_version=0.4.0 → act-step invoked release-prep.sh --rolling-mode --target-version 0.4.0 → release-prep silently ran in dry-run, printed [DRY-RUN] would: git checkout -B release-prep/rolling + METHOD POST summary, exited 0. No rolling PR opened. Workflow reported success at 6 seconds total elapsed.

Root cause (verified at source)

scripts/release-prep.sh lines 127-136:

# Env-var fallbacks (CLI args win)
if [[ "$BUMP_OVERRIDE_ARG" == "auto" && -n "${BUMP_OVERRIDE:-}" ]]; then
    BUMP_OVERRIDE_ARG="$BUMP_OVERRIDE"
fi
if [[ -z "$DRY_RUN" && -n "${DRY_RUN_ENV:-}${DRY_RUN_INPUT:-}" ]]; then
    DRY_RUN=1
fi
# Forgejo Actions workflow_dispatch sets DRY_RUN env var directly when
# the consumer wires it; respect any truthy value.
if [[ -n "${DRY_RUN:-}" && "$DRY_RUN" != "false" && "$DRY_RUN" != "0" ]]; then
    DRY_RUN=1
else
    DRY_RUN=""
fi

The first check (line 127) treats any non-empty DRY_RUN_INPUT or DRY_RUN_ENV as a request to enable dry-run, regardless of value. It sets DRY_RUN=1.

The second check (line 132) then sanitizes against "false" or "0"but it operates on $DRY_RUN (now "1"), not on the original DRY_RUN_INPUT. So "1" != "false"DRY_RUN stays 1. The sanitization is structurally unable to catch the DRY_RUN_INPUT="false" case.

_release.yml's workflow_call inputs declare dry_run with default: 'false'. For push events (where dry_run is not explicitly set), Forgejo Actions renders ${{ inputs.dry_run }} as the default string 'false'. This is passed to the act-step as DRY_RUN_INPUT="false" → trips line 127 → silent dry-run.

Why slice 2's merge didn't reveal this

Slice 2's first-fire was mode=noop (no release-relevant content since v0.3.5). The act step short-circuits noop before invoking release-prep.sh, so the dry-run-when-input-false bug never executes on noop runs. Only manifests when mode=update or mode=cut fires.

Tested fix

The first check should use truthy semantics matching the second check, OR the second check should sanitize the SOURCE env vars before promoting to DRY_RUN. Cleanest: merge both checks into a single truthy probe across sources:

# Combined truthy check across all env-var sources
for raw in "${DRY_RUN_ENV:-}" "${DRY_RUN_INPUT:-}"; do
    if [[ -n "$raw" && "$raw" != "false" && "$raw" != "0" ]]; then
        DRY_RUN=1
    fi
done
# Also accept direct DRY_RUN env var (workflow_dispatch may set it that way)
if [[ -n "${DRY_RUN:-}" && "$DRY_RUN" != "false" && "$DRY_RUN" != "0" ]]; then
    DRY_RUN=1
else
    DRY_RUN=""
fi

Sibling silent-failure-class lessons

  • #41: pull_request.closed expression-engine fail-opens silently
  • #56: manifest-vs-tag-vs-history silent desync
  • #66: prerelease-tag-walk silent window-shift (closed in v0.3.5)
  • #70 (this): DRY_RUN_INPUT='false' silent dry-run promotion

n=4 in the silent-failure-class series. Per the logical-vs-empirical promotion criterion (Surveyor 5898), the empirical path to memory-promotion is n=3 structurally-distinct instances. Four instances of "silent-wrong from substrate misinterpretation" might warrant promoting to a project-memory pin now.

Sequencing knot (same as #66)

_release.yml@v0.4.0-rc.1 and _release-prep.yml@v0.3.5 both contain the buggy script. The fix must land in release-prep.sh, ship as a new patch tag (v0.3.6), then _release.yml consumers re-pin via a new bootstrap-tag v0.4.0-rc.2 at v0.3.6's SHA.

Refs

  • Blocks: v0.4.0 dogfood cut (slice 4 of #52)
  • Sibling silent-failure-class: #41, #56, #66
  • Surfacing run: actions/runs/131 (workflow status=success at 6s; release-prep.sh ran in silent dry-run)

Filed: 2026-06-25 from live dogfood-catch during v0.4.0 cut attempt (the architectural arc surfacing yet another substrate bug at the substrate level).

## Symptom When the v0.4.0 `_release.yml` reusable fires on `push:main` with mode=update, `release-prep.sh` runs in DRY-RUN even though the operator never set dry_run=true. No rolling PR is created. Workflow reports `success` because release-prep.sh exits 0 normally. Surfaced live during the v0.4.0 dogfood cut (slice 4): fragment-PR #69 merged → workflow fired → release-decide.sh correctly emitted `mode=update, bump_level=minor, next_version=0.4.0` → act-step invoked `release-prep.sh --rolling-mode --target-version 0.4.0` → release-prep silently ran in dry-run, printed `[DRY-RUN] would: git checkout -B release-prep/rolling` + `METHOD POST` summary, exited 0. No rolling PR opened. Workflow reported success at 6 seconds total elapsed. ## Root cause (verified at source) `scripts/release-prep.sh` lines 127-136: ```bash # Env-var fallbacks (CLI args win) if [[ "$BUMP_OVERRIDE_ARG" == "auto" && -n "${BUMP_OVERRIDE:-}" ]]; then BUMP_OVERRIDE_ARG="$BUMP_OVERRIDE" fi if [[ -z "$DRY_RUN" && -n "${DRY_RUN_ENV:-}${DRY_RUN_INPUT:-}" ]]; then DRY_RUN=1 fi # Forgejo Actions workflow_dispatch sets DRY_RUN env var directly when # the consumer wires it; respect any truthy value. if [[ -n "${DRY_RUN:-}" && "$DRY_RUN" != "false" && "$DRY_RUN" != "0" ]]; then DRY_RUN=1 else DRY_RUN="" fi ``` The first check (line 127) treats **any non-empty `DRY_RUN_INPUT` or `DRY_RUN_ENV`** as a request to enable dry-run, regardless of value. It sets `DRY_RUN=1`. The second check (line 132) then sanitizes against `"false"` or `"0"` — **but it operates on `$DRY_RUN` (now `"1"`)**, not on the original `DRY_RUN_INPUT`. So `"1" != "false"` → `DRY_RUN` stays `1`. The sanitization is structurally unable to catch the `DRY_RUN_INPUT="false"` case. `_release.yml`'s workflow_call inputs declare `dry_run` with `default: 'false'`. For push events (where dry_run is not explicitly set), Forgejo Actions renders `${{ inputs.dry_run }}` as the default string `'false'`. This is passed to the act-step as `DRY_RUN_INPUT="false"` → trips line 127 → silent dry-run. ## Why slice 2's merge didn't reveal this Slice 2's first-fire was `mode=noop` (no release-relevant content since v0.3.5). The act step short-circuits noop before invoking release-prep.sh, so the dry-run-when-input-false bug never executes on noop runs. Only manifests when mode=update or mode=cut fires. ## Tested fix The first check should use truthy semantics matching the second check, OR the second check should sanitize the SOURCE env vars before promoting to `DRY_RUN`. Cleanest: merge both checks into a single truthy probe across sources: ```bash # Combined truthy check across all env-var sources for raw in "${DRY_RUN_ENV:-}" "${DRY_RUN_INPUT:-}"; do if [[ -n "$raw" && "$raw" != "false" && "$raw" != "0" ]]; then DRY_RUN=1 fi done # Also accept direct DRY_RUN env var (workflow_dispatch may set it that way) if [[ -n "${DRY_RUN:-}" && "$DRY_RUN" != "false" && "$DRY_RUN" != "0" ]]; then DRY_RUN=1 else DRY_RUN="" fi ``` ## Sibling silent-failure-class lessons - **#41**: pull_request.closed expression-engine fail-opens silently - **#56**: manifest-vs-tag-vs-history silent desync - **#66**: prerelease-tag-walk silent window-shift (closed in v0.3.5) - **#70 (this)**: DRY_RUN_INPUT='false' silent dry-run promotion n=4 in the silent-failure-class series. Per the logical-vs-empirical promotion criterion (Surveyor 5898), the *empirical* path to memory-promotion is n=3 structurally-distinct instances. Four instances of "silent-wrong from substrate misinterpretation" might warrant promoting to a project-memory pin now. ## Sequencing knot (same as #66) `_release.yml@v0.4.0-rc.1` and `_release-prep.yml@v0.3.5` both contain the buggy script. The fix must land in `release-prep.sh`, ship as a new patch tag (v0.3.6), then `_release.yml` consumers re-pin via a new bootstrap-tag `v0.4.0-rc.2` at v0.3.6's SHA. ## Refs - Blocks: v0.4.0 dogfood cut (slice 4 of [#52](https://git.frankenbit.de/frankenbit/release-toolkit/issues/52)) - Sibling silent-failure-class: [#41](https://git.frankenbit.de/frankenbit/release-toolkit/issues/41), [#56](https://git.frankenbit.de/frankenbit/release-toolkit/issues/56), [#66](https://git.frankenbit.de/frankenbit/release-toolkit/issues/66) - Surfacing run: actions/runs/131 (workflow status=success at 6s; release-prep.sh ran in silent dry-run) Filed: 2026-06-25 from live dogfood-catch during v0.4.0 cut attempt (the architectural arc surfacing yet another substrate bug at the substrate level).
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
frankenbit/release-toolkit#70
No description provided.