@htekdev/actions-debugger 1.0.16 → 1.0.18
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/known-unsolved/no-automatic-job-retry.yml +92 -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/github-env-same-step-not-available.yml +72 -0
- package/errors/triggers/push-pull-request-duplicate-runs.yml +74 -0
- package/errors/triggers/workflow-dispatch-inputs-string-coercion.yml +89 -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,92 @@
|
|
|
1
|
+
id: known-unsolved-019
|
|
2
|
+
title: "No built-in automatic retry for individual failed jobs — only full workflow re-run or manual step"
|
|
3
|
+
category: known-unsolved
|
|
4
|
+
severity: limitation
|
|
5
|
+
tags:
|
|
6
|
+
- retry
|
|
7
|
+
- flaky
|
|
8
|
+
- job
|
|
9
|
+
- re-run
|
|
10
|
+
- known-limitation
|
|
11
|
+
- matrix
|
|
12
|
+
patterns:
|
|
13
|
+
- regex: "retry|re-run|rerun|flaky"
|
|
14
|
+
flags: "i"
|
|
15
|
+
error_messages:
|
|
16
|
+
- "There is no native retry: N option for jobs in GitHub Actions"
|
|
17
|
+
root_cause: |
|
|
18
|
+
GitHub Actions provides no built-in retry: count field at the job level. When a
|
|
19
|
+
job fails due to a flaky test, a transient network error, or an intermittent
|
|
20
|
+
service dependency, the only native options are: (1) manually re-run the failed
|
|
21
|
+
job via the UI or API, (2) re-run the entire workflow, or (3) add shell-level
|
|
22
|
+
retry loops inside run steps. There is no declarative way to say "retry this job
|
|
23
|
+
up to N times automatically before marking it as failed." This is a long-standing
|
|
24
|
+
platform limitation tracked in multiple GitHub Community discussions. The
|
|
25
|
+
workflow-level timeout-minutes and job-level timeout-minutes exist, but neither
|
|
26
|
+
provides automatic retry semantics. For matrix jobs, a single flaky matrix entry
|
|
27
|
+
failing causes the whole matrix (or the workflow, with default fail-fast:true) to
|
|
28
|
+
fail with no automatic per-matrix-slot retry.
|
|
29
|
+
fix: |
|
|
30
|
+
Workarounds depend on the scope of flakiness. For step-level flakiness, use a
|
|
31
|
+
shell retry loop or the nick-fields/retry third-party action. For job-level
|
|
32
|
+
flakiness in CI, consider using the GitHub REST API with a calling orchestrator
|
|
33
|
+
workflow that re-dispatches on failure. For matrix flakiness, capture failed
|
|
34
|
+
matrix entries and dispatch a targeted follow-up workflow run. Manual re-run via
|
|
35
|
+
gh run rerun --failed is the simplest workaround for human-in-the-loop flows.
|
|
36
|
+
fix_code:
|
|
37
|
+
- language: yaml
|
|
38
|
+
label: "Workaround — shell-level retry loop inside a run step"
|
|
39
|
+
code: |
|
|
40
|
+
steps:
|
|
41
|
+
- name: Flaky network step with retry
|
|
42
|
+
run: |
|
|
43
|
+
for i in 1 2 3; do
|
|
44
|
+
curl -sf https://api.example.com/deploy && break
|
|
45
|
+
echo "Attempt $i failed, retrying in 10s..."
|
|
46
|
+
sleep 10
|
|
47
|
+
done
|
|
48
|
+
- language: yaml
|
|
49
|
+
label: "Workaround — nick-fields/retry action for step-level retry"
|
|
50
|
+
code: |
|
|
51
|
+
steps:
|
|
52
|
+
- name: Retry flaky step
|
|
53
|
+
uses: nick-fields/retry@v3
|
|
54
|
+
with:
|
|
55
|
+
timeout_minutes: 10
|
|
56
|
+
max_attempts: 3
|
|
57
|
+
command: npm test
|
|
58
|
+
- language: yaml
|
|
59
|
+
label: "Workaround — re-run only failed jobs via gh CLI after workflow completes"
|
|
60
|
+
code: |
|
|
61
|
+
# In a post-pipeline script or calling workflow:
|
|
62
|
+
# gh run rerun <RUN_ID> --failed re-runs only the jobs that failed
|
|
63
|
+
# Automate this with an on.workflow_run trigger checking conclusion == 'failure'
|
|
64
|
+
on:
|
|
65
|
+
workflow_run:
|
|
66
|
+
workflows: ["CI"]
|
|
67
|
+
types: [completed]
|
|
68
|
+
|
|
69
|
+
jobs:
|
|
70
|
+
auto-retry:
|
|
71
|
+
if: github.event.workflow_run.conclusion == 'failure'
|
|
72
|
+
runs-on: ubuntu-latest
|
|
73
|
+
steps:
|
|
74
|
+
- name: Re-run failed jobs once
|
|
75
|
+
env:
|
|
76
|
+
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
77
|
+
run: |
|
|
78
|
+
gh run rerun ${{ github.event.workflow_run.id }} --failed --repo ${{ github.repository }}
|
|
79
|
+
prevention:
|
|
80
|
+
- "Design tests and deployment steps to be idempotent so manual re-runs are safe"
|
|
81
|
+
- "Use step-level retry wrappers (shell loops or nick-fields/retry) for known-flaky external calls"
|
|
82
|
+
- "Separate flaky integration tests into their own workflow so re-runs are scoped and cheaper"
|
|
83
|
+
- "Track flakiness metrics; persistent flakiness signals a real bug, not just a retry need"
|
|
84
|
+
docs:
|
|
85
|
+
- url: "https://github.com/orgs/community/discussions/26186"
|
|
86
|
+
label: "GitHub Community — native job retry support request"
|
|
87
|
+
- url: "https://docs.github.com/en/actions/managing-workflow-runs-and-deployments/managing-workflow-runs/re-running-workflows-and-jobs"
|
|
88
|
+
label: "Re-running workflows and jobs — GitHub Actions"
|
|
89
|
+
- url: "https://github.com/nick-fields/retry"
|
|
90
|
+
label: "nick-fields/retry — step-level retry action (third-party workaround)"
|
|
91
|
+
- url: "https://cli.github.com/manual/gh_run_rerun"
|
|
92
|
+
label: "gh run rerun — GitHub CLI reference"
|
|
@@ -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,72 @@
|
|
|
1
|
+
id: silent-failures-024
|
|
2
|
+
title: "GITHUB_ENV variable written in a step is not available within the same step"
|
|
3
|
+
category: silent-failures
|
|
4
|
+
severity: silent-failure
|
|
5
|
+
tags:
|
|
6
|
+
- GITHUB_ENV
|
|
7
|
+
- environment-variables
|
|
8
|
+
- step-ordering
|
|
9
|
+
- run
|
|
10
|
+
- same-step
|
|
11
|
+
patterns:
|
|
12
|
+
- regex: "GITHUB_ENV[^\\n]*\\n[^\\n]*\\$\\{?[A-Z_][A-Z0-9_]*\\}?"
|
|
13
|
+
flags: "ms"
|
|
14
|
+
- regex: "echo\\s+[\"']?[A-Z_][A-Z0-9_]*=[^\"'\\n]+[\"']?\\s*>>\\s*\\$GITHUB_ENV"
|
|
15
|
+
flags: "i"
|
|
16
|
+
error_messages:
|
|
17
|
+
- "echo MY_VAR=value >> $GITHUB_ENV"
|
|
18
|
+
root_cause: |
|
|
19
|
+
Writes to $GITHUB_ENV (appending NAME=value lines to the runner's environment file)
|
|
20
|
+
take effect for all SUBSEQUENT steps in the same job, but NOT for the current
|
|
21
|
+
step's run block. The Actions runner reads the environment file once at the start
|
|
22
|
+
of each step, before the run block executes. Any $GITHUB_ENV writes made during
|
|
23
|
+
that run block are not re-read until the next step begins. Consequently, if a run
|
|
24
|
+
block writes `echo "BUILD_ID=abc" >> $GITHUB_ENV` and then immediately references
|
|
25
|
+
`$BUILD_ID` later in the same run block, the variable is empty. No error or warning
|
|
26
|
+
is emitted — the reference silently evaluates to an empty string. The identical
|
|
27
|
+
delayed-effect behavior applies to $GITHUB_PATH: path entries added in one step
|
|
28
|
+
are only visible in the PATH of subsequent steps.
|
|
29
|
+
fix: |
|
|
30
|
+
Split the write and the read into separate steps. If you need the value within the
|
|
31
|
+
same shell invocation, use a native shell variable assignment (VAR=value) for
|
|
32
|
+
immediate access and still write to $GITHUB_ENV if the value is needed by later
|
|
33
|
+
steps. Do not rely on $GITHUB_ENV for intra-step communication.
|
|
34
|
+
fix_code:
|
|
35
|
+
- language: yaml
|
|
36
|
+
label: "Wrong — reference GITHUB_ENV variable in the same step that writes it"
|
|
37
|
+
code: |
|
|
38
|
+
steps:
|
|
39
|
+
- name: Set and use variable (BROKEN — BUILD_ID is empty)
|
|
40
|
+
run: |
|
|
41
|
+
echo "BUILD_ID=abc123" >> $GITHUB_ENV
|
|
42
|
+
echo "Build ID is: $BUILD_ID" # Empty — GITHUB_ENV not re-read mid-step
|
|
43
|
+
- language: yaml
|
|
44
|
+
label: "Correct — read the exported variable in the following step"
|
|
45
|
+
code: |
|
|
46
|
+
steps:
|
|
47
|
+
- name: Export variable
|
|
48
|
+
run: echo "BUILD_ID=abc123" >> $GITHUB_ENV
|
|
49
|
+
|
|
50
|
+
- name: Use exported variable
|
|
51
|
+
run: echo "Build ID is: $BUILD_ID" # Available here — next step reads GITHUB_ENV
|
|
52
|
+
- language: yaml
|
|
53
|
+
label: "Correct — use a shell variable for same-step access, also export for later steps"
|
|
54
|
+
code: |
|
|
55
|
+
steps:
|
|
56
|
+
- name: Compute and export build ID
|
|
57
|
+
run: |
|
|
58
|
+
BUILD_ID=$(git rev-parse --short HEAD)
|
|
59
|
+
echo "BUILD_ID=${BUILD_ID}" >> $GITHUB_ENV # Export for later steps
|
|
60
|
+
echo "Build ID is: ${BUILD_ID}" # Shell var — available immediately
|
|
61
|
+
prevention:
|
|
62
|
+
- "Never reference a variable by its GITHUB_ENV name ($VAR_NAME) in the same run block that writes it"
|
|
63
|
+
- "Use native shell variables (VAR=value; echo $VAR) for same-step access; only use GITHUB_ENV for cross-step sharing"
|
|
64
|
+
- "The same rule applies to $GITHUB_PATH — path additions take effect in the next step, not the current one"
|
|
65
|
+
- "If a step relies on a GITHUB_ENV variable set by a prior step in the same run, ensure step ordering is correct"
|
|
66
|
+
docs:
|
|
67
|
+
- url: "https://docs.github.com/en/actions/writing-workflows/choosing-what-your-workflow-does/store-information-in-variables#passing-a-value-between-steps"
|
|
68
|
+
label: "Passing values between steps using GITHUB_ENV — GitHub Actions"
|
|
69
|
+
- url: "https://github.com/orgs/community/discussions/26672"
|
|
70
|
+
label: "GitHub Community — GITHUB_ENV variable not available in same step it is written"
|
|
71
|
+
- url: "https://docs.github.com/en/actions/writing-workflows/choosing-what-your-workflow-does/workflow-commands-for-github-actions#setting-an-environment-variable"
|
|
72
|
+
label: "Setting an environment variable via workflow commands — GitHub Actions"
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
id: triggers-017
|
|
2
|
+
title: "push and pull_request events both fire for same-repo PR branches causing duplicate CI runs"
|
|
3
|
+
category: triggers
|
|
4
|
+
severity: warning
|
|
5
|
+
tags:
|
|
6
|
+
- push
|
|
7
|
+
- pull_request
|
|
8
|
+
- duplicate-runs
|
|
9
|
+
- concurrency
|
|
10
|
+
- same-repo
|
|
11
|
+
- branch
|
|
12
|
+
patterns:
|
|
13
|
+
- regex: "on:\\s*\\n\\s+(push|pull_request):"
|
|
14
|
+
flags: "ms"
|
|
15
|
+
error_messages:
|
|
16
|
+
- "Duplicate workflow runs triggered for the same commit SHA"
|
|
17
|
+
- "Two simultaneous CI runs on the same branch"
|
|
18
|
+
root_cause: |
|
|
19
|
+
When a developer pushes to a branch that has an open pull request within the same
|
|
20
|
+
repository (not a fork), both the push event and the pull_request event (types:
|
|
21
|
+
[synchronize]) fire independently for the same commit SHA. This creates two
|
|
22
|
+
separate workflow runs executing identical CI checks simultaneously. For fork PRs
|
|
23
|
+
only the pull_request event fires (the fork push doesn't trigger the base repo),
|
|
24
|
+
so duplication only affects same-repository branches. The redundant run wastes
|
|
25
|
+
billed minutes, creates confusing duplicate status check entries on the PR, and
|
|
26
|
+
can cause race conditions in deployment or release workflows where two runs race
|
|
27
|
+
to update the same environment.
|
|
28
|
+
fix: |
|
|
29
|
+
For workflows intended purely as PR checks, use only on.pull_request and remove
|
|
30
|
+
the push trigger. If the same workflow needs to run both on PRs and on direct
|
|
31
|
+
pushes to main/trunk (post-merge), filter with branches: so they don't overlap.
|
|
32
|
+
Adding a concurrency group keyed on github.ref or github.head_ref automatically
|
|
33
|
+
cancels the slower duplicate run when both triggers must remain.
|
|
34
|
+
fix_code:
|
|
35
|
+
- language: yaml
|
|
36
|
+
label: "Option 1 — separate triggers so they do not overlap"
|
|
37
|
+
code: |
|
|
38
|
+
on:
|
|
39
|
+
# PR CI: only pull_request covers commits pushed to the PR branch
|
|
40
|
+
pull_request:
|
|
41
|
+
branches: [main]
|
|
42
|
+
# Post-merge CI: only the push to main after merging
|
|
43
|
+
push:
|
|
44
|
+
branches: [main]
|
|
45
|
+
# This way a push to a feature branch triggers pull_request ONLY,
|
|
46
|
+
# and a merge to main triggers push ONLY — no duplicates.
|
|
47
|
+
- language: yaml
|
|
48
|
+
label: "Option 2 — concurrency group to cancel the slower duplicate"
|
|
49
|
+
code: |
|
|
50
|
+
on:
|
|
51
|
+
push:
|
|
52
|
+
branches-ignore: [main]
|
|
53
|
+
pull_request:
|
|
54
|
+
branches: [main]
|
|
55
|
+
|
|
56
|
+
concurrency:
|
|
57
|
+
group: ci-${{ github.head_ref || github.ref }}
|
|
58
|
+
cancel-in-progress: true
|
|
59
|
+
# When both push and pull_request fire for the same branch, the second
|
|
60
|
+
# run cancels the first, leaving only one active run per branch.
|
|
61
|
+
prevention:
|
|
62
|
+
- "Audit every workflow with both on.push and on.pull_request — verify the branch filters don't overlap for same-repo contributors"
|
|
63
|
+
- "For PR validation CI, prefer on.pull_request only; add a separate on.push.branches: [main] for post-merge checks"
|
|
64
|
+
- "Always add a concurrency group when both push and pull_request triggers are needed to prevent redundant runs"
|
|
65
|
+
- "Check your Actions billing dashboard for unexpectedly doubled minute usage as a signal for duplicate trigger overlap"
|
|
66
|
+
docs:
|
|
67
|
+
- url: "https://docs.github.com/en/actions/writing-workflows/choosing-when-your-workflow-runs/events-that-trigger-workflows#pull_request"
|
|
68
|
+
label: "pull_request event — GitHub Actions documentation"
|
|
69
|
+
- url: "https://docs.github.com/en/actions/writing-workflows/choosing-when-your-workflow-runs/events-that-trigger-workflows#push"
|
|
70
|
+
label: "push event — GitHub Actions documentation"
|
|
71
|
+
- url: "https://docs.github.com/en/actions/writing-workflows/choosing-what-your-workflow-does/control-the-concurrency-of-workflows-and-jobs"
|
|
72
|
+
label: "Controlling concurrency of workflows and jobs — GitHub Actions"
|
|
73
|
+
- url: "https://github.com/orgs/community/discussions/26284"
|
|
74
|
+
label: "GitHub Community — avoiding duplicate workflow runs on push and pull_request"
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
id: triggers-016
|
|
2
|
+
title: "workflow_dispatch inputs are always string type regardless of declared type"
|
|
3
|
+
category: triggers
|
|
4
|
+
severity: silent-failure
|
|
5
|
+
tags:
|
|
6
|
+
- workflow_dispatch
|
|
7
|
+
- inputs
|
|
8
|
+
- type-coercion
|
|
9
|
+
- boolean
|
|
10
|
+
- number
|
|
11
|
+
- expression
|
|
12
|
+
patterns:
|
|
13
|
+
- regex: "inputs\\.[a-zA-Z_][a-zA-Z0-9_]*\\s*==\\s*(true|false)"
|
|
14
|
+
flags: "i"
|
|
15
|
+
- regex: "inputs\\.[a-zA-Z_][a-zA-Z0-9_]*\\s*[><!]=?\\s*[0-9]"
|
|
16
|
+
flags: "i"
|
|
17
|
+
error_messages:
|
|
18
|
+
- "if: ${{ inputs.enable_debug == true }}"
|
|
19
|
+
- "if: ${{ inputs.retry_count > 3 }}"
|
|
20
|
+
- "if: ${{ inputs.deploy == false }}"
|
|
21
|
+
root_cause: |
|
|
22
|
+
All workflow_dispatch input values are delivered as strings at runtime, regardless
|
|
23
|
+
of the type: boolean or type: number declaration in the workflow YAML. The type
|
|
24
|
+
declaration controls the GitHub UI widget (checkbox vs text field) but does NOT
|
|
25
|
+
change the runtime data type. Comparing inputs.my_flag == true evaluates as
|
|
26
|
+
string('true') == boolean(true) which is always false. Similarly, arithmetic
|
|
27
|
+
comparisons like inputs.count > 3 perform lexicographic string comparison, not
|
|
28
|
+
numeric comparison. This is documented in GitHub Actions docs but commonly missed
|
|
29
|
+
by developers relying on the YAML type declaration to enforce runtime types.
|
|
30
|
+
The same caveat applies to inputs passed via the GitHub REST API and GitHub CLI.
|
|
31
|
+
fix: |
|
|
32
|
+
Compare boolean inputs against the string 'true' or 'false'. For numeric inputs,
|
|
33
|
+
wrap with fromJSON() before arithmetic comparison. The expression
|
|
34
|
+
inputs.enable_debug == 'true' correctly evaluates when the checkbox is checked
|
|
35
|
+
in the GitHub UI or when the value 'true' is passed via the API.
|
|
36
|
+
fix_code:
|
|
37
|
+
- language: yaml
|
|
38
|
+
label: "Boolean input — compare against string literal 'true'"
|
|
39
|
+
code: |
|
|
40
|
+
on:
|
|
41
|
+
workflow_dispatch:
|
|
42
|
+
inputs:
|
|
43
|
+
enable_debug:
|
|
44
|
+
type: boolean
|
|
45
|
+
description: "Enable debug mode"
|
|
46
|
+
default: false
|
|
47
|
+
|
|
48
|
+
jobs:
|
|
49
|
+
build:
|
|
50
|
+
runs-on: ubuntu-latest
|
|
51
|
+
steps:
|
|
52
|
+
- name: Debug step
|
|
53
|
+
# WRONG: inputs.enable_debug == true (string vs boolean, always false)
|
|
54
|
+
# CORRECT: compare against string
|
|
55
|
+
if: inputs.enable_debug == 'true'
|
|
56
|
+
run: echo "Debug mode enabled"
|
|
57
|
+
- language: yaml
|
|
58
|
+
label: "Number input — use fromJSON() for numeric comparison"
|
|
59
|
+
code: |
|
|
60
|
+
on:
|
|
61
|
+
workflow_dispatch:
|
|
62
|
+
inputs:
|
|
63
|
+
retry_count:
|
|
64
|
+
type: number
|
|
65
|
+
description: "Number of retries"
|
|
66
|
+
default: 3
|
|
67
|
+
|
|
68
|
+
jobs:
|
|
69
|
+
deploy:
|
|
70
|
+
runs-on: ubuntu-latest
|
|
71
|
+
steps:
|
|
72
|
+
- name: High-retry warning
|
|
73
|
+
# WRONG: inputs.retry_count > 5 (string comparison, '6' > '5' is true but '10' > '5' is false lexicographically)
|
|
74
|
+
# CORRECT: cast to number first
|
|
75
|
+
if: fromJSON(inputs.retry_count) > 5
|
|
76
|
+
run: echo "Warning: high retry count configured"
|
|
77
|
+
prevention:
|
|
78
|
+
- "Always compare workflow_dispatch boolean inputs against the string 'true' or 'false', never native boolean literals"
|
|
79
|
+
- "Use fromJSON(inputs.my_number) before any arithmetic or numeric comparison on number inputs"
|
|
80
|
+
- "Add an early diagnostic step echoing ${{ toJSON(inputs) }} to inspect actual runtime types during development"
|
|
81
|
+
- "Document in workflow comments that all workflow_dispatch inputs are strings at runtime, regardless of declared type"
|
|
82
|
+
- "When passing inputs via API or gh CLI, always pass boolean values as the strings 'true' or 'false'"
|
|
83
|
+
docs:
|
|
84
|
+
- url: "https://docs.github.com/en/actions/writing-workflows/choosing-when-your-workflow-runs/events-that-trigger-workflows#providing-inputs"
|
|
85
|
+
label: "workflow_dispatch inputs documentation — GitHub Actions"
|
|
86
|
+
- url: "https://github.com/actions/runner/issues/1483"
|
|
87
|
+
label: "actions/runner#1483 — Boolean workflow_dispatch inputs treated as strings"
|
|
88
|
+
- url: "https://docs.github.com/en/actions/writing-workflows/choosing-what-your-workflow-does/evaluate-expressions-in-workflows-and-actions#fromjson"
|
|
89
|
+
label: "fromJSON() expression function — GitHub Actions"
|
package/package.json
CHANGED