Skip to content

Gate the release workflow on the QA ladder - #110

Merged
EONRaider merged 1 commit into
masterfrom
claude/decoder-depth-polish-8m54to
Sep 1, 2026
Merged

Gate the release workflow on the QA ladder#110
EONRaider merged 1 commit into
masterfrom
claude/decoder-depth-polish-8m54to

Conversation

@EONRaider

@EONRaiderEONRaider commented Sep 1, 2026

Copy link
Copy Markdown
Owner

Summary

Closes#78.

Pushing a v* tag ran uv build and uv publish with no dependency on
the lint, typecheck or test jobs. The problem is worse than the issue
described: ci.yml triggers only on push/pull_request targeting
master, and a tag push matches neither, so a release ran no
checks at all
— there was not even a CI run to depend on. A tag against
broken code published to PyPI unchallenged, and a published version can
only be yanked, never replaced.

Cross-workflow needs: does not exist, so the release workflow has to
run the checks itself. Rather than duplicating the four jobs — and
letting the release gate drift away from the pull-request gate — ci.yml
gains a workflow_call trigger and release.yml invokes it against the
tagged commit, with publish gated behind it. One definition serves both.

What's included

  • .github/workflows/ci.yml — adds a workflow_call: trigger. Existing
    push/PR triggers are unchanged.
  • .github/workflows/release.yml — a ci job calling
    ./.github/workflows/ci.yml, and needs: ci on publish. Adds a
    workflow-level permissions: contents: read; publish keeps its
    id-token: write for trusted publishing.
  • tests/test_workflows.py — four assertions that the gate stays wired.
  • CHANGELOG.md — entries under ## [Unreleased].

Verification

  • uv run ruff check and uv run ruff format --check are clean
  • uv run mypy is clean (strict)
  • uv run pytest passes locally — 702 tests, 4 new
  • CHANGELOG.md has an entry under ## [Unreleased]

The gate cannot be verified by running it, because publishing is
irreversible. So each assertion was checked against a deliberately
broken workflow to confirm it fails when the gate is actually gone:

MutationCaught by
Remove workflow_call: from ci.ymltest_ci_workflow_is_callable, test_every_local_reusable_reference_resolves
Delete the ci gate jobtest_release_calls_the_ci_workflow, test_publish_depends_on_the_ci_gate
Remove needs: ci from publishtest_publish_depends_on_the_ci_gate
Point uses: at a missing workflowall three of the above

Worth recording: actionlint passes every one of those mutations.
It does not resolve local reusable-workflow references, so it reports a
missing target file and a missing workflow_call trigger as clean. Its
green result on this PR validates syntax, not wiring — which is why the
tests exist.

Notes

Two deliberate non-changes:

  • uv build still runs twice (once in the CI build job, once in
    publish). Sharing an artifact across a reusable-workflow boundary
    costs more complexity than a rebuild from the same commit is worth,
    and publishing from the checkout it just built is the safer default.
  • The gate re-runs CI for a commit already green on master. That is
    the point — a tag can name any commit, including one that never landed
    on master.

Follow-up filed as #111: a tag whose version disagrees with
pyproject.toml is a separate hole this PR does not close, and one the
QA ladder structurally cannot catch, since the code is fine and only the
tag is wrong.

🤖 Generated with Claude Code

https://claude.ai/code/session_01QJnVMNGwTRDktC4rkABtgt

Pushing a v* tag ran uv build and uv publish with no dependency on the
lint, typecheck or test jobs. Worse, the CI workflow triggers only on
pushes and pull requests targeting master, so a tag push matched
neither and ran no checks at all -- a tag against broken code published
to PyPI unchallenged, and a published version can only be yanked, never
replaced.
Cross-workflow "needs" does not exist, so gating requires the release
workflow to run the checks itself. Rather than duplicating the four
jobs and letting the release gate drift from the pull-request gate,
ci.yml gains a workflow_call trigger and release.yml invokes it against
the tagged commit, with publish gated behind it. One definition serves
both.
The wiring is easy to break silently -- drop needs:, or workflow_call:,
and releases keep working while the gate stops existing -- and it
cannot be verified by running it, because publishing is irreversible.
So tests/test_workflows.py asserts each half of it. Each assertion was
checked against a mutated workflow to confirm it fails when the gate is
actually broken; actionlint does not resolve local reusable-workflow
references and passes all of those mutations.
Closes#78
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QJnVMNGwTRDktC4rkABtgt
@EONRaider
EONRaider merged commit da46b27 into masterSep 1, 2026
6 checks passed
@EONRaiderEONRaider mentioned this pull request Sep 4, 2026
Sign up for freeto 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.

Gate the release workflow on the CI jobs

2 participants

@EONRaider@claude