Skip to content

[CI/CD] Gate every release on the exact commit that passed CI #220

Description

@fettpl

Audit metadata

  • Priority: P1
  • Estimated effort: M
  • Implementation risk: MED
  • Category: bug
  • Evidence baseline: 07be5be7ce69bea0c3118744ab90d148b010fce0 (origin/main on 2026-07-17)

Dependencies

None.

Description and impact

Release 2.0 is published by a workflow that is independent of the repository's
CI workflow. A push to main can therefore begin semantic-release even when a
gate for that same commit is still running or ultimately fails. The release job
must be a downstream consumer of all required gates, and the frontend gate must
exercise the canonical production build rather than a weaker subset.

Current state

  • .github/workflows/ci.yml:3-6 runs for pull requests and pushes to main.
  • .github/workflows/ci.yml:60-71 runs frontend static checks and tests, but
    does not invoke the canonical check:quality command that includes next build.
  • .github/workflows/release.yml:3-39 independently runs on every push to
    main and invokes semantic-release with write permissions.
  • .github/workflows/ci.yml has no explicit read-only token default. When the
    release becomes a called workflow, its token permissions can only retain or
    reduce what the caller job grants; declarations in release.yml cannot
    elevate a read-only caller.
  • frontend/package.json:43 defines check:quality as the complete frontend
    gate, including lint, both typechecks, architecture checks, and build.
  • AGENTS.md requires conventional commits and says semantic-release on main
    is the only supported release mechanism.

This is a workflow-ordering defect, not a request to change semantic versioning
or bypass branch protection.

Steps to reproduce

  1. Inspect the push triggers and job dependencies in
    .github/workflows/ci.yml and .github/workflows/release.yml.
  2. Confirm that both workflows start independently for a push to main and
    that the release workflow has no dependency on the CI run for the same SHA.
  3. Compare the frontend CI commands with check:quality in
    frontend/package.json and confirm that the CI job omits its production
    build.

Expected: release publication is reachable only after every required gate for
the exact commit succeeds, including the production frontend build. Actual:
publication and validation are independent workflow runs, and the frontend gate
executes a weaker command set.

Verification commands

Purpose Command Expected on success
Frontend gate npm --prefix frontend run check:quality exit 0, production build completes
Repository gate npm run check exit 0
Integration npm run test:integration all integration/regression tests pass
Workflow assertions `rg -n 'workflow_call needs:.*gates.*controller.*frontend
Patch hygiene git diff --check exit 0, no whitespace errors

Scope

In scope:

  • .github/workflows/ci.yml
  • .github/workflows/release.yml

Out of scope:

  • Release-version rules and commit taxonomy; audit proposal 018 owns those.
  • Action and runtime pinning; audit proposal 017 owns those.
  • Branch-protection settings, tags, releases, or publishing a test release.
  • Product code and package versions.

Solution design

Convert release.yml into a reusable workflow invoked from ci.yml. Add a
release job in ci.yml that runs only for a push to main, depends on every
required gate, and calls the reusable workflow from the same checked-out ref.
Set the caller workflow's default token permissions to read-only, then grant
contents: write, issues: write, and pull-requests: write explicitly on the
single reusable-workflow caller job. Retain the same or narrower permissions in
the called release workflow: a reusable workflow cannot elevate permissions it
did not receive from its caller. Add workflow-level release concurrency so
retries cannot publish the same ref concurrently. Replace the frontend subset
with npm run check:quality, while retaining tests not already included in
that command.

Implementation plan

Step 1: Make release publication reusable and single-flight

In .github/workflows/release.yml, replace the independent push trigger with
workflow_call and preserve the semantic-release job. Add a release concurrency
group with cancellation disabled. Keep the called workflow's permissions no
broader than contents: write, issues: write, and pull-requests: write, and
do not assume those declarations can elevate a read-only caller token.

Verify: rg -n 'workflow_call|concurrency|semantic-release' .github/workflows/release.yml → each concept appears, and rg -n 'push:' .github/workflows/release.yml returns no matches.

Step 2: Strengthen the frontend gate

In .github/workflows/ci.yml, run npm run check:quality from frontend/ in
place of its weaker hand-selected static-check sequence. Retain frontend unit and
regression tests unless inspection proves check:quality already includes them.

Verify: rg -n 'npm run check:quality|npm run test|npm run test:regression' .github/workflows/ci.yml → the quality gate and both test classes are present exactly once.

Step 3: Chain release to the validated SHA

Add a job-level reusable-workflow call in .github/workflows/ci.yml. It must use
if: github.event_name == 'push' && github.ref == 'refs/heads/main', depend on
gates, controller, and frontend, and call
./.github/workflows/release.yml. Set workflow-default permissions to
contents: read, then grant only the release caller job contents: write,
issues: write, and pull-requests: write. Do not check out another ref,
dispatch a second workflow, or grant write access to validation jobs.

Verify: rg -n -U 'release:[\s\S]{0,700}needs:.*gates.*controller.*frontend[\s\S]{0,700}permissions:[\s\S]{0,250}contents: write[\s\S]{0,250}issues: write[\s\S]{0,250}pull-requests: write[\s\S]{0,700}uses: ./\.github/workflows/release\.yml' .github/workflows/ci.yml → one release caller with the exact write permissions matches; rg -n -U '^(permissions:| permissions:)[\s\S]{0,120}contents: read' .github/workflows/ci.yml → the validation default is read-only.

Step 4: Run the full local gates

Run the canonical repository and integration checks without weakening them.

Verify: npm run check && npm run test:integration → both commands exit 0.

Test plan

  • Inspect the workflow graph locally with the rg assertions above.
  • Confirm pull-request events cannot satisfy the release condition.
  • Confirm a failed gates, controller, or frontend job prevents the release
    job by virtue of needs and no always() override.
  • Confirm validation jobs receive no write permission and the reusable release
    caller explicitly passes every permission required by semantic-release.
  • Confirm the reusable release workflow does not independently react to pushes.
  • Confirm the production frontend build executes through check:quality.

Acceptance criteria

  • A single push workflow owns both validation and release ordering.
  • Release publication depends on all required jobs for the same workflow SHA.
  • Pull requests never invoke semantic-release.
  • The frontend CI job runs the canonical production build.
  • Validation jobs are read-only; only the release caller receives explicit
    contents/issues/pull-request write permissions.
  • The called workflow requests no permission broader than its caller grants.
  • Release concurrency cannot cancel an in-progress publication.
  • npm run check and npm run test:integration exit 0.
  • Only the two in-scope workflow files are modified.

Risks and stop conditions

Stop and report if:

  • GitHub rejects a reusable workflow with the required permissions model.
  • A required CI job is intentionally allowed to fail or is not safe to make a
    release prerequisite.
  • The live release workflow has acquired a non-main publication path.
  • Completing this requires changing branch protection, secrets, tags, or an
    existing release.
  • A verification command fails twice after a reasonable correction.

Maintenance considerations

Reviewers should scrutinize the job-level if, needs, permissions, and
concurrency semantics. audit proposal 017 should preserve this graph while pinning every
tool. Any future required gate must be added to the release job's dependencies.


Generated from Local Studio 2.0 main-branch audit proposal 001.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions