From 1e09f063a69d5f30f55aab90bac98f4194e554de Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 16 Sep 2026 09:35:22 +0000 Subject: [PATCH] Stop ci-shared capping the pipelines' permissions 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 ktsu-dev/.github/.github/workflows/dotnet.yml@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 Claude-Session: https://claude.ai/code/session_014RABe2NufFc9hwm94iB3Rf --- .github/workflows/ci-shared.yml | 11 +++++++++-- docs/shared-ci.md | 9 +++++++++ 2 files changed, 18 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci-shared.yml b/.github/workflows/ci-shared.yml index 13315fb..223a394 100644 --- a/.github/workflows/ci-shared.yml +++ b/.github/workflows/ci-shared.yml @@ -26,14 +26,21 @@ on: default: auto type: string -permissions: - contents: read +# Deliberately no workflow-level `permissions` here. A called workflow cannot elevate +# above what its caller granted, and a `permissions` block at this level becomes the +# ceiling for the pipelines dispatched below. `contents: read` here meant dotnet.yml's +# release job -- which needs `contents: write` and `packages: write` -- could never be +# satisfied, and the run failed at startup with no jobs at all. Leaving it unset lets the +# caller's grant flow through; `detect`, which only reads repository metadata, drops to +# read-only on its own. jobs: detect: name: Classify repository runs-on: ubuntu-latest timeout-minutes: 5 + permissions: + contents: read outputs: stack: ${{ steps.classify.outputs.stack }} private: ${{ steps.classify.outputs.private }} diff --git a/docs/shared-ci.md b/docs/shared-ci.md index 42ef753..a0a725b 100644 --- a/docs/shared-ci.md +++ b/docs/shared-ci.md @@ -74,6 +74,15 @@ concurrency: group: ${{ github.workflow }}-${{ github.ref }} cancel-in-progress: true +# The caller grants; a called workflow can only reduce. This is the union of what the +# pipelines request -- dotnet.yml's release job needs contents and packages, its security +# job needs id-token -- and a pipeline that needs a permission missing here fails the run +# at startup, before any job exists to report it. +permissions: + contents: write + packages: write + id-token: write + jobs: ci: uses: ktsu-dev/.github/.github/workflows/ci-shared.yml@release