@htekdev/actions-debugger 1.0.95 → 1.0.97

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.
@@ -0,0 +1,90 @@
1
+ id: caching-artifacts-053
2
+ title: "download-artifact@v4 finds no artifacts in workflow_run context without run-id"
3
+ category: caching-artifacts
4
+ severity: error
5
+ tags:
6
+ - download-artifact
7
+ - workflow_run
8
+ - run-id
9
+ - cross-workflow
10
+ - artifact
11
+ patterns:
12
+ - regex: 'No artifacts found'
13
+ flags: 'i'
14
+ - regex: 'Unable to find any artifacts for the associated workflow'
15
+ flags: 'i'
16
+ error_messages:
17
+ - "No artifacts found for the associated workflow run"
18
+ - "Unable to find any artifacts for the associated workflow"
19
+ - "Error: Unable to find any artifacts for the associated workflow run"
20
+ root_cause: |
21
+ `actions/download-artifact@v4` defaults to downloading artifacts from the
22
+ CURRENT workflow run (`github.run_id`). In a `workflow_run`-triggered
23
+ workflow, the current run is the downstream (deploy) workflow — which has
24
+ produced no artifacts. The upstream (build) workflow's artifacts belong to
25
+ the triggering run, identified by `github.event.workflow_run.id`.
26
+
27
+ Without setting `run-id: ${{ github.event.workflow_run.id }}`, the download
28
+ step searches the current run's artifacts, finds nothing, and either fails
29
+ with "No artifacts found" or silently exits (depending on `if-no-files-found`
30
+ setting). No artifact was ever associated with the downstream run, so the
31
+ error can be confusing — the artifact clearly exists in the Actions UI under
32
+ the upstream run.
33
+
34
+ This is a distinct issue from the cross-run permissions error (ca-040):
35
+ that occurs when `run-id` IS set but `actions: read` permission is missing.
36
+ This issue occurs when `run-id` is simply not set at all.
37
+ fix: |
38
+ Set `run-id: ${{ github.event.workflow_run.id }}` on the download step to
39
+ target the triggering workflow's run. Also provide `github-token` (required
40
+ for cross-run downloads) and ensure `actions: read` permission is set.
41
+ fix_code:
42
+ - language: yaml
43
+ label: "Correct: set run-id from triggering workflow in workflow_run context"
44
+ code: |
45
+ on:
46
+ workflow_run:
47
+ workflows: ["CI"]
48
+ types: [completed]
49
+
50
+ permissions:
51
+ actions: read
52
+ contents: read
53
+
54
+ jobs:
55
+ deploy:
56
+ if: github.event.workflow_run.conclusion == 'success'
57
+ runs-on: ubuntu-latest
58
+ steps:
59
+ - name: Download build artifacts
60
+ uses: actions/download-artifact@v4
61
+ with:
62
+ name: build-output
63
+ # Required: point to the upstream run, not the current run
64
+ run-id: ${{ github.event.workflow_run.id }}
65
+ github-token: ${{ secrets.GITHUB_TOKEN }}
66
+
67
+ - name: Deploy
68
+ run: echo "Deploying from artifact"
69
+ - language: yaml
70
+ label: "Incorrect: missing run-id causes 'No artifacts found'"
71
+ code: |
72
+ jobs:
73
+ deploy:
74
+ runs-on: ubuntu-latest
75
+ steps:
76
+ - name: Download build artifacts
77
+ uses: actions/download-artifact@v4
78
+ with:
79
+ name: build-output
80
+ # Missing run-id — searches the CURRENT run, which has no artifacts
81
+ prevention:
82
+ - "In any `workflow_run`-triggered workflow, always set `run-id: ${{ github.event.workflow_run.id }}`"
83
+ - "Pair with `github-token: ${{ secrets.GITHUB_TOKEN }}` and `permissions: actions: read`"
84
+ - "Verify artifacts exist under the triggering run via the Actions UI before debugging download steps"
85
+ - "Name artifacts consistently between upload (in CI) and download (in deploy) workflows"
86
+ docs:
87
+ - url: "https://github.com/actions/download-artifact?tab=readme-ov-file#download-artifacts-from-other-workflow-runs-or-repositories"
88
+ label: "actions/download-artifact: Downloading from other workflow runs"
89
+ - url: "https://docs.github.com/en/actions/writing-workflows/choosing-when-your-workflow-runs/events-that-trigger-workflows#workflow_run"
90
+ label: "GitHub Docs: workflow_run event"
@@ -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,70 @@
1
+ id: concurrency-timing-045
2
+ title: "workflow_run-triggered workflows run concurrently — no upstream concurrency linkage"
3
+ category: concurrency-timing
4
+ severity: silent-failure
5
+ tags:
6
+ - workflow_run
7
+ - concurrency
8
+ - deployment
9
+ - build-deploy-pipeline
10
+ - downstream-workflow
11
+ patterns:
12
+ - regex: 'on:\s+workflow_run:'
13
+ flags: 'si'
14
+ error_messages:
15
+ - "Multiple deployments triggered simultaneously for the same branch"
16
+ - "Deploy workflow triggered concurrently"
17
+ root_cause: |
18
+ Workflows triggered via `on: workflow_run:` do not inherit any concurrency
19
+ group from the triggering (upstream) workflow. If multiple upstream runs
20
+ complete in quick succession — for example, two commits pushed rapidly — each
21
+ `completed` event spawns an independent downstream run. All downstream runs
22
+ execute concurrently with no serialization or cancellation, even when the
23
+ upstream workflow had a concurrency group that serialized upstream runs.
24
+
25
+ The downstream workflow runs in the context of the default branch and receives
26
+ the triggering run's metadata via `github.event.workflow_run.*`, but GitHub
27
+ does not propagate any concurrency scope from upstream to downstream.
28
+
29
+ For build-then-deploy pipelines this creates a race condition: two deploy runs
30
+ may begin simultaneously, with whichever finishes last determining the final
31
+ deployed state regardless of commit order.
32
+ fix: |
33
+ Add an explicit `concurrency:` block to the `workflow_run`-triggered workflow,
34
+ keyed on `github.event.workflow_run.head_branch` to scope per branch.
35
+
36
+ Use `cancel-in-progress: true` for idempotent deploys (only the latest commit
37
+ matters) or `cancel-in-progress: false` for non-idempotent operations that
38
+ must complete once queued.
39
+ fix_code:
40
+ - language: yaml
41
+ label: "Add explicit concurrency group to workflow_run-triggered deploy workflow"
42
+ code: |
43
+ on:
44
+ workflow_run:
45
+ workflows: ["CI"]
46
+ types: [completed]
47
+ branches: [main]
48
+
49
+ # Without this block, concurrent CI completions spawn concurrent deploys
50
+ concurrency:
51
+ group: deploy-${{ github.event.workflow_run.head_branch }}
52
+ cancel-in-progress: true
53
+
54
+ jobs:
55
+ deploy:
56
+ if: github.event.workflow_run.conclusion == 'success'
57
+ runs-on: ubuntu-latest
58
+ steps:
59
+ - name: Deploy
60
+ run: echo "Deploying ${{ github.event.workflow_run.head_sha }}"
61
+ prevention:
62
+ - "Always define an explicit `concurrency:` block in `workflow_run`-triggered workflows"
63
+ - "Key the concurrency group on `github.event.workflow_run.head_branch` to scope by branch"
64
+ - "For CI-to-deploy pipelines, use `cancel-in-progress: true` so only the latest commit deploys"
65
+ - "Consider consolidating CI and deploy into a single workflow using `needs:` if elevated permissions are not required"
66
+ docs:
67
+ - url: "https://docs.github.com/en/actions/writing-workflows/choosing-when-your-workflow-runs/events-that-trigger-workflows#workflow_run"
68
+ label: "GitHub Docs: workflow_run event"
69
+ - url: "https://docs.github.com/en/actions/writing-workflows/choosing-when-your-workflow-runs/controlling-workflow-and-job-execution"
70
+ label: "GitHub Docs: Controlling workflow and job execution (concurrency)"
@@ -0,0 +1,99 @@
1
+ id: permissions-auth-054
2
+ title: "OIDC sub claim format changes when environment: block added — IdP trust policy breaks"
3
+ category: permissions-auth
4
+ severity: error
5
+ tags:
6
+ - oidc
7
+ - environment
8
+ - aws
9
+ - gcp
10
+ - azure
11
+ - sub-claim
12
+ - trust-policy
13
+ - deployment-protection
14
+ patterns:
15
+ - regex: 'Not authorized to perform sts:AssumeRoleWithWebIdentity'
16
+ flags: 'i'
17
+ - regex: 'Error: Credentials could not be loaded'
18
+ flags: 'i'
19
+ - regex: 'Permission.*denied.*generateAccessToken|WorkloadIdentityPool.*rejected'
20
+ flags: 'i'
21
+ error_messages:
22
+ - "Not authorized to perform sts:AssumeRoleWithWebIdentity"
23
+ - "Error: Credentials could not be loaded, please check your action inputs: Could not load credentials from any providers"
24
+ - "Permission 'iam.serviceAccounts.getOpenIdToken' denied on resource"
25
+ - "The provided token could not be validated"
26
+ root_cause: |
27
+ GitHub Actions OIDC token `sub` (subject) claim format depends on whether the
28
+ job has an `environment:` key. Without an environment, the subject is:
29
+ `repo:OWNER/REPO:ref:refs/heads/BRANCH`
30
+
31
+ When `environment: production` is present on the job, the format changes to:
32
+ `repo:OWNER/REPO:environment:production`
33
+
34
+ The branch/ref component is replaced entirely by the environment name. AWS IAM
35
+ role trust policies, GCP Workload Identity Federation conditions, and Azure
36
+ federated credential filters that matched the branch-ref format now receive a
37
+ token with a different sub claim and reject the OIDC exchange with a 403 or
38
+ permission-denied error.
39
+
40
+ This commonly occurs when a team adds environment protection rules (required
41
+ reviewers, wait timers) to an existing workflow that already had OIDC
42
+ credentials working. CI passes before adding `environment:` but fails after.
43
+ fix: |
44
+ Update the IdP trust policy to match the new subject format containing the
45
+ environment name. Options:
46
+ 1. Narrow to environment: change the condition to match
47
+ `repo:OWNER/REPO:environment:production`.
48
+ 2. Use a wildcard: match `repo:OWNER/REPO:*` to accept both formats (less secure).
49
+ 3. GitHub subject claim customization (Enterprise): define a consistent sub
50
+ claim format that does not change based on environment presence.
51
+ fix_code:
52
+ - language: yaml
53
+ label: "Workflow: annotate environment and required OIDC permissions"
54
+ code: |
55
+ permissions:
56
+ id-token: write
57
+ contents: read
58
+
59
+ jobs:
60
+ deploy:
61
+ # Adding this key changes the OIDC sub claim format — update IdP trust policy
62
+ environment: production
63
+ runs-on: ubuntu-latest
64
+ steps:
65
+ - uses: aws-actions/configure-aws-credentials@v4
66
+ with:
67
+ role-to-assume: arn:aws:iam::123456789012:role/DeployRole
68
+ aws-region: us-east-1
69
+ # AWS trust policy must now use:
70
+ # "token.actions.githubusercontent.com:sub":
71
+ # "StringEquals": "repo:org/repo:environment:production"
72
+ # (not "repo:org/repo:ref:refs/heads/main")
73
+ - language: yaml
74
+ label: "AWS IAM trust policy: match environment sub claim"
75
+ code: |
76
+ # AWS IAM Role Trust Policy — update Condition after adding environment:
77
+ # Before (no environment):
78
+ # "token.actions.githubusercontent.com:sub": "repo:org/repo:ref:refs/heads/main"
79
+ #
80
+ # After (with environment: production):
81
+ # "token.actions.githubusercontent.com:sub": "repo:org/repo:environment:production"
82
+ #
83
+ # Wildcard to accept both (less restrictive):
84
+ # "token.actions.githubusercontent.com:sub":
85
+ # StringLike: "repo:org/repo:*"
86
+ #
87
+ # GCP: update attribute.repository_environment or use attribute mapping
88
+ prevention:
89
+ - "Before adding `environment:` to a job using OIDC, audit and update all IdP trust policies"
90
+ - "Document the expected sub claim format in trust policy comments to avoid future confusion"
91
+ - "Use GitHub's OIDC token debugger step to inspect the actual sub claim at runtime"
92
+ - "For consistent sub format across environment and non-environment jobs, consider subject claim customization"
93
+ docs:
94
+ - url: "https://docs.github.com/en/actions/security-for-github-actions/security-hardening-your-deployments/about-security-hardening-with-openid-connect#filtering-for-a-specific-environment"
95
+ label: "GitHub Docs: OIDC filtering for a specific environment"
96
+ - url: "https://docs.github.com/en/actions/security-for-github-actions/security-hardening-your-deployments/about-security-hardening-with-openid-connect#customizing-the-subject-claims-for-an-organization-or-repository"
97
+ label: "GitHub Docs: Customizing subject claims"
98
+ - url: "https://docs.github.com/en/actions/security-for-github-actions/security-hardening-your-deployments/configuring-openid-connect-in-amazon-web-services"
99
+ label: "GitHub Docs: Configuring OIDC in AWS"
@@ -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,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-059
2
+ title: "needs: key does not accept runtime expressions — must be static job IDs"
3
+ category: yaml-syntax
4
+ severity: error
5
+ tags:
6
+ - needs
7
+ - expressions
8
+ - dynamic-jobs
9
+ - job-dependencies
10
+ - parse-time
11
+ - actionlint
12
+ patterns:
13
+ - regex: "The pipeline is not valid.*needs|needs.*references.*job.*does not exist"
14
+ flags: 'i'
15
+ - regex: 'Unrecognized named-value.*steps.*in needs|needs.*invalid.*expression'
16
+ flags: 'i'
17
+ error_messages:
18
+ - "The pipeline is not valid. Job 'deploy' needs 'steps' which does not exist"
19
+ - "Unrecognized named-value: 'steps'"
20
+ - "Job 'X' depends on unknown job '${{ fromJSON(...) }}'"
21
+ - "needs: field value '${{ ... }}' is not a valid job identifier"
22
+ root_cause: |
23
+ The `needs:` key in a GitHub Actions job definition must contain static job ID
24
+ strings. It is evaluated at workflow parse time — before any step or job runs
25
+ — so runtime expressions such as `${{ steps.gen.outputs.jobs }}` or
26
+ `${{ fromJSON(env.DYNAMIC_JOBS) }}` are not evaluated. They are treated as
27
+ literal strings, and since no job with that literal name exists, GitHub reports
28
+ a validation error or the dependency is silently ignored.
29
+
30
+ This surprises developers who successfully use `fromJSON()` in `matrix:` or
31
+ `env:` blocks (which ARE expression-capable) and assume `needs:` works the
32
+ same way. The difference is that the job dependency graph must be fully
33
+ resolved before execution begins; expressions in `needs:` would create a
34
+ circular dependency at parse time.
35
+
36
+ actionlint statically detects this and reports: "needs: field cannot be
37
+ computed by a dynamic value. Use a literal value instead."
38
+ fix: |
39
+ Job dependencies must be statically defined. Use one of these patterns:
40
+ 1. List all potential dependent jobs statically; use `if:` conditions on
41
+ each downstream job to skip those not needed.
42
+ 2. Use a matrix fan-out + fan-in pattern where the fan-in job has a single
43
+ static `needs:` on the matrix job name (not individual matrix legs).
44
+ 3. Restructure so the dependency graph is known at workflow authoring time.
45
+ fix_code:
46
+ - language: yaml
47
+ label: "Correct: static needs: with if: conditions to control execution"
48
+ code: |
49
+ jobs:
50
+ build:
51
+ runs-on: ubuntu-latest
52
+ outputs:
53
+ should_deploy: ${{ steps.check.outputs.deploy }}
54
+ steps:
55
+ - id: check
56
+ run: echo "deploy=true" >> $GITHUB_OUTPUT
57
+
58
+ deploy:
59
+ # Static needs: always declared; if: controls whether it runs
60
+ needs: [build]
61
+ if: needs.build.outputs.should_deploy == 'true'
62
+ runs-on: ubuntu-latest
63
+ steps:
64
+ - run: echo "deploying"
65
+ - language: yaml
66
+ label: "Incorrect: expression in needs: causes parse-time validation error"
67
+ code: |
68
+ jobs:
69
+ generate:
70
+ runs-on: ubuntu-latest
71
+ outputs:
72
+ jobs: ${{ steps.list.outputs.jobs }}
73
+ steps:
74
+ - id: list
75
+ run: echo 'jobs=["build","lint"]' >> $GITHUB_OUTPUT
76
+
77
+ # ERROR: needs: does not evaluate expressions — this is a parse-time failure
78
+ deploy:
79
+ needs: ${{ fromJSON(needs.generate.outputs.jobs) }}
80
+ runs-on: ubuntu-latest
81
+ steps:
82
+ - run: echo "this never runs"
83
+ prevention:
84
+ - "Always use static job ID string literals in `needs:` — no expressions, no fromJSON()"
85
+ - "Use actionlint to pre-validate workflows before pushing: it catches invalid expression contexts"
86
+ - "For dynamic fan-in, depend on the matrix job name itself, not individual matrix leg names"
87
+ - "Use `if: contains(needs.*.result, 'failure')` for conditional logic after static needs"
88
+ docs:
89
+ - url: "https://docs.github.com/en/actions/writing-workflows/workflow-syntax-for-github-actions#jobsjob_idneeds"
90
+ label: "GitHub Docs: jobs.<job_id>.needs"
91
+ - url: "https://rhysd.github.io/actionlint/"
92
+ label: "actionlint: Static checker for GitHub Actions workflow files"
93
+
@@ -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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@htekdev/actions-debugger",
3
- "version": "1.0.95",
3
+ "version": "1.0.97",
4
4
  "description": "65+ real GitHub Actions errors, queryable by agents. CLI + MCP server + Copilot skills + error database.",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",