Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 7 additions & 1 deletion .github/workflows/node-build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -62,9 +62,15 @@ jobs:
name: Build and Test
runs-on: ${{ inputs.runs-on }}
timeout-minutes: ${{ inputs.timeout-minutes }}
# id-token: write is declared unconditionally, and every caller must grant it.
# A called workflow's permissions can only be equal to or MORE restrictive than
# the caller's, and a static block cannot vary with `run-codecov`. Declaring
# only contents: read would silently cap Codecov callers and stop their uploads;
# omitting the block entirely does NOT inherit the caller's grant -- the job
# drops to `Metadata: read` and even checkout fails. Verified both ways.
permissions:
contents: read
id-token: write # Codecov OIDC (unused when run-codecov is false)
id-token: write # Codecov OIDC; unused when run-codecov is false

steps:
- name: Checkout code
Expand Down
18 changes: 17 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ This trips up every reviewer once. It is not a typo.
|---|---|---|
| `gradle-build.yml` | Build + test a Java project, upload test results, Codecov | `contents: read`, `id-token: write` |
| `gradle-publish.yml` | Build + publish to GitHub Packages on `release: created` | `contents: read`, `packages: write` |
| `node-build.yml` | `npm ci` / build / test, upload test results | `contents: read` (+ `id-token: write` if `run-codecov`) |
| `node-build.yml` | `npm ci` / build / test, upload test results | `contents: read`, `id-token: write` (**both always**) |
| `test-report.yml` | Publish a JUnit check run from an uploaded artifact | `contents: read`, `actions: read`, `checks: write` |

Inputs are documented inline in each file — read the `workflow_call.inputs` block, it is
Expand Down Expand Up @@ -124,3 +124,19 @@ so the release stays manual and the ruleset stays strict.
| `.github/dependabot.yml` | **No.** Each repo keeps its own |
| `CODEOWNERS` | **No.** Each repo keeps its own |
| `LICENSE` | **No** — explicitly unsupported by GitHub |

## Caller permissions are not inherited — declare them

A called workflow's job permissions must be **equal to or more restrictive** than the
caller job's. Two consequences, both verified the hard way:

- **Grant at least what the reusable workflow declares.** Granting less is not a warning
or a downgrade — the run dies with `startup_failure` before any job begins, and the
Actions UI says only "This run likely failed because of a workflow file issue."
- **Omitting `permissions:` in the reusable workflow does not inherit the caller's.**
The job falls back to `Metadata: read`, and even `actions/checkout` fails with
`Repository not found` on a private repo.

So the tables above are minimums *and* requirements. `node-build.yml` needs
`id-token: write` from every caller even when `run-codecov` is false, because a static
permissions block cannot vary with an input.