@htekdev/actions-debugger 1.0.96 → 1.0.98
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/caching-artifacts/caching-artifacts-054.yml +102 -0
- package/errors/known-unsolved/reusable-workflow-github-event-is-workflow-call-event.yml +112 -0
- package/errors/permissions-auth/oidc-token-rate-limited-large-parallel-matrix.yml +114 -0
- package/errors/permissions-auth/permissions-auth-055.yml +110 -0
- package/errors/permissions-auth/permissions-auth-057.yml +97 -0
- package/errors/runner-environment/runner-environment-164.yml +89 -0
- package/errors/runner-environment/runner-environment-165.yml +86 -0
- package/errors/silent-failures/vars-context-environment-shadows-repo-org.yml +97 -0
- package/errors/triggers/triggers-064.yml +122 -0
- package/errors/yaml-syntax/timeout-minutes-env-context-unavailable.yml +93 -0
- package/errors/yaml-syntax/yaml-syntax-060.yml +100 -0
- package/errors/yaml-syntax/yaml-syntax-062.yml +101 -0
- package/package.json +1 -1
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
id: caching-artifacts-054
|
|
2
|
+
title: "upload-artifact@v4 Rejects Artifact Names Containing Special Characters"
|
|
3
|
+
category: caching-artifacts
|
|
4
|
+
severity: error
|
|
5
|
+
tags:
|
|
6
|
+
- upload-artifact
|
|
7
|
+
- artifact-name
|
|
8
|
+
- v4-migration
|
|
9
|
+
- special-characters
|
|
10
|
+
- validation
|
|
11
|
+
- branch-name
|
|
12
|
+
patterns:
|
|
13
|
+
- regex: 'The artifact name is not valid'
|
|
14
|
+
flags: "i"
|
|
15
|
+
- regex: 'Artifact name .+ is not valid'
|
|
16
|
+
flags: "i"
|
|
17
|
+
- regex: 'characters are not allowed in artifact names?'
|
|
18
|
+
flags: "i"
|
|
19
|
+
error_messages:
|
|
20
|
+
- "Error: The artifact name is not valid. The following characters are not allowed in artifact names: \\ / : * ? \" < > |"
|
|
21
|
+
- "Artifact name 'feature/login-ui' is not valid. The following characters are not allowed: /"
|
|
22
|
+
- "Error: An artifact name must not contain the following characters: \\ / : * ? \" < > |"
|
|
23
|
+
root_cause: |
|
|
24
|
+
actions/upload-artifact@v4 introduced strict server-side validation of
|
|
25
|
+
artifact names. The following characters are now explicitly rejected because
|
|
26
|
+
they are illegal in file or directory names on the artifact storage backend:
|
|
27
|
+
|
|
28
|
+
\ / : * ? " < > |
|
|
29
|
+
|
|
30
|
+
In v3, artifact names were either silently sanitized or accepted with these
|
|
31
|
+
characters (depending on the storage backend behavior). v4 fails hard with a
|
|
32
|
+
clear error.
|
|
33
|
+
|
|
34
|
+
The most common failure mode is dynamic artifact names constructed from:
|
|
35
|
+
- Branch names: github.ref_name or github.head_ref (contain /)
|
|
36
|
+
- Pull request titles: github.event.pull_request.title (contain : / " etc.)
|
|
37
|
+
- Matrix values: matrix.os or matrix.target (may contain : on Windows paths)
|
|
38
|
+
- Composite keys: combining multiple context values with : as separator
|
|
39
|
+
|
|
40
|
+
Example patterns that break:
|
|
41
|
+
name: build-${{ github.ref_name }} # "feature/login" contains /
|
|
42
|
+
name: results-${{ matrix.os }} # "windows-latest" is fine but
|
|
43
|
+
# some custom os values may not be
|
|
44
|
+
name: ${{ github.event.pull_request.title }} # PR titles can contain any char
|
|
45
|
+
fix: |
|
|
46
|
+
Sanitize the artifact name before passing it to upload-artifact. Replace or
|
|
47
|
+
strip disallowed characters using a shell substitution or a prior step that
|
|
48
|
+
outputs a safe name. The simplest approach is replacing / with - or _.
|
|
49
|
+
fix_code:
|
|
50
|
+
- language: yaml
|
|
51
|
+
label: "Sanitize branch name artifact using shell substitution"
|
|
52
|
+
code: |
|
|
53
|
+
- name: Upload build artifact
|
|
54
|
+
uses: actions/upload-artifact@v4
|
|
55
|
+
with:
|
|
56
|
+
# Replace / with - to make branch name safe
|
|
57
|
+
name: build-${{ github.ref_name && replace(github.ref_name, '/', '-') || 'main' }}
|
|
58
|
+
path: dist/
|
|
59
|
+
|
|
60
|
+
- language: yaml
|
|
61
|
+
label: "Compute safe artifact name in prior step output"
|
|
62
|
+
code: |
|
|
63
|
+
- name: Compute safe artifact name
|
|
64
|
+
id: artifact-name
|
|
65
|
+
run: |
|
|
66
|
+
# Strip or replace characters not allowed in artifact names: \ / : * ? " < > |
|
|
67
|
+
SAFE_NAME=$(echo "${{ github.ref_name }}" | tr '/:*?"<>|\\' '-')
|
|
68
|
+
echo "name=build-${SAFE_NAME}" >> $GITHUB_OUTPUT
|
|
69
|
+
|
|
70
|
+
- name: Upload artifact
|
|
71
|
+
uses: actions/upload-artifact@v4
|
|
72
|
+
with:
|
|
73
|
+
name: ${{ steps.artifact-name.outputs.name }}
|
|
74
|
+
path: dist/
|
|
75
|
+
|
|
76
|
+
- language: yaml
|
|
77
|
+
label: "Use matrix hash or index for safe matrix artifact names"
|
|
78
|
+
code: |
|
|
79
|
+
strategy:
|
|
80
|
+
matrix:
|
|
81
|
+
config: [debug, release]
|
|
82
|
+
os: [ubuntu-latest, windows-latest, macos-latest]
|
|
83
|
+
|
|
84
|
+
steps:
|
|
85
|
+
- name: Upload matrix artifact
|
|
86
|
+
uses: actions/upload-artifact@v4
|
|
87
|
+
with:
|
|
88
|
+
# Safe: all matrix.* values here contain no special chars
|
|
89
|
+
name: build-${{ matrix.os }}-${{ matrix.config }}
|
|
90
|
+
path: dist/
|
|
91
|
+
prevention:
|
|
92
|
+
- "Never pass github.ref_name, github.head_ref, or PR titles directly as artifact names without sanitization."
|
|
93
|
+
- "When constructing dynamic artifact names, run them through tr or sed to replace / : * ? \" < > | \\ with - or _."
|
|
94
|
+
- "Use actionlint or a pre-commit hook to flag unescaped context expressions in artifact name: fields."
|
|
95
|
+
- "Consider using a matrix index or a hash of the artifact key for truly collision-safe names."
|
|
96
|
+
docs:
|
|
97
|
+
- url: "https://github.com/actions/upload-artifact/releases/tag/v4.0.0"
|
|
98
|
+
label: "upload-artifact v4.0.0 release notes — breaking changes"
|
|
99
|
+
- url: "https://github.com/actions/upload-artifact#inputs"
|
|
100
|
+
label: "upload-artifact README — name input and allowed characters"
|
|
101
|
+
- url: "https://stackoverflow.com/questions/77975069"
|
|
102
|
+
label: "SO#77975069 — upload-artifact v4 artifact name is not valid"
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
id: known-unsolved-054
|
|
2
|
+
title: 'github.event in a reusable workflow is the workflow_call event, not the caller event'
|
|
3
|
+
category: known-unsolved
|
|
4
|
+
severity: silent-failure
|
|
5
|
+
tags:
|
|
6
|
+
- reusable-workflow
|
|
7
|
+
- workflow-call
|
|
8
|
+
- github-event
|
|
9
|
+
- context
|
|
10
|
+
- inputs
|
|
11
|
+
- github-context
|
|
12
|
+
patterns:
|
|
13
|
+
- regex: 'github\.event\.(pull_request|commits|release|head_commit)\b'
|
|
14
|
+
flags: 'i'
|
|
15
|
+
error_messages: []
|
|
16
|
+
root_cause: |
|
|
17
|
+
When a caller workflow invokes a reusable workflow via jobs.<id>.uses, the
|
|
18
|
+
github.event context inside the callee is the workflow_call event object,
|
|
19
|
+
NOT the triggering event (push, pull_request, schedule, etc.) of the caller.
|
|
20
|
+
|
|
21
|
+
All event-specific payload fields are undefined inside the reusable workflow
|
|
22
|
+
and evaluate to empty string or null without any error:
|
|
23
|
+
- github.event.pull_request (undefined — returns null)
|
|
24
|
+
- github.event.commits (undefined — returns null)
|
|
25
|
+
- github.event.release (undefined — returns null)
|
|
26
|
+
- github.event.head_commit (undefined — returns null)
|
|
27
|
+
- github.event.inputs (undefined — use inputs context instead)
|
|
28
|
+
|
|
29
|
+
Additionally, github.event_name inside the callee always returns 'workflow_call',
|
|
30
|
+
regardless of what event triggered the caller.
|
|
31
|
+
|
|
32
|
+
Importantly, github.ref, github.sha, and github.actor DO retain the caller's
|
|
33
|
+
values inside the callee — it is specifically github.event and github.event_name
|
|
34
|
+
that reflect the workflow_call context, not the caller's triggering event.
|
|
35
|
+
|
|
36
|
+
This catches developers by surprise when refactoring inline jobs into reusable
|
|
37
|
+
workflows: the refactored callee silently loses access to the event payload it
|
|
38
|
+
previously used, with no validation error or warning at parse time or runtime.
|
|
39
|
+
|
|
40
|
+
This is by design — reusable workflows execute as independent workflow_call
|
|
41
|
+
events and there is no mechanism to inherit the caller's event context. GitHub
|
|
42
|
+
has confirmed this will not change.
|
|
43
|
+
fix: |
|
|
44
|
+
There is no workaround that exposes the caller's github.event inside the callee.
|
|
45
|
+
The required approach is to declare all needed event data as explicit inputs:
|
|
46
|
+
in the reusable workflow and forward them via with: in the caller.
|
|
47
|
+
|
|
48
|
+
github.ref, github.sha, and github.actor are available in the callee without
|
|
49
|
+
any explicit forwarding — they are inherited automatically from the caller context.
|
|
50
|
+
fix_code:
|
|
51
|
+
- language: yaml
|
|
52
|
+
label: 'Wrong — accessing caller event payload inside reusable workflow (silently empty)'
|
|
53
|
+
code: |
|
|
54
|
+
# .github/workflows/reusable.yml — these are all EMPTY inside the callee
|
|
55
|
+
on:
|
|
56
|
+
workflow_call:
|
|
57
|
+
|
|
58
|
+
jobs:
|
|
59
|
+
process:
|
|
60
|
+
runs-on: ubuntu-latest
|
|
61
|
+
steps:
|
|
62
|
+
- run: echo "PR = ${{ github.event.pull_request.number }}" # null
|
|
63
|
+
- run: echo "Event = ${{ github.event_name }}" # 'workflow_call'
|
|
64
|
+
- run: echo "Commit = ${{ github.event.head_commit.message }}" # null
|
|
65
|
+
- language: yaml
|
|
66
|
+
label: 'Correct — declare inputs in callee and forward event data from caller'
|
|
67
|
+
code: |
|
|
68
|
+
# .github/workflows/reusable.yml — declare needed event data as inputs
|
|
69
|
+
on:
|
|
70
|
+
workflow_call:
|
|
71
|
+
inputs:
|
|
72
|
+
pr_number:
|
|
73
|
+
type: number
|
|
74
|
+
default: 0
|
|
75
|
+
description: 'Pull request number from github.event.pull_request.number'
|
|
76
|
+
triggering_event:
|
|
77
|
+
type: string
|
|
78
|
+
required: true
|
|
79
|
+
description: 'The caller event name (github.event_name)'
|
|
80
|
+
|
|
81
|
+
jobs:
|
|
82
|
+
process:
|
|
83
|
+
runs-on: ubuntu-latest
|
|
84
|
+
steps:
|
|
85
|
+
- run: echo "PR = ${{ inputs.pr_number }}"
|
|
86
|
+
- run: echo "Triggered by = ${{ inputs.triggering_event }}"
|
|
87
|
+
# github.ref and github.sha are safe to use directly — they are inherited
|
|
88
|
+
- run: echo "SHA = ${{ github.sha }}"
|
|
89
|
+
|
|
90
|
+
# ---- CALLER WORKFLOW ----
|
|
91
|
+
# .github/workflows/caller.yml — forward event data via with:
|
|
92
|
+
on: [push, pull_request]
|
|
93
|
+
|
|
94
|
+
jobs:
|
|
95
|
+
call-reusable:
|
|
96
|
+
uses: ./.github/workflows/reusable.yml
|
|
97
|
+
with:
|
|
98
|
+
pr_number: ${{ github.event.pull_request.number || 0 }}
|
|
99
|
+
triggering_event: ${{ github.event_name }}
|
|
100
|
+
prevention:
|
|
101
|
+
- 'Never access github.event.* fields directly inside a reusable workflow — they will always be empty'
|
|
102
|
+
- 'github.event_name inside a reusable workflow always returns workflow_call, not the caller event'
|
|
103
|
+
- 'Declare all required event data as explicit workflow_call inputs and forward with with: in the caller'
|
|
104
|
+
- 'github.ref, github.sha, and github.actor are inherited from the caller and are safe to use'
|
|
105
|
+
- 'Add a comment in the reusable workflow reminding contributors not to use github.event directly'
|
|
106
|
+
docs:
|
|
107
|
+
- url: 'https://docs.github.com/en/actions/sharing-automations/reusing-workflows#limitations'
|
|
108
|
+
label: 'GitHub Docs — Reusing workflows: Limitations'
|
|
109
|
+
- url: 'https://docs.github.com/en/actions/writing-workflows/choosing-what-your-workflow-does/contexts#github-context'
|
|
110
|
+
label: 'GitHub Docs — github context'
|
|
111
|
+
- url: 'https://docs.github.com/en/actions/sharing-automations/reusing-workflows#passing-inputs-and-secrets-to-a-called-workflow'
|
|
112
|
+
label: 'GitHub Docs — Passing inputs and secrets to a called workflow'
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
id: permissions-auth-056
|
|
2
|
+
title: 'OIDC token requests rate-limited in large parallel matrix — random 429 job failures'
|
|
3
|
+
category: permissions-auth
|
|
4
|
+
severity: error
|
|
5
|
+
tags:
|
|
6
|
+
- oidc
|
|
7
|
+
- id-token
|
|
8
|
+
- rate-limit
|
|
9
|
+
- matrix
|
|
10
|
+
- parallel
|
|
11
|
+
- aws
|
|
12
|
+
- gcp
|
|
13
|
+
- azure
|
|
14
|
+
patterns:
|
|
15
|
+
- regex: 'Failed to get ID token.*429'
|
|
16
|
+
flags: 'i'
|
|
17
|
+
- regex: 'ACTIONS_ID_TOKEN_REQUEST_URL.*429'
|
|
18
|
+
flags: 'i'
|
|
19
|
+
- regex: 'No ID token is available.*rate.limit'
|
|
20
|
+
flags: 'i'
|
|
21
|
+
error_messages:
|
|
22
|
+
- "Error: Failed to get ID token: Request failed with status code 429"
|
|
23
|
+
- "Error: Failed to get ID token: Error code: 429"
|
|
24
|
+
root_cause: |
|
|
25
|
+
GitHub's OIDC token endpoint (ACTIONS_ID_TOKEN_REQUEST_URL) has per-workflow-run
|
|
26
|
+
rate limits on simultaneous token requests. When a large matrix workflow spawns
|
|
27
|
+
many parallel jobs (typically 20+) that all request OIDC tokens at the start of
|
|
28
|
+
their runs, the burst of simultaneous requests can exceed the endpoint's rate
|
|
29
|
+
limit, and some jobs receive HTTP 429 responses.
|
|
30
|
+
|
|
31
|
+
Cloud provider actions that call the OIDC endpoint internally during setup
|
|
32
|
+
are affected:
|
|
33
|
+
- aws-actions/configure-aws-credentials
|
|
34
|
+
- google-github-actions/auth
|
|
35
|
+
- azure/login (with OIDC federated credentials)
|
|
36
|
+
|
|
37
|
+
In a large matrix, all legs start within seconds of each other, creating a
|
|
38
|
+
simultaneous burst that exceeds the endpoint limit. The failure is
|
|
39
|
+
non-deterministic: some jobs succeed and some fail on any given run. Manually
|
|
40
|
+
re-running the failed jobs typically succeeds because the burst window has
|
|
41
|
+
passed. This intermittent behavior makes the 429 root cause difficult to
|
|
42
|
+
diagnose without inspecting verbose action output for the HTTP status code.
|
|
43
|
+
|
|
44
|
+
The rate limit is enforced per workflow run, not per repository or per user.
|
|
45
|
+
Actions that include built-in retry logic may mask the error by successfully
|
|
46
|
+
retrying, but add latency to affected jobs.
|
|
47
|
+
fix: |
|
|
48
|
+
Reduce parallel OIDC token requests by setting max-parallel on the matrix
|
|
49
|
+
strategy. A value between 5 and 15 typically prevents rate limit breaches
|
|
50
|
+
while still providing meaningful parallelism.
|
|
51
|
+
|
|
52
|
+
Alternatively, restructure the workflow to acquire a single OIDC-derived
|
|
53
|
+
credential in a setup job and pass it as a job output to downstream matrix
|
|
54
|
+
jobs — but this must be done carefully to avoid exposing short-lived tokens
|
|
55
|
+
in workflow logs. Check the cloud provider's documentation for recommended
|
|
56
|
+
token-sharing patterns.
|
|
57
|
+
|
|
58
|
+
Enable step debug logging (ACTIONS_STEP_DEBUG secret set to 'true') to
|
|
59
|
+
confirm the 429 root cause when diagnosing intermittent OIDC failures.
|
|
60
|
+
fix_code:
|
|
61
|
+
- language: yaml
|
|
62
|
+
label: 'Set max-parallel to prevent simultaneous OIDC token request burst'
|
|
63
|
+
code: |
|
|
64
|
+
jobs:
|
|
65
|
+
deploy:
|
|
66
|
+
strategy:
|
|
67
|
+
matrix:
|
|
68
|
+
region: [us-east-1, us-west-2, eu-west-1, ap-southeast-1, ap-northeast-1,
|
|
69
|
+
sa-east-1, ca-central-1, ap-south-1, ap-east-1, eu-central-1]
|
|
70
|
+
max-parallel: 5 # Prevents simultaneous OIDC token burst
|
|
71
|
+
runs-on: ubuntu-latest
|
|
72
|
+
permissions:
|
|
73
|
+
id-token: write
|
|
74
|
+
contents: read
|
|
75
|
+
steps:
|
|
76
|
+
- name: Configure AWS credentials
|
|
77
|
+
uses: aws-actions/configure-aws-credentials@v4
|
|
78
|
+
with:
|
|
79
|
+
role-to-assume: arn:aws:iam::123456789:role/deploy-${{ matrix.region }}
|
|
80
|
+
aws-region: ${{ matrix.region }}
|
|
81
|
+
- name: Deploy
|
|
82
|
+
run: ./scripts/deploy.sh ${{ matrix.region }}
|
|
83
|
+
- language: yaml
|
|
84
|
+
label: 'Enable OIDC debug logging to confirm 429 rate limit as root cause'
|
|
85
|
+
code: |
|
|
86
|
+
# Add this secret to the repository: ACTIONS_STEP_DEBUG = true
|
|
87
|
+
# Then re-run the failing workflow and check step logs for:
|
|
88
|
+
# "Request failed with status code 429"
|
|
89
|
+
# in the cloud auth action's output to confirm rate limiting.
|
|
90
|
+
|
|
91
|
+
jobs:
|
|
92
|
+
deploy:
|
|
93
|
+
runs-on: ubuntu-latest
|
|
94
|
+
permissions:
|
|
95
|
+
id-token: write
|
|
96
|
+
steps:
|
|
97
|
+
- name: Configure credentials (verbose for debugging)
|
|
98
|
+
uses: aws-actions/configure-aws-credentials@v4
|
|
99
|
+
with:
|
|
100
|
+
role-to-assume: arn:aws:iam::123456789:role/ci-role
|
|
101
|
+
aws-region: us-east-1
|
|
102
|
+
prevention:
|
|
103
|
+
- 'Use max-parallel on matrix strategies that use OIDC authentication to limit burst requests'
|
|
104
|
+
- 'Enable ACTIONS_STEP_DEBUG secret to see HTTP status codes in cloud auth action logs'
|
|
105
|
+
- 'Treat intermittent OIDC failures in large matrix workflows as a 429 rate limit until proven otherwise'
|
|
106
|
+
- 'Check whether the cloud auth action has built-in retry logic — some versions retry automatically'
|
|
107
|
+
- 'Consider staggering matrix jobs with a sleep step if max-parallel alone does not resolve the issue'
|
|
108
|
+
docs:
|
|
109
|
+
- url: 'https://docs.github.com/en/actions/security-for-github-actions/security-hardening-your-deployments/about-security-hardening-with-openid-connect'
|
|
110
|
+
label: 'GitHub Docs — About security hardening with OpenID Connect'
|
|
111
|
+
- url: 'https://docs.github.com/en/actions/writing-workflows/workflow-syntax-for-github-actions#jobsjob_idstrategymax-parallel'
|
|
112
|
+
label: 'GitHub Docs — jobs.<job_id>.strategy.max-parallel'
|
|
113
|
+
- url: 'https://docs.github.com/en/actions/monitoring-and-troubleshooting-workflows/enabling-debug-logging'
|
|
114
|
+
label: 'GitHub Docs — Enabling debug logging'
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
id: permissions-auth-055
|
|
2
|
+
title: "GITHUB_TOKEN Cannot Push to .github/workflows/ — Requires PAT with workflow Scope"
|
|
3
|
+
category: permissions-auth
|
|
4
|
+
severity: error
|
|
5
|
+
tags:
|
|
6
|
+
- github-token
|
|
7
|
+
- workflow-files
|
|
8
|
+
- workflow-scope
|
|
9
|
+
- pat
|
|
10
|
+
- contents-write
|
|
11
|
+
- self-modifying-workflow
|
|
12
|
+
patterns:
|
|
13
|
+
- regex: 'refusing to allow .+ to create or update workflow .+\.yml'
|
|
14
|
+
flags: "i"
|
|
15
|
+
- regex: 'refusing to allow a GitHub Actions App to create or update workflow'
|
|
16
|
+
flags: "i"
|
|
17
|
+
- regex: 'GH006.*refusing to allow.*workflow'
|
|
18
|
+
flags: "i"
|
|
19
|
+
error_messages:
|
|
20
|
+
- "refusing to allow a GitHub Actions App to create or update workflow .github/workflows/ci.yml"
|
|
21
|
+
- "remote: error: GH006: Protected branch update failed for refs/heads/main. remote: error: refusing to allow a GitHub Actions App to create or update workflow .github/workflows/deploy.yml"
|
|
22
|
+
- "refusing to allow GitHub Actions Bot to update .github/workflows/release.yml"
|
|
23
|
+
- "remote: Resolving deltas: 100% (1/1), done. remote: error: refusing to allow a GitHub Actions App to create or update workflow"
|
|
24
|
+
root_cause: |
|
|
25
|
+
GitHub enforces a hard server-side restriction preventing GITHUB_TOKEN from
|
|
26
|
+
creating or modifying files under .github/workflows/. This restriction applies
|
|
27
|
+
regardless of the permissions: block in the workflow. Even with
|
|
28
|
+
permissions: contents: write, GITHUB_TOKEN will be rejected when the push
|
|
29
|
+
includes changes to workflow files.
|
|
30
|
+
|
|
31
|
+
This security control exists to prevent privilege escalation: a compromised or
|
|
32
|
+
malicious workflow cannot modify itself or other workflows to gain additional
|
|
33
|
+
permissions or persist access. The restriction is enforced at the remote push
|
|
34
|
+
validation layer, not by the GitHub Actions runtime.
|
|
35
|
+
|
|
36
|
+
Common scenarios that trigger this:
|
|
37
|
+
- Automated dependency update workflows that pin action versions
|
|
38
|
+
(e.g., actions/checkout@v3 -> @v4) in workflow files
|
|
39
|
+
- Workflow generators that create or update .github/workflows/*.yml
|
|
40
|
+
- Renovate, Dependabot, or custom bots using GITHUB_TOKEN to update action refs
|
|
41
|
+
- Release automation that stamps version information into workflow files
|
|
42
|
+
- Repository template sync tools that propagate workflow updates
|
|
43
|
+
|
|
44
|
+
Note: fine-grained PATs WITH the repository contents:write permission DO allow
|
|
45
|
+
workflow file updates (this restriction only applies to GITHUB_TOKEN
|
|
46
|
+
specifically, not all tokens).
|
|
47
|
+
fix: |
|
|
48
|
+
Use a Personal Access Token (classic PAT) with the workflow scope, a
|
|
49
|
+
fine-grained PAT with the repository contents:write permission, or a GitHub
|
|
50
|
+
App with the workflows repository permission. Store the token as a repository
|
|
51
|
+
or organization secret and use it in place of GITHUB_TOKEN for the checkout
|
|
52
|
+
or push steps.
|
|
53
|
+
fix_code:
|
|
54
|
+
- language: yaml
|
|
55
|
+
label: "Use PAT with workflow scope via checkout persist-credentials"
|
|
56
|
+
code: |
|
|
57
|
+
jobs:
|
|
58
|
+
update-workflow:
|
|
59
|
+
runs-on: ubuntu-latest
|
|
60
|
+
steps:
|
|
61
|
+
- uses: actions/checkout@v4
|
|
62
|
+
with:
|
|
63
|
+
# Use a PAT with workflow scope — GITHUB_TOKEN cannot push workflow files
|
|
64
|
+
token: ${{ secrets.WORKFLOW_PAT }}
|
|
65
|
+
|
|
66
|
+
- name: Update workflow file
|
|
67
|
+
run: |
|
|
68
|
+
sed -i 's/actions\/checkout@v3/actions\/checkout@v4/g' .github/workflows/*.yml
|
|
69
|
+
|
|
70
|
+
- name: Commit and push workflow changes
|
|
71
|
+
env:
|
|
72
|
+
GH_TOKEN: ${{ secrets.WORKFLOW_PAT }}
|
|
73
|
+
run: |
|
|
74
|
+
echo "Commit workflow changes using PAT token"
|
|
75
|
+
# repository operations using the authenticated token
|
|
76
|
+
|
|
77
|
+
- language: yaml
|
|
78
|
+
label: "Use GitHub App token with workflows permission"
|
|
79
|
+
code: |
|
|
80
|
+
jobs:
|
|
81
|
+
update-workflow:
|
|
82
|
+
runs-on: ubuntu-latest
|
|
83
|
+
steps:
|
|
84
|
+
- uses: actions/create-github-app-token@v1
|
|
85
|
+
id: app-token
|
|
86
|
+
with:
|
|
87
|
+
app-id: ${{ secrets.APP_ID }}
|
|
88
|
+
private-key: ${{ secrets.APP_PRIVATE_KEY }}
|
|
89
|
+
|
|
90
|
+
- uses: actions/checkout@v4
|
|
91
|
+
with:
|
|
92
|
+
token: ${{ steps.app-token.outputs.token }}
|
|
93
|
+
|
|
94
|
+
- name: Modify workflow file and push
|
|
95
|
+
env:
|
|
96
|
+
GH_TOKEN: ${{ steps.app-token.outputs.token }}
|
|
97
|
+
run: |
|
|
98
|
+
echo "Commit workflow changes via GitHub App token"
|
|
99
|
+
prevention:
|
|
100
|
+
- "Store a classic PAT with the workflow scope as a secret (e.g., WORKFLOW_PAT) for any job that writes to .github/workflows/."
|
|
101
|
+
- "Do not expect permissions: contents: write to grant GITHUB_TOKEN the ability to modify workflow files — it will always be rejected."
|
|
102
|
+
- "Use GitHub Apps with the workflows repository permission for scalable, org-wide workflow automation."
|
|
103
|
+
- "Prefer Renovate or Dependabot (which use their own token/app with workflow scope) for automated action version pinning."
|
|
104
|
+
docs:
|
|
105
|
+
- url: "https://docs.github.com/en/authentication/keeping-your-account-and-data-secure/managing-your-personal-access-tokens#creating-a-personal-access-token-classic"
|
|
106
|
+
label: "GitHub Docs — Classic PAT workflow scope"
|
|
107
|
+
- url: "https://docs.github.com/en/actions/security-for-github-actions/security-guides/automatic-token-authentication#permissions-for-the-github_token"
|
|
108
|
+
label: "GitHub Docs — GITHUB_TOKEN permissions"
|
|
109
|
+
- url: "https://stackoverflow.com/questions/64059610"
|
|
110
|
+
label: "SO#64059610 — refusing to allow GitHub Actions App to create or update workflow"
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
id: permissions-auth-057
|
|
2
|
+
title: "gh CLI Uses GITHUB_TOKEN Env Variable, Not actions/checkout Custom Token"
|
|
3
|
+
category: permissions-auth
|
|
4
|
+
severity: silent-failure
|
|
5
|
+
tags:
|
|
6
|
+
- gh-cli
|
|
7
|
+
- GITHUB_TOKEN
|
|
8
|
+
- checkout
|
|
9
|
+
- PAT
|
|
10
|
+
- token-scope
|
|
11
|
+
patterns:
|
|
12
|
+
- regex: 'Resource not accessible by integration'
|
|
13
|
+
flags: "i"
|
|
14
|
+
- regex: 'gh:.*HTTP 403'
|
|
15
|
+
flags: "i"
|
|
16
|
+
- regex: 'gh:.*could not resolve to a node'
|
|
17
|
+
flags: "i"
|
|
18
|
+
error_messages:
|
|
19
|
+
- "gh: Resource not accessible by integration (HTTP 403)"
|
|
20
|
+
- "remote: Permission to org/repo.git denied to github-actions[bot]."
|
|
21
|
+
- "gh: GraphQL: Could not resolve to a node with the global id of ''"
|
|
22
|
+
root_cause: |
|
|
23
|
+
When `actions/checkout` is configured with a custom `token:` (e.g., a PAT or
|
|
24
|
+
GitHub App installation token), the custom token is written into the local
|
|
25
|
+
git credential helper so that git operations (push, fetch) authenticate with
|
|
26
|
+
the custom token.
|
|
27
|
+
|
|
28
|
+
However, the `gh` CLI does NOT read from the git credential helper. It resolves
|
|
29
|
+
authentication by checking these environment variables in priority order:
|
|
30
|
+
1. `GH_TOKEN`
|
|
31
|
+
2. `GITHUB_TOKEN`
|
|
32
|
+
3. System keychain / credential store (unavailable on hosted runners)
|
|
33
|
+
|
|
34
|
+
GitHub Actions always sets `GITHUB_TOKEN` to the default job token, regardless
|
|
35
|
+
of any custom token supplied to `actions/checkout`. When a step uses `gh` CLI
|
|
36
|
+
without an explicit `GH_TOKEN`, it picks up the default `GITHUB_TOKEN` which
|
|
37
|
+
may lack the required scopes (e.g., packages:write, org membership, cross-repo
|
|
38
|
+
access) — producing a 403 or permission error that appears to contradict the
|
|
39
|
+
checkout step succeeding with the PAT.
|
|
40
|
+
|
|
41
|
+
This mismatch is especially confusing because `git push` (using credentials
|
|
42
|
+
from checkout) succeeds while `gh pr create` or `gh release create` (using
|
|
43
|
+
GITHUB_TOKEN) fails in the same job.
|
|
44
|
+
fix: |
|
|
45
|
+
Explicitly pass the custom token to `gh` via the `GH_TOKEN` or `GITHUB_TOKEN`
|
|
46
|
+
environment variable. Do not rely on `actions/checkout token:` to authenticate
|
|
47
|
+
`gh` CLI.
|
|
48
|
+
|
|
49
|
+
Setting `GH_TOKEN` at the step or job level is preferred as it keeps the scope
|
|
50
|
+
limited to where the elevated token is needed.
|
|
51
|
+
fix_code:
|
|
52
|
+
- language: yaml
|
|
53
|
+
label: "Explicitly set GH_TOKEN for gh CLI steps"
|
|
54
|
+
code: |
|
|
55
|
+
jobs:
|
|
56
|
+
release:
|
|
57
|
+
runs-on: ubuntu-latest
|
|
58
|
+
steps:
|
|
59
|
+
- uses: actions/checkout@v4
|
|
60
|
+
with:
|
|
61
|
+
# Custom token sets git credentials — does NOT affect gh CLI
|
|
62
|
+
token: ${{ secrets.ORG_PAT }}
|
|
63
|
+
|
|
64
|
+
- name: Create GitHub release
|
|
65
|
+
# gh reads GH_TOKEN (not checkout token) — pass it explicitly
|
|
66
|
+
env:
|
|
67
|
+
GH_TOKEN: ${{ secrets.ORG_PAT }}
|
|
68
|
+
run: |
|
|
69
|
+
gh release create v1.0.0 --title "v1.0.0" --generate-notes
|
|
70
|
+
- language: yaml
|
|
71
|
+
label: "Set GITHUB_TOKEN at job level to override for all gh CLI steps"
|
|
72
|
+
code: |
|
|
73
|
+
jobs:
|
|
74
|
+
release:
|
|
75
|
+
runs-on: ubuntu-latest
|
|
76
|
+
# Override GITHUB_TOKEN for the entire job so all gh steps use the PAT
|
|
77
|
+
env:
|
|
78
|
+
GITHUB_TOKEN: ${{ secrets.ORG_PAT }}
|
|
79
|
+
steps:
|
|
80
|
+
- uses: actions/checkout@v4
|
|
81
|
+
with:
|
|
82
|
+
token: ${{ secrets.ORG_PAT }}
|
|
83
|
+
- run: gh pr create --title "Automated PR" --body "Auto-generated"
|
|
84
|
+
prevention:
|
|
85
|
+
- "Always check which token gh CLI is using — it reads GH_TOKEN or GITHUB_TOKEN, not git credentials set by actions/checkout."
|
|
86
|
+
- "Set GH_TOKEN or GITHUB_TOKEN explicitly in env: blocks for any step that uses gh CLI with elevated permissions."
|
|
87
|
+
- "Use permissions: at the job level to grant additional scopes to the default GITHUB_TOKEN before reaching for a PAT."
|
|
88
|
+
- "When using a custom checkout token for git operations, remember gh CLI still needs GH_TOKEN set separately."
|
|
89
|
+
docs:
|
|
90
|
+
- url: "https://cli.github.com/manual/gh_help_environment"
|
|
91
|
+
label: "gh CLI — GH_TOKEN environment variable"
|
|
92
|
+
- url: "https://docs.github.com/en/actions/security-for-github-actions/security-guides/automatic-token-authentication"
|
|
93
|
+
label: "GitHub Docs — GITHUB_TOKEN automatic authentication"
|
|
94
|
+
- url: "https://github.com/cli/cli/issues/2534"
|
|
95
|
+
label: "cli/cli#2534 — gh CLI ignores git credential helper token"
|
|
96
|
+
- url: "https://github.com/actions/checkout/issues/1168"
|
|
97
|
+
label: "actions/checkout#1168 — Custom token not used by gh CLI"
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
id: runner-environment-164
|
|
2
|
+
title: "setup-python python-version-file Fails with pyenv Pre-Release Version Notation"
|
|
3
|
+
category: runner-environment
|
|
4
|
+
severity: error
|
|
5
|
+
tags:
|
|
6
|
+
- setup-python
|
|
7
|
+
- python-version-file
|
|
8
|
+
- pyenv
|
|
9
|
+
- pre-release
|
|
10
|
+
- version-format
|
|
11
|
+
patterns:
|
|
12
|
+
- regex: 'Version \d+\.\d+\.\d+[ab]\d+ with arch .* not found'
|
|
13
|
+
flags: "i"
|
|
14
|
+
- regex: 'No available version found for [\d.]+[ab]'
|
|
15
|
+
flags: "i"
|
|
16
|
+
- regex: 'python-version-file.*\.python-version'
|
|
17
|
+
flags: "i"
|
|
18
|
+
error_messages:
|
|
19
|
+
- "Version 3.13.0a4 with arch x64 not found"
|
|
20
|
+
- "Version 3.14.0b2 with arch x64 not found"
|
|
21
|
+
- "No available version found for 3.13.0a1"
|
|
22
|
+
- "The version '3.13.0rc1' with architecture 'x64' was not found."
|
|
23
|
+
root_cause: |
|
|
24
|
+
pyenv-style `.python-version` files use native Python pre-release notation:
|
|
25
|
+
- `3.13.0a4` (alpha 4)
|
|
26
|
+
- `3.14.0b2` (beta 2)
|
|
27
|
+
- `3.13.0rc1` (release candidate 1)
|
|
28
|
+
|
|
29
|
+
`actions/setup-python` looks up versions using semver-style notation in the
|
|
30
|
+
GitHub toolcache, where pre-releases are labeled differently:
|
|
31
|
+
- `3.13.0-alpha.4`
|
|
32
|
+
- `3.14.0-beta.2`
|
|
33
|
+
- `3.13.0-rc.1`
|
|
34
|
+
|
|
35
|
+
When setup-python reads a `.python-version` file containing pyenv-style
|
|
36
|
+
notation (e.g., `3.13.0a4`), it cannot find a matching toolcache entry and
|
|
37
|
+
fails with "Version not found". Stable releases (e.g., `3.11.5`, `3.12.3`)
|
|
38
|
+
are unaffected because their notation is identical in both conventions.
|
|
39
|
+
|
|
40
|
+
This commonly surprises developers who pin a pre-release Python locally for
|
|
41
|
+
testing using pyenv and commit the `.python-version` file expecting CI to
|
|
42
|
+
use the same version.
|
|
43
|
+
fix: |
|
|
44
|
+
Convert the `.python-version` file to semver notation for pre-releases, or
|
|
45
|
+
specify the Python version directly in the workflow using `python-version:`
|
|
46
|
+
to avoid relying on the file format.
|
|
47
|
+
|
|
48
|
+
For CI purposes, pinning to a stable minor version (e.g., `3.13`) is usually
|
|
49
|
+
sufficient and avoids both the notation mismatch and toolcache fallback issues.
|
|
50
|
+
fix_code:
|
|
51
|
+
- language: yaml
|
|
52
|
+
label: "Use semver notation in .python-version for pre-releases"
|
|
53
|
+
code: |
|
|
54
|
+
# .python-version — use semver, not pyenv pre-release notation
|
|
55
|
+
#
|
|
56
|
+
# Wrong (pyenv style): 3.13.0a4
|
|
57
|
+
# Correct (semver style): 3.13.0-alpha.4
|
|
58
|
+
#
|
|
59
|
+
# Other mappings:
|
|
60
|
+
# 3.14.0b2 → 3.14.0-beta.2
|
|
61
|
+
# 3.13.0rc1 → 3.13.0-rc.1
|
|
62
|
+
|
|
63
|
+
# Workflow using the corrected file:
|
|
64
|
+
- uses: actions/setup-python@v5
|
|
65
|
+
with:
|
|
66
|
+
python-version-file: '.python-version'
|
|
67
|
+
allow-prereleases: true # Required for any pre-release version
|
|
68
|
+
- language: yaml
|
|
69
|
+
label: "Pin version directly in workflow to bypass file format issues"
|
|
70
|
+
code: |
|
|
71
|
+
- uses: actions/setup-python@v5
|
|
72
|
+
with:
|
|
73
|
+
# Use stable minor version — avoids pre-release notation issues
|
|
74
|
+
python-version: '3.13'
|
|
75
|
+
# Or use explicit semver pre-release notation:
|
|
76
|
+
# python-version: '3.13.0-alpha.4'
|
|
77
|
+
allow-prereleases: true
|
|
78
|
+
prevention:
|
|
79
|
+
- "Use semver notation (3.13.0-alpha.4) not pyenv notation (3.13.0a4) in .python-version files consumed by CI."
|
|
80
|
+
- "Prefer pinning to a stable minor version (3.13) in CI rather than a specific pre-release patch."
|
|
81
|
+
- "Always set allow-prereleases: true in setup-python when using any pre-release Python version."
|
|
82
|
+
- "Add a comment in .python-version documenting the notation convention to avoid confusion for contributors."
|
|
83
|
+
docs:
|
|
84
|
+
- url: "https://github.com/actions/setup-python/blob/main/docs/advanced-usage.md#using-the-python-version-file-input"
|
|
85
|
+
label: "setup-python docs — python-version-file input"
|
|
86
|
+
- url: "https://github.com/actions/setup-python/issues/770"
|
|
87
|
+
label: "actions/setup-python#770 — Pre-release version notation mismatch"
|
|
88
|
+
- url: "https://docs.python.org/3/faq/general.html#how-does-the-python-version-numbering-scheme-work"
|
|
89
|
+
label: "Python.org — Version numbering scheme"
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
id: runner-environment-165
|
|
2
|
+
title: "ARM64 Linux Runners: Python Packages Without linux_aarch64 Binary Wheels Fail Installation"
|
|
3
|
+
category: runner-environment
|
|
4
|
+
severity: error
|
|
5
|
+
tags:
|
|
6
|
+
- arm64
|
|
7
|
+
- ubuntu-arm
|
|
8
|
+
- python
|
|
9
|
+
- pip
|
|
10
|
+
- binary-wheels
|
|
11
|
+
- linux-aarch64
|
|
12
|
+
patterns:
|
|
13
|
+
- regex: 'No matching distribution found for .+==[\d.]+'
|
|
14
|
+
flags: "i"
|
|
15
|
+
- regex: 'Could not find a version that satisfies the requirement .+ \(from versions: none\)'
|
|
16
|
+
flags: "i"
|
|
17
|
+
- regex: 'ERROR: .+ is not supported on this platform'
|
|
18
|
+
flags: "i"
|
|
19
|
+
error_messages:
|
|
20
|
+
- "ERROR: Could not find a version that satisfies the requirement numpy==1.21.0 (from versions: none)"
|
|
21
|
+
- "ERROR: No matching distribution found for numpy==1.21.0"
|
|
22
|
+
- "error: command '/usr/bin/gcc' failed with exit code 1"
|
|
23
|
+
- "Building wheels for collected packages: X ... error: subprocess-exited-with-error"
|
|
24
|
+
root_cause: |
|
|
25
|
+
When migrating from x64 runners (ubuntu-latest, ubuntu-22.04) to ARM64
|
|
26
|
+
runners (ubuntu-24.04-arm, ubuntu-22.04-arm), Python packages pinned to
|
|
27
|
+
older versions may not have linux_aarch64 binary wheels on PyPI. Without a
|
|
28
|
+
pre-compiled wheel, pip falls back to building from source, which either
|
|
29
|
+
fails outright (missing build dependencies, compiler errors) or succeeds
|
|
30
|
+
slowly. For packages like numpy, scipy, Pillow, cryptography, and pandas,
|
|
31
|
+
ARM64 wheel support was only added in later releases. The error message "No
|
|
32
|
+
matching distribution found" is misleading because the package does exist on
|
|
33
|
+
PyPI — just not the specific version for the linux_aarch64 platform.
|
|
34
|
+
|
|
35
|
+
Commonly affected packages and the minimum version with ARM64 wheel support:
|
|
36
|
+
- numpy: >= 1.24.0
|
|
37
|
+
- Pillow: >= 9.2.0
|
|
38
|
+
- cryptography: >= 38.0.0
|
|
39
|
+
- grpcio: >= 1.50.0
|
|
40
|
+
- scipy: >= 1.9.0
|
|
41
|
+
- lxml: >= 4.9.0
|
|
42
|
+
|
|
43
|
+
This is distinct from PEP 668 (system pip blocked by externally managed env),
|
|
44
|
+
macOS Rosetta (x86_64 binary executing on Apple Silicon), and
|
|
45
|
+
ubuntu-arm64-docker-not-preinstalled (Docker not available on arm runners).
|
|
46
|
+
fix: |
|
|
47
|
+
Upgrade pinned package versions to releases that include linux_aarch64 binary
|
|
48
|
+
wheels. Use pip install --only-binary=:all: to get a clear platform error
|
|
49
|
+
instead of a slow source-build failure. If upgrading is impossible, install
|
|
50
|
+
the required build dependencies (gcc, python3-dev, libssl-dev, etc.) before
|
|
51
|
+
running pip install.
|
|
52
|
+
fix_code:
|
|
53
|
+
- language: yaml
|
|
54
|
+
label: "Upgrade pinned packages to ARM64-compatible versions"
|
|
55
|
+
code: |
|
|
56
|
+
- name: Install Python dependencies (ARM64 compatible)
|
|
57
|
+
run: |
|
|
58
|
+
# Upgrade pinned packages to versions with linux_aarch64 wheels
|
|
59
|
+
pip install "numpy>=1.24.0" "Pillow>=9.2.0" "cryptography>=38.0.0"
|
|
60
|
+
|
|
61
|
+
- language: yaml
|
|
62
|
+
label: "Use --only-binary to detect missing wheels early"
|
|
63
|
+
code: |
|
|
64
|
+
- name: Install dependencies (fail fast if no wheel)"
|
|
65
|
+
run: pip install --only-binary=:all: -r requirements.txt
|
|
66
|
+
|
|
67
|
+
- language: yaml
|
|
68
|
+
label: "Install build dependencies for packages requiring source compilation"
|
|
69
|
+
code: |
|
|
70
|
+
- name: Install build dependencies for source compilation
|
|
71
|
+
run: sudo apt-get install -y gcc python3-dev libssl-dev libffi-dev
|
|
72
|
+
|
|
73
|
+
- name: Install Python packages
|
|
74
|
+
run: pip install -r requirements.txt
|
|
75
|
+
prevention:
|
|
76
|
+
- "Before migrating to ARM64 runners, audit requirements.txt for packages lacking linux_aarch64 wheels at pinned versions."
|
|
77
|
+
- "Use pip install --only-binary=:all: -r requirements.txt in a test run to surface platform compatibility gaps."
|
|
78
|
+
- "Reference PyPI package pages to verify linux_aarch64 wheel availability for the specific version you are pinning."
|
|
79
|
+
- "Prefer unpinned or range-pinned dependencies (numpy>=1.24) over exact-pinned (numpy==1.21.0) to allow ARM64-capable versions."
|
|
80
|
+
docs:
|
|
81
|
+
- url: "https://docs.github.com/en/actions/using-github-hosted-runners/using-github-hosted-runners/about-github-hosted-runners#supported-runners-and-hardware-resources"
|
|
82
|
+
label: "GitHub Docs — Supported GitHub-hosted runners (ARM64)"
|
|
83
|
+
- url: "https://pip.pypa.io/en/stable/cli/pip_install/#cmdoption-only-binary"
|
|
84
|
+
label: "pip docs — --only-binary option"
|
|
85
|
+
- url: "https://stackoverflow.com/questions/75782839"
|
|
86
|
+
label: "SO#75782839 — pip no matching distribution on ARM64 runner"
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
id: silent-failures-088
|
|
2
|
+
title: 'vars.* context: environment-scoped variable silently shadows repository and org variable'
|
|
3
|
+
category: silent-failures
|
|
4
|
+
severity: silent-failure
|
|
5
|
+
tags:
|
|
6
|
+
- vars
|
|
7
|
+
- variables
|
|
8
|
+
- environment
|
|
9
|
+
- precedence
|
|
10
|
+
- shadow
|
|
11
|
+
- configuration
|
|
12
|
+
patterns:
|
|
13
|
+
- regex: 'vars\.[A-Z_]+'
|
|
14
|
+
flags: 'i'
|
|
15
|
+
error_messages: []
|
|
16
|
+
root_cause: |
|
|
17
|
+
The vars.* context resolves configuration variables in strict precedence order:
|
|
18
|
+
environment-scoped > repository-level > organization-level. When a job specifies
|
|
19
|
+
an environment: key, any variable defined at that deployment environment's scope
|
|
20
|
+
with the same name as a repository or organization variable silently overrides
|
|
21
|
+
the higher-scope value.
|
|
22
|
+
|
|
23
|
+
No warning or error is emitted when shadowing occurs. The workflow runs
|
|
24
|
+
successfully but uses the environment-scoped value rather than the expected
|
|
25
|
+
org or repository value.
|
|
26
|
+
|
|
27
|
+
Common scenario: an organization defines vars.REGION='us-east-1' for all repos.
|
|
28
|
+
A production deployment environment defines vars.REGION='eu-west-1' to override
|
|
29
|
+
it for EU deploys. All jobs that reference the production environment now receive
|
|
30
|
+
'eu-west-1' — including notification jobs, post-deploy validation jobs, or any
|
|
31
|
+
other job that happens to run in the production environment but was not intended
|
|
32
|
+
to use the EU override.
|
|
33
|
+
|
|
34
|
+
Unlike secrets, where absence raises a validation error, variable shadowing is
|
|
35
|
+
entirely transparent to the workflow author. Debugging requires manually
|
|
36
|
+
inspecting variable scopes in GitHub Settings across three levels (org, repo,
|
|
37
|
+
environment) to find the effective value.
|
|
38
|
+
fix: |
|
|
39
|
+
Use scope-specific prefixes for variables that intentionally override
|
|
40
|
+
parent-scope defaults (e.g., PROD_REGION instead of REGION). Audit effective
|
|
41
|
+
variable values at runtime by echoing vars.* at the start of critical jobs.
|
|
42
|
+
|
|
43
|
+
To inspect what is currently configured:
|
|
44
|
+
- GitHub > Organization Settings > Variables (org-level)
|
|
45
|
+
- Repo Settings > Secrets and variables > Variables (repo-level)
|
|
46
|
+
- Repo Settings > Environments > <name> > Variables (environment-level)
|
|
47
|
+
fix_code:
|
|
48
|
+
- language: yaml
|
|
49
|
+
label: 'Debug effective vars.* resolution in a job with environment scope'
|
|
50
|
+
code: |
|
|
51
|
+
jobs:
|
|
52
|
+
deploy:
|
|
53
|
+
environment: production
|
|
54
|
+
runs-on: ubuntu-latest
|
|
55
|
+
steps:
|
|
56
|
+
- name: Audit effective variable values
|
|
57
|
+
run: |
|
|
58
|
+
echo "REGION (effective): ${{ vars.REGION }}"
|
|
59
|
+
# If this shows 'eu-west-1' when you expected 'us-east-1',
|
|
60
|
+
# the 'production' environment has a REGION variable that
|
|
61
|
+
# silently shadows the org or repository-level value.
|
|
62
|
+
- language: yaml
|
|
63
|
+
label: 'Use environment-scoped prefixes to make overrides explicit'
|
|
64
|
+
code: |
|
|
65
|
+
# Organization variable: REGION=us-east-1
|
|
66
|
+
# Environment 'production' variable: PROD_REGION=eu-west-1 (distinct name)
|
|
67
|
+
|
|
68
|
+
jobs:
|
|
69
|
+
deploy:
|
|
70
|
+
environment: production
|
|
71
|
+
runs-on: ubuntu-latest
|
|
72
|
+
steps:
|
|
73
|
+
- name: Deploy to production
|
|
74
|
+
run: |
|
|
75
|
+
# Explicit — no ambiguity about which scope is used
|
|
76
|
+
deploy --region ${{ vars.PROD_REGION }}
|
|
77
|
+
|
|
78
|
+
notify:
|
|
79
|
+
environment: production
|
|
80
|
+
runs-on: ubuntu-latest
|
|
81
|
+
steps:
|
|
82
|
+
- name: Notify
|
|
83
|
+
run: |
|
|
84
|
+
# Without prefix, this would silently get 'eu-west-1' instead
|
|
85
|
+
# of 'us-east-1' if REGION is defined at environment scope
|
|
86
|
+
notify --datacenter ${{ vars.NOTIFICATION_REGION }}
|
|
87
|
+
prevention:
|
|
88
|
+
- 'Prefix environment-scoped variable names to make overrides explicit — PROD_REGION not REGION'
|
|
89
|
+
- 'Audit all three variable scopes before deployments: org, repo, and environment'
|
|
90
|
+
- 'Add a comment in the workflow file documenting which variables each environment is expected to override'
|
|
91
|
+
- 'For critical deployment config, avoid relying on implicit variable inheritance — reference scope-prefixed names'
|
|
92
|
+
- 'Run a variable audit step at the start of critical jobs to log effective vars.* values'
|
|
93
|
+
docs:
|
|
94
|
+
- url: 'https://docs.github.com/en/actions/writing-workflows/choosing-what-your-workflow-does/store-information-in-variables#precedence-for-variable-values'
|
|
95
|
+
label: 'GitHub Docs — Variable precedence for the vars.* context'
|
|
96
|
+
- url: 'https://docs.github.com/en/actions/managing-workflow-runs-and-deployments/managing-deployments/managing-environments-for-deployment'
|
|
97
|
+
label: 'GitHub Docs — Managing environments for deployment'
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
id: triggers-064
|
|
2
|
+
title: "workflow_run Triggered Workflow Cannot Access github.event.pull_request — PR Number Is Empty"
|
|
3
|
+
category: triggers
|
|
4
|
+
severity: silent-failure
|
|
5
|
+
tags:
|
|
6
|
+
- workflow_run
|
|
7
|
+
- pull-request
|
|
8
|
+
- event-context
|
|
9
|
+
- pr-number
|
|
10
|
+
- artifact
|
|
11
|
+
patterns:
|
|
12
|
+
- regex: 'github\.event\.pull_request\.number'
|
|
13
|
+
flags: "i"
|
|
14
|
+
- regex: 'event\.number.*workflow_run'
|
|
15
|
+
flags: "i"
|
|
16
|
+
error_messages:
|
|
17
|
+
- "# No error thrown — github.event.pull_request.number evaluates to empty string"
|
|
18
|
+
- "Error: PR number is empty"
|
|
19
|
+
root_cause: |
|
|
20
|
+
When a workflow is triggered by `on: workflow_run`, the event payload is a
|
|
21
|
+
`workflow_run` object — NOT the event that triggered the upstream workflow.
|
|
22
|
+
|
|
23
|
+
This means `github.event.pull_request`, `github.event.number`, and all
|
|
24
|
+
other pull_request-specific context keys evaluate to empty string (not an
|
|
25
|
+
error). The only event data available is under `github.event.workflow_run.*`,
|
|
26
|
+
which exposes run metadata (run ID, head branch, head SHA, conclusion, etc.)
|
|
27
|
+
but NOT the original pull request event payload.
|
|
28
|
+
|
|
29
|
+
The common pattern breaks when developers write `workflow_run`-triggered
|
|
30
|
+
workflows to post PR comments or add labels and reference
|
|
31
|
+
`${{ github.event.pull_request.number }}` — the expression silently evaluates
|
|
32
|
+
to empty string, causing downstream API calls to fail or act on the wrong
|
|
33
|
+
resource without a clear error.
|
|
34
|
+
|
|
35
|
+
`github.event.workflow_run.pull_requests` is populated by GitHub only when
|
|
36
|
+
the triggering workflow was itself triggered by a pull_request event, providing
|
|
37
|
+
a lighter-weight alternative to the artifact pattern.
|
|
38
|
+
fix: |
|
|
39
|
+
Upload the needed PR context (number, head SHA, labels) as a JSON artifact
|
|
40
|
+
in the upstream workflow. In the downstream `workflow_run` workflow, download
|
|
41
|
+
the artifact and extract the values.
|
|
42
|
+
|
|
43
|
+
For simpler cases, read `github.event.workflow_run.pull_requests[0].number`
|
|
44
|
+
— GitHub populates this array when the triggering workflow ran on a PR.
|
|
45
|
+
fix_code:
|
|
46
|
+
- language: yaml
|
|
47
|
+
label: "Upstream workflow: upload PR context as artifact"
|
|
48
|
+
code: |
|
|
49
|
+
on:
|
|
50
|
+
pull_request:
|
|
51
|
+
|
|
52
|
+
jobs:
|
|
53
|
+
build:
|
|
54
|
+
runs-on: ubuntu-latest
|
|
55
|
+
steps:
|
|
56
|
+
- uses: actions/checkout@v4
|
|
57
|
+
- run: make build
|
|
58
|
+
- name: Save PR context for downstream workflow
|
|
59
|
+
run: |
|
|
60
|
+
mkdir -p /tmp/pr-context
|
|
61
|
+
echo '${{ toJSON(github.event.pull_request) }}' \
|
|
62
|
+
> /tmp/pr-context/pr.json
|
|
63
|
+
- uses: actions/upload-artifact@v4
|
|
64
|
+
with:
|
|
65
|
+
name: pr-context
|
|
66
|
+
path: /tmp/pr-context/pr.json
|
|
67
|
+
- language: yaml
|
|
68
|
+
label: "Downstream workflow_run: download artifact and read PR number"
|
|
69
|
+
code: |
|
|
70
|
+
on:
|
|
71
|
+
workflow_run:
|
|
72
|
+
workflows: ["CI"]
|
|
73
|
+
types: [completed]
|
|
74
|
+
|
|
75
|
+
jobs:
|
|
76
|
+
report:
|
|
77
|
+
runs-on: ubuntu-latest
|
|
78
|
+
permissions:
|
|
79
|
+
actions: read
|
|
80
|
+
pull-requests: write
|
|
81
|
+
steps:
|
|
82
|
+
- uses: actions/download-artifact@v4
|
|
83
|
+
with:
|
|
84
|
+
name: pr-context
|
|
85
|
+
run-id: ${{ github.event.workflow_run.id }}
|
|
86
|
+
github-token: ${{ secrets.GITHUB_TOKEN }}
|
|
87
|
+
- name: Read PR number and comment
|
|
88
|
+
run: |
|
|
89
|
+
PR_NUMBER=$(jq '.number' pr.json)
|
|
90
|
+
echo "PR: $PR_NUMBER"
|
|
91
|
+
env:
|
|
92
|
+
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
93
|
+
- language: yaml
|
|
94
|
+
label: "Lightweight alternative: use github.event.workflow_run.pull_requests array"
|
|
95
|
+
code: |
|
|
96
|
+
on:
|
|
97
|
+
workflow_run:
|
|
98
|
+
workflows: ["CI"]
|
|
99
|
+
types: [completed]
|
|
100
|
+
|
|
101
|
+
jobs:
|
|
102
|
+
report:
|
|
103
|
+
runs-on: ubuntu-latest
|
|
104
|
+
steps:
|
|
105
|
+
- name: Get PR number from workflow_run context
|
|
106
|
+
run: |
|
|
107
|
+
echo "PR: ${{ github.event.workflow_run.pull_requests[0].number }}"
|
|
108
|
+
# Note: pull_requests array is only populated when the triggering
|
|
109
|
+
# workflow was triggered by a pull_request event from a non-fork.
|
|
110
|
+
# For forks, the array is empty — use the artifact pattern instead.
|
|
111
|
+
prevention:
|
|
112
|
+
- "Never reference github.event.pull_request in workflow_run-triggered workflows — the event payload is workflow_run, not pull_request."
|
|
113
|
+
- "Upload PR context (number, head SHA, labels) as a JSON artifact in the upstream workflow for downstream consumption."
|
|
114
|
+
- "Use github.event.workflow_run.pull_requests[0].number for a lighter approach when the triggering workflow ran on a non-fork pull_request."
|
|
115
|
+
- "Add defensive checks: if the PR number resolves to empty, skip steps that require it to avoid silent API failures."
|
|
116
|
+
docs:
|
|
117
|
+
- url: "https://docs.github.com/en/actions/writing-workflows/choosing-when-your-workflow-runs/events-that-trigger-workflows#workflow_run"
|
|
118
|
+
label: "GitHub Docs — workflow_run event"
|
|
119
|
+
- url: "https://docs.github.com/en/actions/writing-workflows/choosing-when-your-workflow-runs/events-that-trigger-workflows#using-data-from-the-triggering-workflow"
|
|
120
|
+
label: "GitHub Docs — Using data from the triggering workflow"
|
|
121
|
+
- url: "https://stackoverflow.com/questions/71570882"
|
|
122
|
+
label: "SO#71570882 — Get PR number in workflow_run triggered workflow"
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
id: yaml-syntax-061
|
|
2
|
+
title: 'env context unavailable in job-level timeout-minutes field'
|
|
3
|
+
category: yaml-syntax
|
|
4
|
+
severity: error
|
|
5
|
+
tags:
|
|
6
|
+
- env
|
|
7
|
+
- timeout-minutes
|
|
8
|
+
- context
|
|
9
|
+
- context-availability
|
|
10
|
+
- job-level
|
|
11
|
+
patterns:
|
|
12
|
+
- regex: 'Unrecognized named-value: .env.'
|
|
13
|
+
flags: 'i'
|
|
14
|
+
- regex: 'timeout-minutes.*\$\{\{.*env\.'
|
|
15
|
+
flags: 'i'
|
|
16
|
+
error_messages:
|
|
17
|
+
- "The workflow is not valid. .github/workflows/<workflow>.yml (Line: X, Col: Y): Unrecognized named-value: 'env'. Located at position Z within expression: env.MY_TIMEOUT"
|
|
18
|
+
root_cause: |
|
|
19
|
+
The env context is not available in job-level timeout-minutes expressions.
|
|
20
|
+
GitHub Actions evaluates jobs.<job_id>.timeout-minutes before steps run and
|
|
21
|
+
before step-level env values are populated. The available contexts at this
|
|
22
|
+
position are: github, inputs, vars, needs, strategy, and matrix only.
|
|
23
|
+
|
|
24
|
+
Developers commonly try to make timeouts configurable by setting a workflow-level
|
|
25
|
+
env: block (e.g., env: { JOB_TIMEOUT: 60 }) and referencing it as
|
|
26
|
+
${{ env.JOB_TIMEOUT }} in timeout-minutes, expecting it to work like env
|
|
27
|
+
references inside steps. This fails with a parse-time validation error because
|
|
28
|
+
the env context is not evaluated at job metadata positions.
|
|
29
|
+
|
|
30
|
+
Note: env IS available in step-level timeout-minutes
|
|
31
|
+
(jobs.<job_id>.steps[*].timeout-minutes) — this restriction only applies at
|
|
32
|
+
the job level. This asymmetry trips up developers who test in steps first
|
|
33
|
+
and then try to lift the same expression to job level.
|
|
34
|
+
fix: |
|
|
35
|
+
Use the vars context (repository or environment variables) instead of env for
|
|
36
|
+
job-level timeout-minutes. Repository variables can be set under
|
|
37
|
+
Settings > Secrets and variables > Variables and are available at job-evaluation
|
|
38
|
+
time.
|
|
39
|
+
|
|
40
|
+
If the timeout value needs to vary per workflow run, pass it as a workflow
|
|
41
|
+
input (inputs.*) for workflow_dispatch or workflow_call, or use matrix values
|
|
42
|
+
to parameterize timeouts per leg.
|
|
43
|
+
fix_code:
|
|
44
|
+
- language: yaml
|
|
45
|
+
label: 'Wrong — env context in job-level timeout-minutes (validation error)'
|
|
46
|
+
code: |
|
|
47
|
+
env:
|
|
48
|
+
JOB_TIMEOUT: 60
|
|
49
|
+
|
|
50
|
+
jobs:
|
|
51
|
+
build:
|
|
52
|
+
runs-on: ubuntu-latest
|
|
53
|
+
timeout-minutes: ${{ env.JOB_TIMEOUT }} # ERROR: env not available here
|
|
54
|
+
steps:
|
|
55
|
+
- run: make build
|
|
56
|
+
- language: yaml
|
|
57
|
+
label: 'Correct — use vars context for a configurable job timeout'
|
|
58
|
+
code: |
|
|
59
|
+
# Set JOB_TIMEOUT_MINUTES in repository Settings > Variables (e.g., "60")
|
|
60
|
+
jobs:
|
|
61
|
+
build:
|
|
62
|
+
runs-on: ubuntu-latest
|
|
63
|
+
timeout-minutes: ${{ fromJSON(vars.JOB_TIMEOUT_MINUTES || '60') }}
|
|
64
|
+
steps:
|
|
65
|
+
- run: make build
|
|
66
|
+
- language: yaml
|
|
67
|
+
label: 'Correct — pass timeout as workflow_dispatch input'
|
|
68
|
+
code: |
|
|
69
|
+
on:
|
|
70
|
+
workflow_dispatch:
|
|
71
|
+
inputs:
|
|
72
|
+
timeout:
|
|
73
|
+
type: number
|
|
74
|
+
default: 60
|
|
75
|
+
description: 'Job timeout in minutes'
|
|
76
|
+
|
|
77
|
+
jobs:
|
|
78
|
+
build:
|
|
79
|
+
runs-on: ubuntu-latest
|
|
80
|
+
timeout-minutes: ${{ inputs.timeout }}
|
|
81
|
+
steps:
|
|
82
|
+
- run: make build
|
|
83
|
+
prevention:
|
|
84
|
+
- 'Use vars.* (repository or environment variables) for static configurable timeouts at job level'
|
|
85
|
+
- 'Use inputs.* for runtime-configurable timeouts passed via workflow_dispatch or workflow_call'
|
|
86
|
+
- 'env context is only available at step level and below — not at the job metadata level'
|
|
87
|
+
- 'step-level timeout-minutes does support env context; only job-level timeout-minutes does not'
|
|
88
|
+
- 'Available contexts in job-level fields: github, inputs, vars, needs, strategy, matrix'
|
|
89
|
+
docs:
|
|
90
|
+
- url: 'https://docs.github.com/en/actions/writing-workflows/choosing-what-your-workflow-does/contexts#context-availability'
|
|
91
|
+
label: 'GitHub Docs — Context availability per workflow position'
|
|
92
|
+
- url: 'https://docs.github.com/en/actions/writing-workflows/workflow-syntax-for-github-actions#jobsjob_idtimeout-minutes'
|
|
93
|
+
label: 'GitHub Docs — jobs.<job_id>.timeout-minutes'
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
id: yaml-syntax-060
|
|
2
|
+
title: "Object Filter Expression .* on Null Parent Context Raises Template Validation Error"
|
|
3
|
+
category: yaml-syntax
|
|
4
|
+
severity: error
|
|
5
|
+
tags:
|
|
6
|
+
- expression
|
|
7
|
+
- object-filter
|
|
8
|
+
- null-context
|
|
9
|
+
- wildcard
|
|
10
|
+
- pull_request
|
|
11
|
+
- multi-trigger
|
|
12
|
+
patterns:
|
|
13
|
+
- regex: 'Object filter left operand must be of type Array or Object but is Null'
|
|
14
|
+
flags: "i"
|
|
15
|
+
- regex: 'github\.event\.[a-z_]+\.\*\.[a-z_]+'
|
|
16
|
+
flags: "i"
|
|
17
|
+
- regex: 'The template is not valid.*Object filter'
|
|
18
|
+
flags: "i"
|
|
19
|
+
error_messages:
|
|
20
|
+
- "Error: The template is not valid. .github/workflows/ci.yml (Line: 12, Col: 8): Object filter left operand must be of type Array or Object but is Null"
|
|
21
|
+
- "Object filter left operand must be of type Array or Object but is Null"
|
|
22
|
+
- "Unexpected value 'null'. Located at position 0 within expression: contains(github.event.pull_request.labels.*.name, 'skip-ci')"
|
|
23
|
+
root_cause: |
|
|
24
|
+
GitHub Actions supports object filtering via the .* wildcard syntax, which
|
|
25
|
+
maps over an array or object and extracts a named property from each element.
|
|
26
|
+
For example: github.event.pull_request.labels.*.name returns an array of
|
|
27
|
+
label names on a pull_request event.
|
|
28
|
+
|
|
29
|
+
When the parent context object is null — which occurs whenever the event
|
|
30
|
+
payload does not include that property — the .* filter throws a template
|
|
31
|
+
validation error at expression evaluation time rather than returning an empty
|
|
32
|
+
array. This is a common pitfall in workflows that handle multiple event types:
|
|
33
|
+
|
|
34
|
+
- On push events, github.event.pull_request is null
|
|
35
|
+
- On schedule events, github.event.issue is null
|
|
36
|
+
- On workflow_dispatch events, github.event.commits is null
|
|
37
|
+
|
|
38
|
+
The expression evaluator does NOT fall back to an empty result for null .*.
|
|
39
|
+
Instead it fails the step with a template error, even though null.property
|
|
40
|
+
(single property access) would silently return an empty string.
|
|
41
|
+
|
|
42
|
+
Typical pattern that fails on push events:
|
|
43
|
+
if: contains(github.event.pull_request.labels.*.name, 'skip-ci')
|
|
44
|
+
fix: |
|
|
45
|
+
Guard the object filter expression with an event type check so the .* filter
|
|
46
|
+
is only evaluated when the parent context is known to be non-null. Use
|
|
47
|
+
github.event_name to gate the condition, or split the if: into separate
|
|
48
|
+
trigger-specific jobs.
|
|
49
|
+
fix_code:
|
|
50
|
+
- language: yaml
|
|
51
|
+
label: "Guard with event type check"
|
|
52
|
+
code: |
|
|
53
|
+
# BAD: fails on push events because pull_request context is null
|
|
54
|
+
# if: contains(github.event.pull_request.labels.*.name, 'skip-ci')
|
|
55
|
+
|
|
56
|
+
# GOOD: gate on event type first
|
|
57
|
+
- name: Check for skip-ci label
|
|
58
|
+
if: >
|
|
59
|
+
github.event_name == 'pull_request' &&
|
|
60
|
+
contains(github.event.pull_request.labels.*.name, 'skip-ci')
|
|
61
|
+
run: echo "skip-ci label found, skipping build"
|
|
62
|
+
|
|
63
|
+
- language: yaml
|
|
64
|
+
label: "Use separate jobs per event trigger"
|
|
65
|
+
code: |
|
|
66
|
+
on:
|
|
67
|
+
push:
|
|
68
|
+
branches: [main]
|
|
69
|
+
pull_request:
|
|
70
|
+
|
|
71
|
+
jobs:
|
|
72
|
+
# Only runs on pull_request — safe to use pull_request context
|
|
73
|
+
label-check:
|
|
74
|
+
if: github.event_name == 'pull_request'
|
|
75
|
+
runs-on: ubuntu-latest
|
|
76
|
+
steps:
|
|
77
|
+
- name: Check labels
|
|
78
|
+
if: contains(github.event.pull_request.labels.*.name, 'skip-ci')
|
|
79
|
+
run: echo "Label found"
|
|
80
|
+
|
|
81
|
+
# Runs on both push and pull_request — no pull_request context access
|
|
82
|
+
build:
|
|
83
|
+
runs-on: ubuntu-latest
|
|
84
|
+
steps:
|
|
85
|
+
- uses: actions/checkout@v4
|
|
86
|
+
- run: make build
|
|
87
|
+
prevention:
|
|
88
|
+
- "Before using github.event.X.*.property, verify the event type with github.event_name == 'X' in the same if: expression."
|
|
89
|
+
- "In multi-trigger workflows (on: push, pull_request), never access pull_request context properties without an event type guard."
|
|
90
|
+
- "Use actionlint to catch null context dereferences statically before they fail in CI."
|
|
91
|
+
- "When checking PR labels in workflows triggered by both push and pull_request, consider splitting into separate per-event jobs."
|
|
92
|
+
docs:
|
|
93
|
+
- url: "https://docs.github.com/en/actions/writing-workflows/choosing-what-your-workflow-does/evaluate-expressions-in-workflows-and-actions#object-filters"
|
|
94
|
+
label: "GitHub Docs — Object filters in expressions"
|
|
95
|
+
- url: "https://docs.github.com/en/actions/writing-workflows/choosing-what-your-workflow-does/contexts#github-context"
|
|
96
|
+
label: "GitHub Docs — github context availability by event"
|
|
97
|
+
- url: "https://github.com/rhysd/actionlint"
|
|
98
|
+
label: "actionlint — Static checker for GitHub Actions workflow files"
|
|
99
|
+
- url: "https://stackoverflow.com/questions/67368005"
|
|
100
|
+
label: "SO#67368005 — Object filter left operand must be of type Array or Object but is Null"
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
id: yaml-syntax-062
|
|
2
|
+
title: "if: always() Runs Even on Manual Workflow Cancellation — Use if: !cancelled() for Cleanup Jobs"
|
|
3
|
+
category: yaml-syntax
|
|
4
|
+
severity: silent-failure
|
|
5
|
+
tags:
|
|
6
|
+
- if-condition
|
|
7
|
+
- always
|
|
8
|
+
- cancelled
|
|
9
|
+
- status-functions
|
|
10
|
+
- cleanup-job
|
|
11
|
+
patterns:
|
|
12
|
+
- regex: 'if:\s*always\(\)'
|
|
13
|
+
flags: "i"
|
|
14
|
+
- regex: 'always\(\).*cleanup\|cleanup.*always\(\)'
|
|
15
|
+
flags: "i"
|
|
16
|
+
error_messages:
|
|
17
|
+
- "# No error — cleanup job unexpectedly runs after manual workflow cancellation"
|
|
18
|
+
- "# Job marked 'Cancelled' in UI but cleanup/notify step still executed"
|
|
19
|
+
root_cause: |
|
|
20
|
+
The `always()` status check function evaluates to `true` in ALL job states:
|
|
21
|
+
`success`, `failure`, `cancelled`, and `skipped`. This is intentional and
|
|
22
|
+
documented, but the implication for cleanup/notification jobs surprises many
|
|
23
|
+
developers.
|
|
24
|
+
|
|
25
|
+
When a workflow is manually cancelled (via the GitHub UI, API, or CLI), all
|
|
26
|
+
running and queued jobs receive a `cancelled` status. Jobs with
|
|
27
|
+
`if: always()` will still execute because `always()` explicitly includes the
|
|
28
|
+
`cancelled` state.
|
|
29
|
+
|
|
30
|
+
The common intent is "always run this cleanup step even if the build failed"
|
|
31
|
+
— but the actual effect includes "run even when an operator stops the workflow
|
|
32
|
+
mid-run." This can cause cleanup jobs to run unexpectedly, send spurious
|
|
33
|
+
notifications, or consume runner minutes after an intentional cancellation.
|
|
34
|
+
|
|
35
|
+
Status function reference:
|
|
36
|
+
- `success()` — true only when job status is success (default when no if:)
|
|
37
|
+
- `failure()` — true only when job status is failure
|
|
38
|
+
- `cancelled()` — true only when workflow was manually cancelled
|
|
39
|
+
- `always()` — true for success, failure, cancelled, AND skipped
|
|
40
|
+
- `!cancelled()` — true for success and failure; skips on cancelled/skipped
|
|
41
|
+
|
|
42
|
+
`if: always()` and `if: !cancelled()` are NOT equivalent — the difference
|
|
43
|
+
becomes apparent only when workflows are manually cancelled.
|
|
44
|
+
fix: |
|
|
45
|
+
For cleanup and notification jobs that should respect manual cancellation,
|
|
46
|
+
use `if: !cancelled()` instead of `if: always()`.
|
|
47
|
+
|
|
48
|
+
Reserve `if: always()` for jobs that genuinely must run in every state,
|
|
49
|
+
including intentional cancellations (rare).
|
|
50
|
+
fix_code:
|
|
51
|
+
- language: yaml
|
|
52
|
+
label: "Use !cancelled() for cleanup jobs that should respect manual cancellation"
|
|
53
|
+
code: |
|
|
54
|
+
jobs:
|
|
55
|
+
build:
|
|
56
|
+
runs-on: ubuntu-latest
|
|
57
|
+
steps:
|
|
58
|
+
- run: make build
|
|
59
|
+
|
|
60
|
+
notify:
|
|
61
|
+
runs-on: ubuntu-latest
|
|
62
|
+
needs: build
|
|
63
|
+
# Bad: always() runs even when workflow is manually cancelled
|
|
64
|
+
# if: ${{ always() }}
|
|
65
|
+
|
|
66
|
+
# Good: !cancelled() runs on success and failure, skips on cancellation
|
|
67
|
+
if: ${{ !cancelled() }}
|
|
68
|
+
steps:
|
|
69
|
+
- name: Send Slack notification
|
|
70
|
+
run: echo "Build result: ${{ needs.build.result }}"
|
|
71
|
+
- language: yaml
|
|
72
|
+
label: "Status function comparison"
|
|
73
|
+
code: |
|
|
74
|
+
jobs:
|
|
75
|
+
# Runs only on success (implicit default — no if: needed)
|
|
76
|
+
deploy:
|
|
77
|
+
if: ${{ success() }}
|
|
78
|
+
|
|
79
|
+
# Runs on success or failure — skips on manual cancellation
|
|
80
|
+
notify:
|
|
81
|
+
if: ${{ !cancelled() }}
|
|
82
|
+
|
|
83
|
+
# Runs in every state including manual cancellation (use sparingly)
|
|
84
|
+
audit-log:
|
|
85
|
+
if: ${{ always() }}
|
|
86
|
+
|
|
87
|
+
# Runs only when the workflow was manually cancelled
|
|
88
|
+
on-cancel:
|
|
89
|
+
if: ${{ cancelled() }}
|
|
90
|
+
prevention:
|
|
91
|
+
- "Use if: !cancelled() for cleanup and notification jobs to respect manual workflow cancellation."
|
|
92
|
+
- "Reserve if: always() for jobs that must run even when an operator intentionally stops the workflow."
|
|
93
|
+
- "When combining status functions with needs: checks, use needs.X.result == 'failure' for explicit conditions."
|
|
94
|
+
- "Run actionlint to detect common if: expression issues before pushing."
|
|
95
|
+
docs:
|
|
96
|
+
- url: "https://docs.github.com/en/actions/writing-workflows/choosing-what-your-workflow-does/evaluate-expressions-in-workflows-and-actions#status-check-functions"
|
|
97
|
+
label: "GitHub Docs — Status check functions"
|
|
98
|
+
- url: "https://github.com/rhysd/actionlint/blob/main/docs/checks.md"
|
|
99
|
+
label: "actionlint — Expression checks"
|
|
100
|
+
- url: "https://stackoverflow.com/questions/58457140"
|
|
101
|
+
label: "SO#58457140 — Difference between if: always() and if: !cancelled()"
|
package/package.json
CHANGED