Skip to content

Stop ci-shared capping the pipelines' permissions - #21

Merged
matt-edmondson merged 1 commit into
mainfrom
claude/dependabot-ci-workflow-rollout-fbrhdn
Sep 16, 2026
Merged

matt-edmondson merged 1 commit into
mainfrom
claude/dependabot-ci-workflow-rollout-fbrhdn

Conversation

@matt-edmondson

Copy link
Copy Markdown
Contributor

What the canary found

ktsu-dev/Sorting#46 adopted ci.yml and the run ended startup_failure with zero jobs.

A called workflow cannot elevate above what its caller granted, and ci-shared.yml's workflow-level permissions: contents: read became the ceiling for every pipeline it dispatches. dotnet.yml's release job asks for contents: write and packages: write, which under that ceiling can never be satisfied — so the run died before a job existed to report it.

The permission surface the pipelines actually request:

Workflow Job Requests
dotnet.yml release contents: write, packages: write
dotnet.yml security id-token: write, contents: write
dotnet-private.yml build contents: write, packages: read
dotnet-private.yml winget contents: write
dotnet-private.yml security id-token: write, contents: write

Union: contents: write, packages: write, id-token: write.

The fix

  • ci-shared.yml drops its workflow-level permissions so the caller's grant flows through, and detect takes contents: read on its own — it only reads repository metadata.
  • The caller template in docs/shared-ci.md now grants that union. It is the real contract between a repository and the shared pipelines, so it belongs in the caller where it is granted, rather than being discovered through a startup failure.

This widens nothing at runtime. dotnet.yml keeps its own permissions: contents: read default and its per-job overrides, behaving exactly as it did standalone. The only difference is that the ceiling above it is now high enough to permit what it already asked for.

What the canary proved before it failed

The run recorded its referenced_workflows as:

ktsu-dev/.github/.github/workflows/ci-shared.yml@release        (refs/tags/release)
ktsu-dev/.github/.github/workflows/dotnet.yml@905bdc6…          (refs/tags/release)
ktsu-dev/.github/.github/workflows/dotnet-private.yml@905bdc6…  (refs/tags/release)

The relative ./ reference inside ci-shared.yml resolves within ktsu-dev/.github, at the same commit as ci-shared.yml — not against the calling repository. That is the assumption the entire dispatcher rests on, it was the one thing the docs did not state for the nested case, and it is now settled by observation rather than inference. The @release tag also resolved correctly on its first real use.

After this merges

The release tag needs re-promoting to pick this up (Promote release, ref main), then Sorting#46 needs its ci.yml updated with the permissions block and re-run.

Testing

actionlint 1.7.7 with shellcheck clean; markdownlint-cli clean.

🤖 Generated with Claude Code

https://claude.ai/code/session_014RABe2NufFc9hwm94iB3Rf


Generated by Claude Code

The canary found this. ktsu-dev/Sorting#46 adopted ci.yml and the run failed
at startup with no jobs: a called workflow cannot elevate above what its caller
granted, and ci-shared.yml's workflow-level `permissions: contents: read`
became the ceiling for every pipeline it dispatches. dotnet.yml's release job
asks for `contents: write` and `packages: write`, which under that ceiling can
never be satisfied, so the run died before a job existed to report it.

Remove the workflow-level block so the caller's grant flows through, and give
`detect` its own `contents: read` -- it only reads repository metadata.

The caller template now grants the union the pipelines request: contents,
packages and id-token, all write. That union is the real contract between a
repository and the shared pipelines, so it is stated in the caller where it is
granted rather than left to be discovered by a startup failure.

This does not widen anything at runtime. dotnet.yml keeps its own
`permissions: contents: read` default and its per-job overrides, exactly as it
behaved as a standalone workflow; the difference is only that the ceiling above
it is now high enough to permit them.

What the canary did prove, before failing: the relative `./` reference inside
ci-shared.yml resolves within ktsu-dev/.github. The run recorded
905bdc6 -- the same commit as
ci-shared.yml, not the caller's repository -- which is the assumption the whole
dispatcher rests on.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014RABe2NufFc9hwm94iB3Rf
matt-edmondson pushed a commit to ktsu-dev/Sorting that referenced this pull request Sep 16, 2026
Two findings on this pull request, one root cause.

The CI run ended in startup_failure with no jobs: a called workflow cannot
elevate above what its caller granted, and neither this caller nor the
dispatcher granted enough for dotnet.yml's release job, which asks for
`contents: write` and `packages: write`.

CodeQL flagged the same file for having no permissions block at all, which is
the other half of it -- without one the caller silently takes the repository
default, which is both unstated and, here, insufficient.

Grant the union the shared pipelines request. The file stays byte-identical to
the template published in ktsu-dev/.github's docs/shared-ci.md, so the fan-out
remains a copy rather than a per-repository edit.

This alone does not turn the run green: the dispatcher also caps its callees at
`contents: read`, fixed in ktsu-dev/.github#21. The tag this repository
resolves, @Release, still points at the version with that cap, so CI here stays
red until #21 merges and the tag is re-promoted.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014RABe2NufFc9hwm94iB3Rf
@matt-edmondson
matt-edmondson merged commit c9ef0b3 into main Sep 16, 2026
3 checks passed
@matt-edmondson
matt-edmondson deleted the claude/dependabot-ci-workflow-rollout-fbrhdn branch September 16, 2026 09:40
Sign up for free to 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.

2 participants