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
- Inspect the
push triggers and job dependencies in
.github/workflows/ci.yml and .github/workflows/release.yml.
- 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.
- 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
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.
Audit metadata
07be5be7ce69bea0c3118744ab90d148b010fce0(origin/mainon 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
maincan therefore begin semantic-release even when agate 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-6runs for pull requests and pushes tomain..github/workflows/ci.yml:60-71runs frontend static checks and tests, butdoes not invoke the canonical
check:qualitycommand that includesnext build..github/workflows/release.yml:3-39independently runs on every push tomainand invokes semantic-release with write permissions..github/workflows/ci.ymlhas no explicit read-only token default. When therelease becomes a called workflow, its token permissions can only retain or
reduce what the caller job grants; declarations in
release.ymlcannotelevate a read-only caller.
frontend/package.json:43definescheck:qualityas the complete frontendgate, including lint, both typechecks, architecture checks, and build.
AGENTS.mdrequires conventional commits and says semantic-release onmainis 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
pushtriggers and job dependencies in.github/workflows/ci.ymland.github/workflows/release.yml.mainandthat the release workflow has no dependency on the CI run for the same SHA.
check:qualityinfrontend/package.jsonand confirm that the CI job omits its productionbuild.
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
npm --prefix frontend run check:qualitynpm run checknpm run test:integrationgit diff --checkScope
In scope:
.github/workflows/ci.yml.github/workflows/release.ymlOut of scope:
Solution design
Convert
release.ymlinto a reusable workflow invoked fromci.yml. Add areleasejob inci.ymlthat runs only for a push tomain, depends on everyrequired 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, andpull-requests: writeexplicitly on thesingle 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 inthat command.
Implementation plan
Step 1: Make release publication reusable and single-flight
In
.github/workflows/release.yml, replace the independentpushtrigger withworkflow_calland preserve the semantic-release job. Add a release concurrencygroup with cancellation disabled. Keep the called workflow's permissions no
broader than
contents: write,issues: write, andpull-requests: write, anddo 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, andrg -n 'push:' .github/workflows/release.ymlreturns no matches.Step 2: Strengthen the frontend gate
In
.github/workflows/ci.yml, runnpm run check:qualityfromfrontend/inplace of its weaker hand-selected static-check sequence. Retain frontend unit and
regression tests unless inspection proves
check:qualityalready 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 useif: github.event_name == 'push' && github.ref == 'refs/heads/main', depend ongates,controller, andfrontend, and call./.github/workflows/release.yml. Set workflow-default permissions tocontents: read, then grant only the release caller jobcontents: write,issues: write, andpull-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
rgassertions above.gates,controller, orfrontendjob prevents the releasejob by virtue of
needsand noalways()override.caller explicitly passes every permission required by semantic-release.
check:quality.Acceptance criteria
contents/issues/pull-request write permissions.
npm run checkandnpm run test:integrationexit 0.Risks and stop conditions
Stop and report if:
release prerequisite.
mainpublication path.existing release.
Maintenance considerations
Reviewers should scrutinize the job-level
if,needs, permissions, andconcurrency 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.