bug(mirror): if: cut gate fires on non-cut push:main events (post-#319 empirical) #322

Closed
opened 2026-07-03 12:49:33 +02:00 by quartermaster · 1 comment

Empirical anchor

After #319 merged (introducing the mirror job in .forgejo/workflows/release.yml gated on ${{ needs.release.outputs.mode == 'cut' }}), the Forgejo Actions history shows 2× mirror: failure runs on sha=31970151e0a7332fc005ad6cd86fdc95ced9e997 — the merge commit of #319 to main, which was a mode=update event (rolling PR update, not a cut).

The mirror job should have been SKIPPED via the if: needs.release.outputs.mode == 'cut' gate. Instead it fired + failed. The cut path itself worked cleanly (v0.25.0 shipped fine + Codeberg auto-mirror on the REAL cut commit 97bb8076 shows mirror: success), so the failure is dispatch-scope noise, not substrate-of-record corruption.

Hypothesis

  • (a) Forgejo Actions may evaluate if: expressions differently on job-level workflow_call gating vs GitHub Actions equivalents
  • (b) needs.release.outputs.mode may resolve to empty string OR the literal ${{ ... }} unevaluated on some evaluation paths, making the == 'cut' compare unexpectedly
  • (c) The mirror job's continue-on-error: true may cause "failure" display even on skipped state (unlikely — skipped ≠ failed in Actions)

Investigation surface

  • Read the actual mirror-workflow run logs at sha=31970151 — what did the if: evaluation see? What step failed?
  • Compare with the successful cut-commit run at sha=97bb8076 — how does the evaluation path differ?
  • Check Forgejo Actions vs GitHub Actions semantics on job-level if: with needs.<job>.outputs reference

Fix direction (post-diagnosis)

  • Sharpen the if: to something less ambiguous
  • Or gate at earlier step-level rather than job-level
  • Or accept the noise as harmless (mirror-failure isn't cut-failure due to continue-on-error: true; but noise floor is a real cost)

Priority

priority/low size/S — the substrate is behaviorally correct (real cut fires mirror clean; non-cut fires get warned but don't propagate). This is quality-of-workflow-status polish, not correctness. Don't gate v1.0.0 on it.

  • #319 — introduced the mirror job (this tracker's follow-up scope)
  • Bosun 2e68 bus-catch (2026-07-03 12:46) — dispatcher surfaced the pre-cut noise
  • QM bef7 bus-flag (2026-07-03 12:42) — first-observation flag

Anchor

Set I close, post-v0.25.0 first-live-exercise. Noise observed but doesn't gate the cut path.

## Empirical anchor After #319 merged (introducing the `mirror` job in `.forgejo/workflows/release.yml` gated on `${{ needs.release.outputs.mode == 'cut' }}`), the Forgejo Actions history shows 2× `mirror: failure` runs on `sha=31970151e0a7332fc005ad6cd86fdc95ced9e997` — the merge commit of #319 to main, which was a **mode=update** event (rolling PR update, not a cut). The mirror job should have been SKIPPED via the `if: needs.release.outputs.mode == 'cut'` gate. Instead it fired + failed. The cut path itself worked cleanly (v0.25.0 shipped fine + Codeberg auto-mirror on the REAL cut commit `97bb8076` shows `mirror: success`), so the failure is dispatch-scope noise, not substrate-of-record corruption. ## Hypothesis - (a) Forgejo Actions may evaluate `if:` expressions differently on job-level workflow_call gating vs GitHub Actions equivalents - (b) `needs.release.outputs.mode` may resolve to empty string OR the literal `${{ ... }}` unevaluated on some evaluation paths, making the `== 'cut'` compare unexpectedly - (c) The mirror job's `continue-on-error: true` may cause "failure" display even on `skipped` state (unlikely — skipped ≠ failed in Actions) ## Investigation surface - Read the actual mirror-workflow run logs at `sha=31970151` — what did the `if:` evaluation see? What step failed? - Compare with the successful cut-commit run at `sha=97bb8076` — how does the evaluation path differ? - Check Forgejo Actions vs GitHub Actions semantics on job-level `if:` with `needs.<job>.outputs` reference ## Fix direction (post-diagnosis) - Sharpen the `if:` to something less ambiguous - Or gate at earlier step-level rather than job-level - Or accept the noise as harmless (mirror-failure isn't cut-failure due to `continue-on-error: true`; but noise floor is a real cost) ## Priority `priority/low` `size/S` — the substrate is behaviorally correct (real cut fires mirror clean; non-cut fires get warned but don't propagate). This is quality-of-workflow-status polish, not correctness. Don't gate v1.0.0 on it. ## Related - #319 — introduced the `mirror` job (this tracker's follow-up scope) - Bosun `2e68` bus-catch (2026-07-03 12:46) — dispatcher surfaced the pre-cut noise - QM `bef7` bus-flag (2026-07-03 12:42) — first-observation flag ## Anchor Set I close, post-v0.25.0 first-live-exercise. Noise observed but doesn't gate the cut path.
Author
Owner

Sharper diagnosis (Surveyor c778, 2026-07-03)

Original filing had two hypothesis gaps that Surveyor's Actions-task-list dive resolved:

The is not a double-fire — it's the reusable-call representation. A uses: job in Forgejo Actions shows as caller-job "mirror" + the reusable's internal job "mirror" = 2 rows per invocation.

Real finding: mirror runs on EVERY push:main; the if: mode == 'cut' gate isn't filtering. Evidence: mirror ran on 31970151 (#319 merge, non-cut → FAILURE from empty TAG_NAME → Forgejo-fetch-fail) AND on 97bb8076 (rolling-PR push, non-cut → SUCCESS from idempotency-or-empty-PAT path).

Hypothesis (unverified pending run-log): Forgejo Actions likely doesn't honor needs.<reusable-call-job>.outputs in a job-level if: the way GitHub does — so needs.release.outputs.mode == 'cut' doesn't skip non-cuts.

Fail-safe holds — this is noisy not dangerous. The tag-guard + Forgejo-fetch-fail path catch the empty-TAG case exactly as designed. Wrong-commit class stays closed; symptom is spurious workflow failures cluttering Actions on non-cut pushes.

Immediate mitigation (filed as follow-up PR): empty-TAG_NAME guard as FIRST check in the reusable workflow — Forgejo-agnostic script-level gate. Sidesteps the outputs-based gating question by not depending on it.

Broader question this tracker stays open on: does Forgejo Actions populate reusable-call outputs at all? If not, the outputs-based gating pattern needs revisiting repo-wide — but that's post-v1.0 investigation, not immediate.

Refs: Surveyor c778 Actions-task-list evidence (2026-07-03 12:49 post-v0.25.0-cut analysis), QM bef7 first-flag, Bosun 2e68 dispatch to me for this filing.

## Sharper diagnosis (Surveyor `c778`, 2026-07-03) Original filing had two hypothesis gaps that Surveyor's Actions-task-list dive resolved: **The `2×` is not a double-fire** — it's the reusable-call representation. A `uses:` job in Forgejo Actions shows as caller-job "mirror" + the reusable's internal job "mirror" = 2 rows per invocation. **Real finding**: mirror runs on EVERY push:main; the `if: mode == 'cut'` gate isn't filtering. Evidence: mirror ran on `31970151` (#319 merge, non-cut → FAILURE from empty TAG_NAME → Forgejo-fetch-fail) AND on `97bb8076` (rolling-PR push, non-cut → SUCCESS from idempotency-or-empty-PAT path). **Hypothesis (unverified pending run-log)**: Forgejo Actions likely doesn't honor `needs.<reusable-call-job>.outputs` in a job-level `if:` the way GitHub does — so `needs.release.outputs.mode == 'cut'` doesn't skip non-cuts. **Fail-safe holds** — this is noisy not dangerous. The tag-guard + Forgejo-fetch-fail path catch the empty-TAG case exactly as designed. Wrong-commit class stays closed; symptom is spurious workflow failures cluttering Actions on non-cut pushes. **Immediate mitigation** (filed as follow-up PR): empty-TAG_NAME guard as FIRST check in the reusable workflow — Forgejo-agnostic script-level gate. Sidesteps the outputs-based gating question by not depending on it. **Broader question this tracker stays open on**: does Forgejo Actions populate reusable-call outputs at all? If not, the outputs-based gating pattern needs revisiting repo-wide — but that's post-v1.0 investigation, not immediate. Refs: Surveyor `c778` Actions-task-list evidence (2026-07-03 12:49 post-v0.25.0-cut analysis), QM `bef7` first-flag, Bosun `2e68` dispatch to me for this filing.
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#322
No description provided.