feat(pipeline): let a stage be granted permissions beyond the baseline - #58
Merged
Conversation
Every orchestrator stage job is granted a fixed baseline — contents: read
+ packages: read for CI, contents: write + packages: write for CD — and a
called workflow can only ever NARROW what its caller grants. A leaf
needing a scope the baseline withholds cannot ask for it back, so until
now the template said so outright: "a stage needing more than this is a
gt change."
This is that change. wardnet is the repository it blocks: its build leaf
uploads clippy SARIF via codeql-action/upload-sarif, which needs
security-events: write, and its end2end leaf pushes a registry build
cache, which needs packages: write. Neither would have failed loudly on
migration — the upload and the cache export would simply have stopped
happening while the pipeline stayed green. The alternative was keeping
wardnet off the CI pipeline entirely.
A stage may now be widened by name:
pipeline:
ci:
stage_permissions:
build:
security-events: write
end2end:
packages: write
The grant is merged over the baseline, so a named scope is raised
(packages: read -> write) and everything unnamed is untouched. Only the
named stage is affected; a grant never reaches its siblings.
Stage names and the scope/level vocabulary are both validated. An
unknown scope is rejected rather than rendered, because GitHub ignores
one silently: the job would read as widened, pass review, and fail later
at the single API call it was meant to permit.
The field is omitempty and the baseline is unchanged, so no existing spec
gains a key and no rendered directive moves. Verified by rendering every
file on both trees and diffing: with comments stripped the orchestrators
are byte-identical. The comments themselves do change, so the next sync
across the fleet is a comment-only diff.
Scopes are sorted, so map iteration order cannot make a re-render look
like drift.
|
gt governs itself, so changing the orchestrator template drifts gt's own committed workflow and `gt repo check` fails — which is what the governance stage reported on this branch. Comment-only, exactly as predicted for the rest of the fleet: the three stage jobs now point at pipeline.ci.stage_permissions instead of saying a wider stage is a gt change. No directive moves. gt_version stays v1.0.0. It has not moved since #31 and this is not the change that should move it, so the sync ran from a build stamped v1.0.0 rather than letting an unversioned one repoint v1 workflows at v0 — which sync refuses outright, correctly.
This was referenced Aug 25, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Why
Every orchestrator stage job gets a fixed baseline —
contents: read+packages: readfor CI,contents: write+packages: writefor CD — and a called workflow can only ever narrow what its caller grants. A leaf needing a scope the baseline withholds cannot ask for it back. The template said so outright:This is that change, and wardnet is the repository it unblocks. Two of its eight
pr.ymlleaves need more:build-daemonsecurity-events: writecodeql-action/upload-sariffor clippy SARIFtests-e2epackages: write--cache-to type=registry,...buildcacheNeither would fail loudly on migration. The SARIF upload and the cache export would just stop happening while the pipeline stayed green — which is precisely the "green forever" defect this rollout exists to remove. Without this the only honest option was
pipeline.ci.enabled: false, leaving wardnet off the CI pipeline indefinitely.What
packages: read→write), everything unnamed is untouched.pipeline.cd.stage_permissions), since that template carried the same "is a gt change today" note.Validation
Stage names and the scope/level vocabulary are both checked at sync time. The scope list is enumerated rather than passed through deliberately: GitHub ignores an unknown scope silently, so
code-scanning: writewould render a job that reads as widened, passes review, and fails later at the one API call it was meant to permit.Rejected: a stage that isn't enabled, an unknown scope, an invalid level, an empty scope map, and any grant while the pipeline is disabled.
Backwards compatibility
This is the part worth checking rather than trusting, so I checked it. I rendered every file on
origin/maingt and on this branch and diffed:omitempty, so no existing.gt-repo.yamlgains a key and the "only overrides are written" rule holds.ci-orchestration.yml(andcd-where enabled). Expected, and worth knowing before it shows up across 17 repos.Scopes render sorted, so Go's randomised map iteration can't make a re-render look like drift — there's a test asserting 20 consecutive renders are identical.
Tests
go test ./...,go vet ./...,gofmtall clean.