@htekdev/actions-debugger 1.0.17 → 1.0.19
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/errors/concurrency-timing/cancel-runs-list-zombie-needs-jobs.yml +89 -0
- package/errors/concurrency-timing/head-ref-empty-push-unintended-cancellation.yml +76 -0
- package/errors/permissions-auth/create-github-app-token-jwt-decode-error.yml +79 -0
- package/errors/permissions-auth/create-github-app-token-workflows-permission-denied.yml +84 -0
- package/errors/permissions-auth/enterprise-oidc-issuer-slug-mismatch.yml +103 -0
- package/errors/permissions-auth/oidc-sub-claim-repo-rename-breaks-trust.yml +111 -0
- package/errors/silent-failures/attest-build-provenance-push-to-registry-404.yml +105 -0
- package/errors/triggers/workflow-run-artifact-download-missing-run-id.yml +106 -0
- package/errors/yaml-syntax/reusable-workflow-env-context-with-inputs.yml +102 -0
- package/package.json +1 -1
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
id: concurrency-timing-015
|
|
2
|
+
title: "Cancelling workflow from the Actions runs list leaves downstream needs/always() jobs zombie-queued"
|
|
3
|
+
category: concurrency-timing
|
|
4
|
+
severity: error
|
|
5
|
+
tags:
|
|
6
|
+
- cancel
|
|
7
|
+
- needs
|
|
8
|
+
- always
|
|
9
|
+
- zombie
|
|
10
|
+
- matrix
|
|
11
|
+
- queued
|
|
12
|
+
patterns:
|
|
13
|
+
- regex: "queued.*cancel|cancel.*queued"
|
|
14
|
+
flags: "i"
|
|
15
|
+
- regex: "This run has been cancelled"
|
|
16
|
+
flags: "i"
|
|
17
|
+
error_messages:
|
|
18
|
+
- "This run has been cancelled"
|
|
19
|
+
- "Job is queued"
|
|
20
|
+
root_cause: |
|
|
21
|
+
When a workflow is cancelled from the GitHub Actions **runs list page**
|
|
22
|
+
(https://github.com/{owner}/{repo}/actions), the cancellation signal does not
|
|
23
|
+
propagate to all pending/queued jobs. Specifically:
|
|
24
|
+
|
|
25
|
+
- Running jobs receive the cancellation correctly and terminate.
|
|
26
|
+
- Jobs that were not yet picked up by a runner but are waiting in a `needs:` chain
|
|
27
|
+
(especially with `if: always()`) remain stuck indefinitely in a "queued" state.
|
|
28
|
+
- These "zombie" queued jobs never start and never cancel on their own.
|
|
29
|
+
|
|
30
|
+
The problem is reproducible with:
|
|
31
|
+
1. A large matrix build (many parallel jobs)
|
|
32
|
+
2. A finalizer/aggregator job using `needs: [matrix-job]` + `if: always()`
|
|
33
|
+
3. Cancelling from the Actions overview list before all matrix jobs have started
|
|
34
|
+
|
|
35
|
+
The finalizer job enters the "queued" state but never receives the cancel signal
|
|
36
|
+
because the runs-list cancel does not do a full transitive cancel of downstream jobs.
|
|
37
|
+
|
|
38
|
+
Reported in actions/runner#4411 (May 2026) — closed as a known UI inconsistency.
|
|
39
|
+
fix: |
|
|
40
|
+
**Workaround (immediate):** To fully cancel a stuck workflow:
|
|
41
|
+
1. Click through to the specific workflow run detail page
|
|
42
|
+
(not the Actions overview list)
|
|
43
|
+
2. Click the red "Cancel workflow" button from inside the run page
|
|
44
|
+
|
|
45
|
+
This second cancel terminates all zombie queued jobs immediately.
|
|
46
|
+
|
|
47
|
+
**Structural mitigation:** Add a `timeout-minutes` to your finalizer/aggregator job
|
|
48
|
+
so it self-terminates even if the cancel signal is not received:
|
|
49
|
+
|
|
50
|
+
jobs:
|
|
51
|
+
aggregate:
|
|
52
|
+
needs: [build]
|
|
53
|
+
if: always()
|
|
54
|
+
timeout-minutes: 5
|
|
55
|
+
runs-on: ubuntu-latest
|
|
56
|
+
steps:
|
|
57
|
+
- run: echo "Aggregating results"
|
|
58
|
+
fix_code:
|
|
59
|
+
- language: yaml
|
|
60
|
+
label: "Add timeout-minutes to always() jobs as a safety net"
|
|
61
|
+
code: |
|
|
62
|
+
jobs:
|
|
63
|
+
build:
|
|
64
|
+
strategy:
|
|
65
|
+
matrix:
|
|
66
|
+
target: [a, b, c, d, e, f, g, h]
|
|
67
|
+
runs-on: ubuntu-latest
|
|
68
|
+
steps:
|
|
69
|
+
- run: make ${{ matrix.target }}
|
|
70
|
+
|
|
71
|
+
aggregate:
|
|
72
|
+
needs: [build]
|
|
73
|
+
if: always()
|
|
74
|
+
timeout-minutes: 5 # prevents zombie queueing if cancel signal is lost
|
|
75
|
+
runs-on: ubuntu-latest
|
|
76
|
+
steps:
|
|
77
|
+
- run: echo "Build result ${{ needs.build.result }}"
|
|
78
|
+
prevention:
|
|
79
|
+
- "Always add `timeout-minutes` to finalizer jobs that use `if: always()` to limit zombie lifetime"
|
|
80
|
+
- "When cancelling a stuck workflow, navigate to the specific run page and use the in-run Cancel button"
|
|
81
|
+
- "Avoid relying on the Actions overview Cancel — it has incomplete propagation to queued downstream jobs"
|
|
82
|
+
- "Consider using a concurrency group to auto-cancel the entire workflow on re-trigger instead of manual cancellation"
|
|
83
|
+
docs:
|
|
84
|
+
- url: "https://github.com/actions/runner/issues/4411"
|
|
85
|
+
label: "actions/runner#4411 — Cancellation from runs list leaves needs+always() jobs zombie-queued"
|
|
86
|
+
- url: "https://docs.github.com/en/actions/writing-workflows/choosing-what-your-workflow-does/using-jobs-in-a-workflow#defining-prerequisite-jobs"
|
|
87
|
+
label: "GitHub Docs — Defining prerequisite jobs (needs:)"
|
|
88
|
+
- url: "https://docs.github.com/en/actions/writing-workflows/choosing-when-your-workflow-runs/expressions#always"
|
|
89
|
+
label: "GitHub Docs — always() status check function"
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
id: concurrency-timing-014
|
|
2
|
+
title: "github.head_ref empty on push events collapses concurrency group and cancels unrelated runs"
|
|
3
|
+
category: concurrency-timing
|
|
4
|
+
severity: silent-failure
|
|
5
|
+
tags:
|
|
6
|
+
- concurrency
|
|
7
|
+
- head_ref
|
|
8
|
+
- push-event
|
|
9
|
+
- cancel-in-progress
|
|
10
|
+
- cross-branch
|
|
11
|
+
patterns:
|
|
12
|
+
- regex: "Run .*? was cancelled"
|
|
13
|
+
flags: "i"
|
|
14
|
+
- regex: "cancel-in-progress.*true"
|
|
15
|
+
flags: "i"
|
|
16
|
+
error_messages:
|
|
17
|
+
- "Run <workflow-name> was cancelled"
|
|
18
|
+
- "This run has been cancelled by a newer run"
|
|
19
|
+
root_cause: |
|
|
20
|
+
On `push` events, `github.head_ref` is always an empty string — it is only populated
|
|
21
|
+
for `pull_request` and `pull_request_target` events where a source branch exists.
|
|
22
|
+
|
|
23
|
+
When a concurrency group expression uses `github.head_ref` without a fallback value,
|
|
24
|
+
every push to any branch evaluates to the same empty-string group key. With
|
|
25
|
+
`cancel-in-progress: true`, each new push cancels any other in-progress run across
|
|
26
|
+
ALL branches — including unrelated feature branches and main.
|
|
27
|
+
|
|
28
|
+
Example of the broken pattern:
|
|
29
|
+
concurrency:
|
|
30
|
+
group: ${{ github.workflow }}-${{ github.head_ref }}
|
|
31
|
+
cancel-in-progress: true
|
|
32
|
+
|
|
33
|
+
Pushing to `main` and `feature-branch` simultaneously means the second push cancels
|
|
34
|
+
the first, even though they are completely unrelated.
|
|
35
|
+
|
|
36
|
+
This is confirmed by GitHub Actions documentation and illustrated by Gitea's
|
|
37
|
+
implementation fix for the same semantic (go-gitea/gitea#37311, April 2026).
|
|
38
|
+
fix: |
|
|
39
|
+
Use the `||` fallback operator so that push events fall back to `github.run_id`,
|
|
40
|
+
which is unique per workflow run and thus prevents any cross-run cancellation on push:
|
|
41
|
+
|
|
42
|
+
concurrency:
|
|
43
|
+
group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }}
|
|
44
|
+
cancel-in-progress: true
|
|
45
|
+
|
|
46
|
+
With this pattern:
|
|
47
|
+
- On pull_request events: `github.head_ref` is the source branch name — runs for the
|
|
48
|
+
same branch/PR cancel each other (desired behavior).
|
|
49
|
+
- On push events: `github.head_ref` is empty, so `github.run_id` is used — each
|
|
50
|
+
push run gets a unique group and is never cancelled by an unrelated run.
|
|
51
|
+
fix_code:
|
|
52
|
+
- language: yaml
|
|
53
|
+
label: "Correct: fallback to github.run_id on push events"
|
|
54
|
+
code: |
|
|
55
|
+
concurrency:
|
|
56
|
+
group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }}
|
|
57
|
+
cancel-in-progress: true
|
|
58
|
+
- language: yaml
|
|
59
|
+
label: "Wrong: head_ref alone is empty on push — collapses all pushes to one group"
|
|
60
|
+
code: |
|
|
61
|
+
# DO NOT USE — cancels unrelated push runs
|
|
62
|
+
concurrency:
|
|
63
|
+
group: ${{ github.workflow }}-${{ github.head_ref }}
|
|
64
|
+
cancel-in-progress: true
|
|
65
|
+
prevention:
|
|
66
|
+
- "Always use `${{ github.head_ref || github.run_id }}` in concurrency groups, never `github.head_ref` alone"
|
|
67
|
+
- "Test concurrency behavior with simultaneous pushes to two unrelated branches to verify isolation"
|
|
68
|
+
- "If you only want per-PR cancellation and not per-push cancellation, restrict the concurrency block to PR events using an `if:` condition"
|
|
69
|
+
- "Add a CI job that verifies no two concurrent push runs share a group key"
|
|
70
|
+
docs:
|
|
71
|
+
- url: "https://docs.github.com/en/actions/writing-workflows/choosing-what-your-workflow-does/control-the-concurrency-of-workflows-and-jobs"
|
|
72
|
+
label: "GitHub Docs — Controlling workflow concurrency"
|
|
73
|
+
- url: "https://docs.github.com/en/actions/writing-workflows/choosing-when-your-workflow-runs/events-that-trigger-workflows#push"
|
|
74
|
+
label: "GitHub Docs — push event payload (head_ref is absent)"
|
|
75
|
+
- url: "https://github.com/go-gitea/gitea/pull/37311"
|
|
76
|
+
label: "go-gitea/gitea#37311 — Fix actions concurrency groups cross-branch leak"
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
id: permissions-auth-021
|
|
2
|
+
title: "create-github-app-token: A JSON web token could not be decoded (PEM key format)"
|
|
3
|
+
category: permissions-auth
|
|
4
|
+
severity: error
|
|
5
|
+
tags:
|
|
6
|
+
- github-app
|
|
7
|
+
- jwt
|
|
8
|
+
- private-key
|
|
9
|
+
- pem
|
|
10
|
+
- create-github-app-token
|
|
11
|
+
patterns:
|
|
12
|
+
- regex: "A JSON web token could not be decoded"
|
|
13
|
+
flags: "i"
|
|
14
|
+
- regex: "Failed to create token for .+\\(attempt \\d+\\): A JSON web token could not be decoded"
|
|
15
|
+
flags: "i"
|
|
16
|
+
error_messages:
|
|
17
|
+
- "A JSON web token could not be decoded - https://docs.github.com/rest"
|
|
18
|
+
- "Failed to create token for \"repo-name\" (attempt 1): A JSON web token could not be decoded"
|
|
19
|
+
- "RequestError [HttpError]: A JSON web token could not be decoded"
|
|
20
|
+
root_cause: |
|
|
21
|
+
The `actions/create-github-app-token` action signs a JWT using the GitHub App private key.
|
|
22
|
+
When the private key stored in the repository secret is malformed, every attempt to generate
|
|
23
|
+
a token fails with a 401 and this message. The action retries 4 times then fails the step.
|
|
24
|
+
|
|
25
|
+
Common causes:
|
|
26
|
+
1. Trailing whitespace or the final newline stripped when pasting the PEM into the GitHub
|
|
27
|
+
Secrets UI (the textarea strips trailing whitespace on save)
|
|
28
|
+
2. Windows-style CRLF line endings introduced during copy-paste from a text editor
|
|
29
|
+
3. Missing PEM header (`-----BEGIN RSA PRIVATE KEY-----`) or footer line
|
|
30
|
+
4. The Base64 body is correct but line breaks inside the key were removed
|
|
31
|
+
5. The full `.pem` file was Base64-encoded before being stored (double-encoded)
|
|
32
|
+
|
|
33
|
+
The GitHub API returns HTTP 401 with the message "A JSON web token could not be decoded"
|
|
34
|
+
whenever the JWT signature cannot be verified, which always indicates a malformed key.
|
|
35
|
+
fix: |
|
|
36
|
+
Store the private key exactly as downloaded from GitHub — raw PEM format with LF line
|
|
37
|
+
endings, including the header and footer. Use the GitHub CLI to set the secret from the
|
|
38
|
+
downloaded file rather than copy-pasting it through the UI.
|
|
39
|
+
|
|
40
|
+
1. Download the private key from the GitHub App settings page
|
|
41
|
+
(Settings → Developer settings → GitHub Apps → Your App → Private keys)
|
|
42
|
+
2. Set the secret from the file:
|
|
43
|
+
gh secret set APP_PRIVATE_KEY < my-app.private-key.pem
|
|
44
|
+
3. Verify the secret was stored correctly by checking the Actions secrets list shows
|
|
45
|
+
the key was updated recently
|
|
46
|
+
|
|
47
|
+
If the key is already in a secret and you cannot change it, create a new private key
|
|
48
|
+
from the App settings and re-set the secret from the fresh download.
|
|
49
|
+
fix_code:
|
|
50
|
+
- language: shell
|
|
51
|
+
label: "Set secret directly from downloaded .pem file (preserves newlines)"
|
|
52
|
+
code: |
|
|
53
|
+
# Download the .pem from GitHub App settings, then:
|
|
54
|
+
gh secret set APP_PRIVATE_KEY < my-app.2024-01-15.private-key.pem
|
|
55
|
+
- language: yaml
|
|
56
|
+
label: "Correct workflow: reference private key secret in action"
|
|
57
|
+
code: |
|
|
58
|
+
- uses: actions/create-github-app-token@v1
|
|
59
|
+
id: app-token
|
|
60
|
+
with:
|
|
61
|
+
app-id: ${{ vars.APP_ID }}
|
|
62
|
+
private-key: ${{ secrets.APP_PRIVATE_KEY }}
|
|
63
|
+
|
|
64
|
+
- name: Use generated token
|
|
65
|
+
run: echo "Token generated successfully"
|
|
66
|
+
env:
|
|
67
|
+
GITHUB_TOKEN: ${{ steps.app-token.outputs.token }}
|
|
68
|
+
prevention:
|
|
69
|
+
- "Always set the APP_PRIVATE_KEY secret using `gh secret set KEY < file.pem`, never by copy-pasting through the browser UI"
|
|
70
|
+
- "Regenerate and re-set the private key if you suspect formatting was corrupted during initial setup"
|
|
71
|
+
- "Verify the secret value in the GitHub UI shows the correct 'Updated' timestamp after setting via CLI"
|
|
72
|
+
- "Store private keys in a secrets manager (e.g., HashiCorp Vault, AWS Secrets Manager) and fetch at runtime to avoid manual copy-paste errors"
|
|
73
|
+
docs:
|
|
74
|
+
- url: "https://github.com/actions/create-github-app-token"
|
|
75
|
+
label: "actions/create-github-app-token README"
|
|
76
|
+
- url: "https://docs.github.com/en/apps/creating-github-apps/authenticating-with-a-github-app/generating-a-private-key-for-a-github-app"
|
|
77
|
+
label: "Generating a private key for a GitHub App"
|
|
78
|
+
- url: "https://github.com/actions/create-github-app-token/issues/153"
|
|
79
|
+
label: "actions/create-github-app-token#153: JWT decode error (27 comments)"
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
id: permissions-auth-022
|
|
2
|
+
title: "create-github-app-token: push rejected — GitHub App missing `workflows` permission"
|
|
3
|
+
category: permissions-auth
|
|
4
|
+
severity: error
|
|
5
|
+
tags:
|
|
6
|
+
- github-app
|
|
7
|
+
- create-github-app-token
|
|
8
|
+
- workflows-permission
|
|
9
|
+
- push
|
|
10
|
+
- git-push
|
|
11
|
+
patterns:
|
|
12
|
+
- regex: "refusing to allow a GitHub App to create or update workflow .+ without `workflows` permission"
|
|
13
|
+
flags: "i"
|
|
14
|
+
- regex: "remote rejected.*refusing to allow a GitHub App.*workflows.*permission"
|
|
15
|
+
flags: "i"
|
|
16
|
+
error_messages:
|
|
17
|
+
- "! [remote rejected] -> (refusing to allow a GitHub App to create or update workflow `.github/workflows/` without `workflows` permission)"
|
|
18
|
+
- "refusing to allow a GitHub App to create or update workflow `.github/workflows/my-workflow.yml` without `workflows` permission"
|
|
19
|
+
- "error: failed to push some refs to ''"
|
|
20
|
+
root_cause: |
|
|
21
|
+
When a workflow uses `actions/create-github-app-token` to generate a token and then uses
|
|
22
|
+
that token to push commits that include changes to `.github/workflows/` files, GitHub
|
|
23
|
+
enforces that the GitHub App's installation token has the `workflows` write permission.
|
|
24
|
+
|
|
25
|
+
This permission gate was tightened in `actions/create-github-app-token` v2.1.4 due to
|
|
26
|
+
changes in `octokit/auth-app.js` (PR #712). Tokens generated without explicitly requesting
|
|
27
|
+
the `workflows` write permission no longer inherit it automatically, even if the GitHub App
|
|
28
|
+
itself has the permission enabled in its settings.
|
|
29
|
+
|
|
30
|
+
Two things must be true for the push to succeed:
|
|
31
|
+
1. The GitHub App has the `Workflows` repository permission set to Read & Write in the
|
|
32
|
+
App's settings page
|
|
33
|
+
2. The `permission-workflows: write` input is passed to `actions/create-github-app-token`
|
|
34
|
+
so the generated token explicitly includes that permission
|
|
35
|
+
fix: |
|
|
36
|
+
Add `permission-workflows: write` to the `actions/create-github-app-token` step. Also
|
|
37
|
+
verify that the GitHub App itself has the Workflows permission enabled in its settings.
|
|
38
|
+
|
|
39
|
+
Steps:
|
|
40
|
+
1. Go to https://github.com/settings/apps/YOUR_APP/permissions
|
|
41
|
+
2. Under "Repository permissions", set "Workflows" to "Read and write"
|
|
42
|
+
3. Save changes and accept the permission update for affected organizations/users
|
|
43
|
+
4. Update your workflow to pass `permission-workflows: write`
|
|
44
|
+
fix_code:
|
|
45
|
+
- language: yaml
|
|
46
|
+
label: "Add workflows write permission to token generation step"
|
|
47
|
+
code: |
|
|
48
|
+
- uses: actions/create-github-app-token@v1
|
|
49
|
+
id: app-token
|
|
50
|
+
with:
|
|
51
|
+
app-id: ${{ vars.APP_ID }}
|
|
52
|
+
private-key: ${{ secrets.APP_PRIVATE_KEY }}
|
|
53
|
+
permission-workflows: write # ✅ Required when pushing workflow file changes
|
|
54
|
+
|
|
55
|
+
- name: Configure git with app token
|
|
56
|
+
run: |
|
|
57
|
+
git config user.name "github-actions[bot]"
|
|
58
|
+
git config user.email "github-actions[bot]@users.noreply.github.com"
|
|
59
|
+
git remote set-url origin https://x-access-token:${{ steps.app-token.outputs.token }}@github.com/${{ github.repository }}
|
|
60
|
+
|
|
61
|
+
- name: Push changes including workflow files
|
|
62
|
+
run: git push
|
|
63
|
+
- language: yaml
|
|
64
|
+
label: "Fallback: pin to v2.1.3 if immediate permission change is not possible"
|
|
65
|
+
code: |
|
|
66
|
+
# Pinning to a pre-v2.1.4 version is a temporary workaround only.
|
|
67
|
+
# Migrate to permission-workflows: write as soon as possible.
|
|
68
|
+
- uses: actions/create-github-app-token@v2.1.3
|
|
69
|
+
id: app-token
|
|
70
|
+
with:
|
|
71
|
+
app-id: ${{ vars.APP_ID }}
|
|
72
|
+
private-key: ${{ secrets.APP_PRIVATE_KEY }}
|
|
73
|
+
prevention:
|
|
74
|
+
- "Always specify `permission-workflows: write` when the token will be used to push `.github/workflows/` changes"
|
|
75
|
+
- "Verify the GitHub App's Workflows repository permission is set to Read & Write in the App settings"
|
|
76
|
+
- "Avoid pinning to old versions as a workaround — explicitly grant the required permission instead"
|
|
77
|
+
- "Use separate token generation steps with minimal permissions for each distinct operation"
|
|
78
|
+
docs:
|
|
79
|
+
- url: "https://github.com/actions/create-github-app-token"
|
|
80
|
+
label: "actions/create-github-app-token README"
|
|
81
|
+
- url: "https://docs.github.com/en/rest/authentication/permissions-required-for-github-apps?apiVersion=2022-11-28#repository-permissions-for-workflows"
|
|
82
|
+
label: "Permissions required for GitHub Apps — Workflows"
|
|
83
|
+
- url: "https://github.com/actions/create-github-app-token/issues/301"
|
|
84
|
+
label: "actions/create-github-app-token#301: workflows permission push rejection"
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
id: permissions-auth-020
|
|
2
|
+
title: "GHEC enterprise OIDC issuer slug causes token verification mismatch with cloud tools"
|
|
3
|
+
category: permissions-auth
|
|
4
|
+
severity: error
|
|
5
|
+
tags:
|
|
6
|
+
- oidc
|
|
7
|
+
- enterprise
|
|
8
|
+
- ghec
|
|
9
|
+
- issuer
|
|
10
|
+
- attestation
|
|
11
|
+
- cloud-auth
|
|
12
|
+
patterns:
|
|
13
|
+
- regex: "expected issuer.*got.*githubusercontent\\.com/[a-z0-9-]+"
|
|
14
|
+
flags: "i"
|
|
15
|
+
- regex: "issuer mismatch.*token\\.actions\\.githubusercontent\\.com"
|
|
16
|
+
flags: "i"
|
|
17
|
+
- regex: "no matching CertificateIdentity.*issuer"
|
|
18
|
+
flags: "i"
|
|
19
|
+
error_messages:
|
|
20
|
+
- "Error: verifying with issuer \"sigstore.dev\": failed to verify certificate identity: no matching CertificateIdentity found, last error: expected issuer value \"https://token.actions.githubusercontent.com\", got \"https://token.actions.githubusercontent.com/my-enterprise\""
|
|
21
|
+
- "Error: expected issuer to be https://token.actions.githubusercontent.com, got https://token.actions.githubusercontent.com/my-enterprise"
|
|
22
|
+
- "UnauthorizedException: Issuer does not match configured value"
|
|
23
|
+
root_cause: |
|
|
24
|
+
GitHub Enterprise Cloud (GHEC) organizations can customize the OIDC token issuer
|
|
25
|
+
claim to include their unique enterprise slug, for example:
|
|
26
|
+
|
|
27
|
+
Default issuer: https://token.actions.githubusercontent.com
|
|
28
|
+
Enterprise issuer: https://token.actions.githubusercontent.com/my-enterprise
|
|
29
|
+
|
|
30
|
+
This customization is a security feature (prevents tokens from one enterprise from
|
|
31
|
+
being used against another's cloud resources), but it creates a mismatch for:
|
|
32
|
+
|
|
33
|
+
1. **`gh attestation verify`**: The CLI tool defaults to verifying against the
|
|
34
|
+
standard issuer. If the enterprise issuer is active, verification fails with
|
|
35
|
+
"expected issuer … got … my-enterprise" unless `--cert-oidc-issuer` is passed.
|
|
36
|
+
|
|
37
|
+
2. **Cloud provider trust policies**: AWS, Azure, GCP OIDC federation configured
|
|
38
|
+
against the default issuer URL rejects enterprise-slug tokens with auth errors.
|
|
39
|
+
|
|
40
|
+
3. **Third-party actions**: Actions that validate the OIDC token internally may
|
|
41
|
+
hardcode the default issuer and fail silently.
|
|
42
|
+
|
|
43
|
+
This affects any GHEC organization that has enabled the enterprise issuer
|
|
44
|
+
customization (via Enterprise Settings → OIDC provider → Customize issuer).
|
|
45
|
+
fix: |
|
|
46
|
+
**For `gh attestation verify`:** Pass the enterprise-specific issuer URL:
|
|
47
|
+
|
|
48
|
+
gh attestation verify artifact.zip \
|
|
49
|
+
--owner my-enterprise \
|
|
50
|
+
--cert-oidc-issuer https://token.actions.githubusercontent.com/my-enterprise
|
|
51
|
+
|
|
52
|
+
**For AWS IAM:** Update the OIDC provider URL in IAM to use the enterprise issuer:
|
|
53
|
+
|
|
54
|
+
Provider URL: https://token.actions.githubusercontent.com/my-enterprise
|
|
55
|
+
Audience: sts.amazonaws.com
|
|
56
|
+
|
|
57
|
+
**For Azure Workload Identity:** Set the issuer in the federated credential to:
|
|
58
|
+
|
|
59
|
+
https://token.actions.githubusercontent.com/my-enterprise
|
|
60
|
+
|
|
61
|
+
**For GCP Workload Identity Pool:** Set the issuer URI in the OIDC provider config
|
|
62
|
+
to the enterprise issuer URL.
|
|
63
|
+
|
|
64
|
+
**To check your enterprise issuer:**
|
|
65
|
+
Visit https://token.actions.githubusercontent.com/my-enterprise/.well-known/openid-configuration
|
|
66
|
+
— if it returns a valid document, enterprise issuer customization is active.
|
|
67
|
+
fix_code:
|
|
68
|
+
- language: yaml
|
|
69
|
+
label: "gh attestation verify with enterprise issuer"
|
|
70
|
+
code: |
|
|
71
|
+
- name: Attest artifact
|
|
72
|
+
run: |
|
|
73
|
+
gh attestation verify dist/my-app \
|
|
74
|
+
--owner my-enterprise \
|
|
75
|
+
--cert-oidc-issuer https://token.actions.githubusercontent.com/my-enterprise
|
|
76
|
+
env:
|
|
77
|
+
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
78
|
+
- language: yaml
|
|
79
|
+
label: "AWS credentials action with enterprise OIDC issuer"
|
|
80
|
+
code: |
|
|
81
|
+
# In AWS IAM, create the OIDC identity provider with the enterprise issuer URL
|
|
82
|
+
# instead of the default https://token.actions.githubusercontent.com
|
|
83
|
+
# Then configure the federated role trust policy:
|
|
84
|
+
{
|
|
85
|
+
"Condition": {
|
|
86
|
+
"StringEquals": {
|
|
87
|
+
"token.actions.githubusercontent.com/my-enterprise:aud": "sts.amazonaws.com",
|
|
88
|
+
"token.actions.githubusercontent.com/my-enterprise:sub": "repo:my-enterprise/my-repo:ref:refs/heads/main"
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
prevention:
|
|
93
|
+
- "Document whether your GHEC enterprise has the custom issuer enabled — include it in your OIDC onboarding checklist"
|
|
94
|
+
- "Store the full enterprise issuer URL in a reusable variable or org-level secret to avoid hardcoding it in every workflow"
|
|
95
|
+
- "When setting up new cloud OIDC federation, verify the issuer from the well-known endpoint rather than copying from docs"
|
|
96
|
+
- "Pin `gh attestation verify` calls to always include `--cert-oidc-issuer` in enterprise repos"
|
|
97
|
+
docs:
|
|
98
|
+
- url: "https://docs.github.com/en/enterprise-cloud@latest/actions/security-for-github-actions/security-hardening-your-deployments/about-security-hardening-with-openid-connect#customizing-the-issuer-value-for-an-enterprise"
|
|
99
|
+
label: "GitHub Docs — Customizing the OIDC issuer value for an enterprise"
|
|
100
|
+
- url: "https://github.com/cli/cli/pull/9616"
|
|
101
|
+
label: "cli/cli#9616 — Better messaging for attestation verify custom issuer mismatch error"
|
|
102
|
+
- url: "https://token.actions.githubusercontent.com/.well-known/openid-configuration"
|
|
103
|
+
label: "GitHub OIDC well-known endpoint (check your enterprise variant at /{slug}/.well-known/openid-configuration)"
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
id: permissions-auth-019
|
|
2
|
+
title: "OIDC sub claim breaks cloud trust policies after repository rename or transfer"
|
|
3
|
+
category: permissions-auth
|
|
4
|
+
severity: error
|
|
5
|
+
tags:
|
|
6
|
+
- oidc
|
|
7
|
+
- sub-claim
|
|
8
|
+
- repo-rename
|
|
9
|
+
- repo-transfer
|
|
10
|
+
- cloud-auth
|
|
11
|
+
- aws
|
|
12
|
+
- azure
|
|
13
|
+
- gcp
|
|
14
|
+
patterns:
|
|
15
|
+
- regex: "no matching CertificateIdentity"
|
|
16
|
+
flags: "i"
|
|
17
|
+
- regex: "sub.*does not match|subject.*mismatch|token validation failed"
|
|
18
|
+
flags: "i"
|
|
19
|
+
- regex: "Error: Could not assume role.*subject claim"
|
|
20
|
+
flags: "i"
|
|
21
|
+
error_messages:
|
|
22
|
+
- "Error: No matching identity found in OIDC token"
|
|
23
|
+
- "Error: Could not assume role with web identity: NotAuthorized"
|
|
24
|
+
- "no matching CertificateIdentity found, last error: certificate identity not found"
|
|
25
|
+
- "subject claim mismatch: expected repo:old-owner/old-repo, got repo:new-owner/new-repo"
|
|
26
|
+
root_cause: |
|
|
27
|
+
GitHub Actions OIDC tokens include a `sub` (subject) claim that encodes the repository
|
|
28
|
+
owner and name using their **mutable string names**, for example:
|
|
29
|
+
|
|
30
|
+
repo:octocat/my-app:ref:refs/heads/main
|
|
31
|
+
|
|
32
|
+
Cloud providers (AWS, Azure, GCP) and tools like `gh attestation verify` configure
|
|
33
|
+
trust policies against this `sub` claim value. When a repository is **renamed** or
|
|
34
|
+
**transferred** to a different organization, the `sub` claim changes immediately:
|
|
35
|
+
|
|
36
|
+
Before transfer: repo:old-org/my-app:ref:refs/heads/main
|
|
37
|
+
After transfer: repo:new-org/my-app:ref:refs/heads/main
|
|
38
|
+
|
|
39
|
+
All cloud trust policies that reference the old `sub` format instantly break.
|
|
40
|
+
OIDC authentication fails with "subject mismatch" or "no matching identity" errors
|
|
41
|
+
even though the workflow code and secrets are identical.
|
|
42
|
+
|
|
43
|
+
GitHub announced immutable sub claims (appending numeric owner/repo IDs) on
|
|
44
|
+
2026-04-23, but existing repositories must opt in explicitly. New repositories
|
|
45
|
+
created or transferred after June 18, 2026 automatically use the new format.
|
|
46
|
+
|
|
47
|
+
Old (mutable) format: repo:owner/repo:ref:refs/heads/main
|
|
48
|
+
New (immutable) format: repo:owner-123456/repo-789012:ref:refs/heads/main
|
|
49
|
+
fix: |
|
|
50
|
+
**Before renaming or transferring a repo:**
|
|
51
|
+
|
|
52
|
+
1. Enable immutable subject claims in repository or organization OIDC settings
|
|
53
|
+
(Settings → Actions → General → OIDC subject claims → Enable immutable format).
|
|
54
|
+
2. Use the preview endpoint to see the new sub claim format before it takes effect.
|
|
55
|
+
3. Update all cloud provider trust policies and IAM role conditions to accept the
|
|
56
|
+
new immutable format (includes numeric IDs alongside names).
|
|
57
|
+
|
|
58
|
+
**After an unplanned rename/transfer:**
|
|
59
|
+
|
|
60
|
+
1. Identify the new `sub` claim value by decoding the OIDC token from a failed run
|
|
61
|
+
or using the GitHub OIDC preview API.
|
|
62
|
+
2. Update the trust policy in each cloud provider to reference the new repo path.
|
|
63
|
+
3. Optionally opt into immutable claims to future-proof against further renames.
|
|
64
|
+
|
|
65
|
+
**For AWS IAM (StringLike condition):**
|
|
66
|
+
Condition:
|
|
67
|
+
StringLike:
|
|
68
|
+
token.actions.githubusercontent.com:sub:
|
|
69
|
+
- "repo:new-org/new-repo:*"
|
|
70
|
+
fix_code:
|
|
71
|
+
- language: yaml
|
|
72
|
+
label: "AWS IAM trust policy update after repo rename"
|
|
73
|
+
code: |
|
|
74
|
+
# Update the StringLike condition in your IAM role trust policy
|
|
75
|
+
# Replace old path with new path after rename/transfer
|
|
76
|
+
{
|
|
77
|
+
"Condition": {
|
|
78
|
+
"StringLike": {
|
|
79
|
+
"token.actions.githubusercontent.com:sub": "repo:new-org/new-repo:*"
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
- language: yaml
|
|
84
|
+
label: "Workflow: use immutable OIDC subject for new repos (June 18 2026+)"
|
|
85
|
+
code: |
|
|
86
|
+
# No workflow change needed — immutable format is set in repo/org settings.
|
|
87
|
+
# Once enabled, the sub claim becomes:
|
|
88
|
+
# repo:owner-123456/repo-789012:ref:refs/heads/main
|
|
89
|
+
# Update cloud trust policies to use the new format BEFORE enabling immutable claims.
|
|
90
|
+
jobs:
|
|
91
|
+
deploy:
|
|
92
|
+
permissions:
|
|
93
|
+
id-token: write
|
|
94
|
+
contents: read
|
|
95
|
+
steps:
|
|
96
|
+
- uses: aws-actions/configure-aws-credentials@v4
|
|
97
|
+
with:
|
|
98
|
+
role-to-assume: arn:aws:iam::123456789:role/my-role
|
|
99
|
+
aws-region: us-east-1
|
|
100
|
+
prevention:
|
|
101
|
+
- "Enable immutable OIDC subject claims before any rename or transfer to prevent trust policy breakage"
|
|
102
|
+
- "Store the expected sub claim value in cloud trust policies as a wildcard (`repo:owner/repo:*`) rather than exact paths to tolerate ref changes"
|
|
103
|
+
- "Add OIDC trust policy updates to your repo rename/transfer runbook"
|
|
104
|
+
- "New repos created after June 18, 2026 automatically use immutable sub claims — update trust policies accordingly"
|
|
105
|
+
docs:
|
|
106
|
+
- url: "https://github.blog/changelog/2026-04-23-immutable-subject-claims-for-github-actions-oidc-tokens/"
|
|
107
|
+
label: "GitHub Changelog 2026-04-23 — Immutable subject claims for GitHub Actions OIDC tokens"
|
|
108
|
+
- url: "https://docs.github.com/en/actions/security-for-github-actions/security-hardening-your-deployments/about-security-hardening-with-openid-connect"
|
|
109
|
+
label: "GitHub Docs — About security hardening with OpenID Connect"
|
|
110
|
+
- url: "https://docs.github.com/en/enterprise-cloud@latest/actions/reference/security/oidc"
|
|
111
|
+
label: "GitHub Docs — OIDC token claims reference"
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
id: silent-failures-025
|
|
2
|
+
title: "attest-build-provenance: OCIError 404 when image not pushed to registry before attestation"
|
|
3
|
+
category: silent-failures
|
|
4
|
+
severity: error
|
|
5
|
+
tags:
|
|
6
|
+
- attest-build-provenance
|
|
7
|
+
- oci
|
|
8
|
+
- container-registry
|
|
9
|
+
- slsa
|
|
10
|
+
- attestation
|
|
11
|
+
- push-to-registry
|
|
12
|
+
patterns:
|
|
13
|
+
- regex: "OCIError: Error uploading artifact to container registry"
|
|
14
|
+
flags: "i"
|
|
15
|
+
- regex: "Error fetching .+/manifests/sha256:[a-f0-9]+ - expected 200, received 404"
|
|
16
|
+
flags: "i"
|
|
17
|
+
- regex: "expected 200, received 404"
|
|
18
|
+
flags: "i"
|
|
19
|
+
error_messages:
|
|
20
|
+
- "Error: OCIError: Error uploading artifact to container registry"
|
|
21
|
+
- "Error: Error fetching https://ghcr.io/v2/owner/repo/manifests/sha256:abc123... - expected 200, received 404"
|
|
22
|
+
root_cause: |
|
|
23
|
+
`actions/attest-build-provenance` with `push-to-registry: true` fetches the image manifest
|
|
24
|
+
from the container registry to embed it in the attestation bundle. If the image was built
|
|
25
|
+
with `load: true` in `docker/build-push-action` but `push: false` (or a conditional push
|
|
26
|
+
that evaluated to false), the image exists only in the runner's local Docker daemon — not
|
|
27
|
+
in the remote registry. The attestation step then fails with a 404 when fetching the
|
|
28
|
+
manifest from the registry URL.
|
|
29
|
+
|
|
30
|
+
This is a silent misconfiguration: the build step "succeeds" (because `load: true` works),
|
|
31
|
+
the attestation step then fails with a cryptic OCI/manifest error instead of a clear message
|
|
32
|
+
explaining that the image was never pushed.
|
|
33
|
+
|
|
34
|
+
Typical trigger: workflows that conditionally push (e.g., only on `main` branch) but run
|
|
35
|
+
the attestation step unconditionally on every push/PR.
|
|
36
|
+
fix: |
|
|
37
|
+
Guard the attestation step with the same `if:` condition used for the image push.
|
|
38
|
+
The attestation step should only run when the image was actually pushed to the registry.
|
|
39
|
+
|
|
40
|
+
If you need to generate attestations on PRs (e.g., for preview images), ensure the image
|
|
41
|
+
is actually pushed to a staging registry before the attestation step runs.
|
|
42
|
+
fix_code:
|
|
43
|
+
- language: yaml
|
|
44
|
+
label: "Wrong: attestation runs unconditionally even when image not pushed"
|
|
45
|
+
code: |
|
|
46
|
+
- name: Build and push
|
|
47
|
+
id: build-push
|
|
48
|
+
uses: docker/build-push-action@v6
|
|
49
|
+
with:
|
|
50
|
+
push: ${{ github.ref == 'refs/heads/main' }} # Only pushes on main
|
|
51
|
+
load: true
|
|
52
|
+
tags: ghcr.io/org/app:latest
|
|
53
|
+
|
|
54
|
+
- name: Generate attestation
|
|
55
|
+
uses: actions/attest-build-provenance@v2
|
|
56
|
+
with:
|
|
57
|
+
subject-name: ghcr.io/org/app
|
|
58
|
+
subject-digest: ${{ steps.build-push.outputs.digest }}
|
|
59
|
+
push-to-registry: true # ❌ Fails on PRs — image is local-only
|
|
60
|
+
- language: yaml
|
|
61
|
+
label: "Fix: match attestation if condition to the push condition"
|
|
62
|
+
code: |
|
|
63
|
+
- name: Build and push
|
|
64
|
+
id: build-push
|
|
65
|
+
uses: docker/build-push-action@v6
|
|
66
|
+
with:
|
|
67
|
+
push: ${{ github.ref == 'refs/heads/main' }}
|
|
68
|
+
load: true
|
|
69
|
+
tags: ghcr.io/org/app:latest
|
|
70
|
+
|
|
71
|
+
- name: Generate attestation
|
|
72
|
+
if: github.ref == 'refs/heads/main' # ✅ Same condition as push
|
|
73
|
+
uses: actions/attest-build-provenance@v2
|
|
74
|
+
with:
|
|
75
|
+
subject-name: ghcr.io/org/app
|
|
76
|
+
subject-digest: ${{ steps.build-push.outputs.digest }}
|
|
77
|
+
push-to-registry: true
|
|
78
|
+
- language: yaml
|
|
79
|
+
label: "Alternative: use push-to-registry: false for GitHub-only attestations"
|
|
80
|
+
code: |
|
|
81
|
+
- name: Build and push
|
|
82
|
+
id: build-push
|
|
83
|
+
uses: docker/build-push-action@v6
|
|
84
|
+
with:
|
|
85
|
+
push: true
|
|
86
|
+
tags: ghcr.io/org/app:latest
|
|
87
|
+
|
|
88
|
+
- name: Generate attestation (stored in GitHub, not registry)
|
|
89
|
+
uses: actions/attest-build-provenance@v2
|
|
90
|
+
with:
|
|
91
|
+
subject-name: ghcr.io/org/app
|
|
92
|
+
subject-digest: ${{ steps.build-push.outputs.digest }}
|
|
93
|
+
push-to-registry: false # ✅ Default; stores attestation in GitHub only
|
|
94
|
+
prevention:
|
|
95
|
+
- "Always guard `push-to-registry: true` attestation steps with the same `if:` condition used for the image push"
|
|
96
|
+
- "Remember: `load: true` in docker/build-push-action loads the image into the local Docker daemon only — it does not push to any registry"
|
|
97
|
+
- "Use `push-to-registry: false` (the default) when attestations only need to be verified via `gh attestation verify`, not via the registry manifest"
|
|
98
|
+
- "Set `push: ${{ steps.should-push.outputs.result }}` and reuse that output in the attestation step's `if:` condition to keep them in sync"
|
|
99
|
+
docs:
|
|
100
|
+
- url: "https://github.com/actions/attest-build-provenance"
|
|
101
|
+
label: "actions/attest-build-provenance README"
|
|
102
|
+
- url: "https://docs.github.com/en/actions/security-guides/using-artifact-attestations-to-establish-provenance-for-builds"
|
|
103
|
+
label: "Using artifact attestations to establish provenance for builds"
|
|
104
|
+
- url: "https://github.com/actions/attest-build-provenance/issues/747"
|
|
105
|
+
label: "actions/attest-build-provenance#747: 404 when uploading artifact to container registry"
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
id: triggers-018
|
|
2
|
+
title: "workflow_run: download-artifact finds no artifacts without explicit run-id"
|
|
3
|
+
category: triggers
|
|
4
|
+
severity: silent-failure
|
|
5
|
+
tags:
|
|
6
|
+
- workflow_run
|
|
7
|
+
- download-artifact
|
|
8
|
+
- run-id
|
|
9
|
+
- cross-workflow
|
|
10
|
+
- artifact
|
|
11
|
+
patterns:
|
|
12
|
+
- regex: "No artifacts found"
|
|
13
|
+
flags: "i"
|
|
14
|
+
- regex: "Unable to find any artifacts for the associated workflow"
|
|
15
|
+
flags: "i"
|
|
16
|
+
- regex: "No artifacts were found with the provided run ID"
|
|
17
|
+
flags: "i"
|
|
18
|
+
error_messages:
|
|
19
|
+
- "No artifacts found"
|
|
20
|
+
- "Unable to find any artifacts for the associated workflow run"
|
|
21
|
+
- "Warning: No artifacts were found with the provided run ID."
|
|
22
|
+
- "Error: Unable to find any artifacts for the associated workflow run"
|
|
23
|
+
root_cause: |
|
|
24
|
+
When a workflow is triggered by the `workflow_run` event, it runs in the context of
|
|
25
|
+
**its own** workflow run — not the triggering workflow run. If `actions/download-artifact`
|
|
26
|
+
is called without specifying `run-id`, it defaults to `${{ github.run_id }}` which is the
|
|
27
|
+
ID of the triggered (consumer) workflow — not the upstream workflow that produced the
|
|
28
|
+
artifacts.
|
|
29
|
+
|
|
30
|
+
Since the consumer workflow produces no artifacts itself, `download-artifact` finds nothing.
|
|
31
|
+
This is a silent failure in some versions: the step exits without error but no files are
|
|
32
|
+
downloaded, causing subsequent steps that depend on the artifact to fail with confusing errors.
|
|
33
|
+
|
|
34
|
+
The artifact was uploaded in the triggering workflow's run. To download it, you must
|
|
35
|
+
explicitly reference `github.event.workflow_run.id` (the upstream run's ID).
|
|
36
|
+
fix: |
|
|
37
|
+
Add `run-id: ${{ github.event.workflow_run.id }}` to the `actions/download-artifact`
|
|
38
|
+
step. This tells the action to download from the triggering workflow run rather than
|
|
39
|
+
the current workflow run.
|
|
40
|
+
|
|
41
|
+
Also ensure the token has `actions: read` permission to download artifacts from other runs.
|
|
42
|
+
fix_code:
|
|
43
|
+
- language: yaml
|
|
44
|
+
label: "Wrong: download-artifact defaults to current run (finds nothing in workflow_run)"
|
|
45
|
+
code: |
|
|
46
|
+
on:
|
|
47
|
+
workflow_run:
|
|
48
|
+
workflows: ["CI"]
|
|
49
|
+
types: [completed]
|
|
50
|
+
|
|
51
|
+
jobs:
|
|
52
|
+
deploy:
|
|
53
|
+
runs-on: ubuntu-latest
|
|
54
|
+
steps:
|
|
55
|
+
- uses: actions/download-artifact@v4
|
|
56
|
+
with:
|
|
57
|
+
name: build-output # ❌ Defaults to github.run_id (this run, has no artifacts)
|
|
58
|
+
- language: yaml
|
|
59
|
+
label: "Fix: explicitly reference the triggering run's ID"
|
|
60
|
+
code: |
|
|
61
|
+
on:
|
|
62
|
+
workflow_run:
|
|
63
|
+
workflows: ["CI"]
|
|
64
|
+
types: [completed]
|
|
65
|
+
|
|
66
|
+
jobs:
|
|
67
|
+
deploy:
|
|
68
|
+
runs-on: ubuntu-latest
|
|
69
|
+
permissions:
|
|
70
|
+
actions: read # Required to download artifacts from other workflow runs
|
|
71
|
+
steps:
|
|
72
|
+
- uses: actions/download-artifact@v4
|
|
73
|
+
with:
|
|
74
|
+
name: build-output
|
|
75
|
+
run-id: ${{ github.event.workflow_run.id }} # ✅ The upstream run's ID
|
|
76
|
+
- language: yaml
|
|
77
|
+
label: "Guard: only run if triggering workflow succeeded"
|
|
78
|
+
code: |
|
|
79
|
+
on:
|
|
80
|
+
workflow_run:
|
|
81
|
+
workflows: ["CI"]
|
|
82
|
+
types: [completed]
|
|
83
|
+
|
|
84
|
+
jobs:
|
|
85
|
+
deploy:
|
|
86
|
+
if: github.event.workflow_run.conclusion == 'success' # ✅ Don't deploy on failure
|
|
87
|
+
runs-on: ubuntu-latest
|
|
88
|
+
permissions:
|
|
89
|
+
actions: read
|
|
90
|
+
steps:
|
|
91
|
+
- uses: actions/download-artifact@v4
|
|
92
|
+
with:
|
|
93
|
+
name: build-output
|
|
94
|
+
run-id: ${{ github.event.workflow_run.id }}
|
|
95
|
+
prevention:
|
|
96
|
+
- "Always specify `run-id: ${{ github.event.workflow_run.id }}` when downloading artifacts in a `workflow_run`-triggered workflow"
|
|
97
|
+
- "Add `permissions: actions: read` to any job that downloads artifacts from a different workflow run"
|
|
98
|
+
- "Guard the job with `if: github.event.workflow_run.conclusion == 'success'` to skip on upstream failures"
|
|
99
|
+
- "Use `github.event.workflow_run.id` (not `github.run_id`) — they are different: the former is the upstream run, the latter is the current consumer run"
|
|
100
|
+
docs:
|
|
101
|
+
- url: "https://docs.github.com/en/actions/writing-workflows/choosing-when-your-workflow-runs/events-that-trigger-workflows#workflow_run"
|
|
102
|
+
label: "Events that trigger workflows — workflow_run"
|
|
103
|
+
- url: "https://docs.github.com/en/actions/writing-workflows/choosing-what-your-workflow-does/storing-and-sharing-data-from-a-workflow#downloading-artifacts-from-a-different-workflow-run"
|
|
104
|
+
label: "Downloading artifacts from a different workflow run"
|
|
105
|
+
- url: "https://github.com/actions/download-artifact"
|
|
106
|
+
label: "actions/download-artifact README"
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
id: yaml-syntax-023
|
|
2
|
+
title: "Reusable workflow: env context rejected in `jobs.<job_id>.with` inputs"
|
|
3
|
+
category: yaml-syntax
|
|
4
|
+
severity: error
|
|
5
|
+
tags:
|
|
6
|
+
- reusable-workflow
|
|
7
|
+
- env-context
|
|
8
|
+
- with-inputs
|
|
9
|
+
- workflow_call
|
|
10
|
+
- variable-scoping
|
|
11
|
+
patterns:
|
|
12
|
+
- regex: "Unrecognized named-value: 'env'"
|
|
13
|
+
flags: "i"
|
|
14
|
+
- regex: "Context access might be invalid: env"
|
|
15
|
+
flags: "i"
|
|
16
|
+
- regex: "The env context is not available"
|
|
17
|
+
flags: "i"
|
|
18
|
+
error_messages:
|
|
19
|
+
- "Unrecognized named-value: 'env'. Located at position 1 within expression: env.MY_VAR"
|
|
20
|
+
- "Context access might be invalid: env"
|
|
21
|
+
- "The env context is not available to reusable workflow inputs"
|
|
22
|
+
root_cause: |
|
|
23
|
+
When calling a reusable workflow using `jobs.<job_id>.uses`, the `env` context is not
|
|
24
|
+
available inside the `with:` block. Only the following contexts are permitted at that
|
|
25
|
+
evaluation point: `github`, `inputs`, `needs`, `strategy`, and `matrix`.
|
|
26
|
+
|
|
27
|
+
This is a platform-level restriction. The `env` context is resolved during job execution
|
|
28
|
+
on the runner, but reusable workflow `with:` inputs are evaluated at workflow dispatch time
|
|
29
|
+
(before a runner is allocated), so `env` values are simply unavailable.
|
|
30
|
+
|
|
31
|
+
Top-level `env:` blocks defined in the caller workflow cannot be referenced inside
|
|
32
|
+
`jobs.<job_id>.with` — using `${{ env.MY_VAR }}` there produces a syntax validation
|
|
33
|
+
error that prevents the workflow from running at all.
|
|
34
|
+
fix: |
|
|
35
|
+
Replace `${{ env.MY_VAR }}` in reusable workflow `with:` inputs with one of:
|
|
36
|
+
|
|
37
|
+
1. `${{ vars.MY_VAR }}` — repository or organization variable (preferred for non-secret
|
|
38
|
+
configuration values that are reused across workflows)
|
|
39
|
+
2. Hardcoded literal value directly in `with:`
|
|
40
|
+
3. An intermediate job that exposes the value as a job output, then reference it via
|
|
41
|
+
`${{ needs.prepare.outputs.my_var }}`
|
|
42
|
+
fix_code:
|
|
43
|
+
- language: yaml
|
|
44
|
+
label: "Wrong: env context in reusable workflow with inputs"
|
|
45
|
+
code: |
|
|
46
|
+
env:
|
|
47
|
+
DEPLOY_ENV: "production"
|
|
48
|
+
|
|
49
|
+
jobs:
|
|
50
|
+
deploy:
|
|
51
|
+
uses: org/repo/.github/workflows/deploy.yml@main
|
|
52
|
+
with:
|
|
53
|
+
environment: ${{ env.DEPLOY_ENV }} # ❌ Error: env context not available
|
|
54
|
+
- language: yaml
|
|
55
|
+
label: "Fix option 1: use vars context (repository/org variable)"
|
|
56
|
+
code: |
|
|
57
|
+
jobs:
|
|
58
|
+
deploy:
|
|
59
|
+
uses: org/repo/.github/workflows/deploy.yml@main
|
|
60
|
+
with:
|
|
61
|
+
environment: ${{ vars.DEPLOY_ENV }} # ✅ vars context works in with:
|
|
62
|
+
- language: yaml
|
|
63
|
+
label: "Fix option 2: propagate via intermediate job output"
|
|
64
|
+
code: |
|
|
65
|
+
env:
|
|
66
|
+
DEPLOY_ENV: "production"
|
|
67
|
+
|
|
68
|
+
jobs:
|
|
69
|
+
prepare:
|
|
70
|
+
runs-on: ubuntu-latest
|
|
71
|
+
outputs:
|
|
72
|
+
deploy_env: ${{ steps.set-env.outputs.value }}
|
|
73
|
+
steps:
|
|
74
|
+
- id: set-env
|
|
75
|
+
run: echo "value=$DEPLOY_ENV" >> "$GITHUB_OUTPUT"
|
|
76
|
+
|
|
77
|
+
deploy:
|
|
78
|
+
needs: prepare
|
|
79
|
+
uses: org/repo/.github/workflows/deploy.yml@main
|
|
80
|
+
with:
|
|
81
|
+
environment: ${{ needs.prepare.outputs.deploy_env }} # ✅
|
|
82
|
+
- language: yaml
|
|
83
|
+
label: "Fix option 3: hardcode the value directly"
|
|
84
|
+
code: |
|
|
85
|
+
jobs:
|
|
86
|
+
deploy:
|
|
87
|
+
uses: org/repo/.github/workflows/deploy.yml@main
|
|
88
|
+
with:
|
|
89
|
+
environment: "production" # ✅ Literal values always work
|
|
90
|
+
prevention:
|
|
91
|
+
- "Store workflow-wide configuration values in repository or organization variables (`vars` context) so they are available in reusable workflow `with:` blocks"
|
|
92
|
+
- "Only `github`, `inputs`, `needs`, `strategy`, and `matrix` contexts are available in `jobs.<job_id>.with` — never `env`, `secrets`, or `steps`"
|
|
93
|
+
- "If a value must be computed at runtime, use an intermediate job with `outputs:` and reference via `needs.<job_id>.outputs.<key>`"
|
|
94
|
+
docs:
|
|
95
|
+
- url: "https://docs.github.com/en/actions/sharing-automations/reusing-workflows#limitations"
|
|
96
|
+
label: "Reusing workflows — Limitations"
|
|
97
|
+
- url: "https://docs.github.com/en/actions/writing-workflows/choosing-what-your-workflow-does/contexts#context-availability"
|
|
98
|
+
label: "GitHub Actions context availability by workflow key"
|
|
99
|
+
- url: "https://github.com/actions/runner/issues/1413"
|
|
100
|
+
label: "actions/runner#1413: env not available in reusable workflow with inputs (known limitation)"
|
|
101
|
+
- url: "https://github.com/actions/toolkit/issues/931"
|
|
102
|
+
label: "actions/toolkit#931: Variable scoping across reusable workflows (58 reactions)"
|
package/package.json
CHANGED