@htekdev/actions-debugger 1.0.68 → 1.0.70
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/known-unsolved/composite-action-no-pre-post-lifecycle-hooks.yml +94 -0
- package/errors/permissions-auth/create-github-app-token-expires-one-hour.yml +124 -0
- package/errors/runner-environment/docker-build-push-provenance-default-manifest-index.yml +81 -0
- package/errors/runner-environment/setup-node-yarn-packagemanager-corepack-conflict.yml +71 -0
- package/errors/silent-failures/steps-conclusion-always-success-with-continue-on-error.yml +90 -0
- package/errors/triggers/release-published-fires-on-prerelease-creation-and-promotion.yml +106 -0
- package/errors/triggers/schedule-workflow-disabled-on-fork.yml +69 -0
- package/errors/yaml-syntax/paths-and-paths-ignore-combined-on-same-event.yml +97 -0
- package/package.json +1 -1
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
id: known-unsolved-043
|
|
2
|
+
title: 'Composite actions do not support pre: and post: lifecycle hooks — no guaranteed setup or cleanup code'
|
|
3
|
+
category: known-unsolved
|
|
4
|
+
severity: limitation
|
|
5
|
+
tags:
|
|
6
|
+
- composite-action
|
|
7
|
+
- pre-step
|
|
8
|
+
- post-step
|
|
9
|
+
- cleanup
|
|
10
|
+
- lifecycle
|
|
11
|
+
patterns:
|
|
12
|
+
- regex: 'Unexpected value ''pre'''
|
|
13
|
+
flags: 'i'
|
|
14
|
+
- regex: 'Unexpected value ''post'''
|
|
15
|
+
flags: 'i'
|
|
16
|
+
error_messages:
|
|
17
|
+
- "Unexpected value 'pre'"
|
|
18
|
+
- "Unexpected value 'post'"
|
|
19
|
+
- 'pre: and post: are only supported in JavaScript and Docker container actions'
|
|
20
|
+
root_cause: |
|
|
21
|
+
GitHub Actions supports pre: and post: lifecycle hooks in JavaScript actions (using: node20/node24)
|
|
22
|
+
and Docker container actions (using: docker). These hooks run before and after the main action
|
|
23
|
+
step respectively, and the post: hook runs even when the workflow is cancelled or fails —
|
|
24
|
+
making them ideal for guaranteed resource cleanup.
|
|
25
|
+
|
|
26
|
+
Composite actions (using: composite) do NOT support pre: or post: hooks. Attempting to add
|
|
27
|
+
pre: or post: to an action.yml with using: composite causes a validation error. This means
|
|
28
|
+
shell-based or Python-based composite actions cannot register guaranteed cleanup code that
|
|
29
|
+
runs after the job, regardless of success, failure, or cancellation.
|
|
30
|
+
|
|
31
|
+
Common affected patterns:
|
|
32
|
+
- Database seeding in setup steps that must be torn down in post:
|
|
33
|
+
- Port forwarding or tunnel setup that must be closed after the job
|
|
34
|
+
- Temporary credential setup that must be revoked after the workflow
|
|
35
|
+
- Cloud resource creation that must be deleted to avoid cost leaks
|
|
36
|
+
fix: |
|
|
37
|
+
There is no direct fix — this is a known GitHub Actions platform limitation. Common workarounds:
|
|
38
|
+
|
|
39
|
+
Option A: Add explicit cleanup steps in calling workflows using if: always().
|
|
40
|
+
Option B: Wrap the composite logic in a thin JavaScript action that shells out to scripts,
|
|
41
|
+
enabling pre:/post: hook support.
|
|
42
|
+
Option C: Use a separate cleanup job with needs: [main-job] and if: always() for teardown.
|
|
43
|
+
Note: Option C adds latency and does not run on runner process kill.
|
|
44
|
+
fix_code:
|
|
45
|
+
- language: yaml
|
|
46
|
+
label: 'Workaround: use if: always() cleanup step in the calling workflow'
|
|
47
|
+
code: |
|
|
48
|
+
jobs:
|
|
49
|
+
build:
|
|
50
|
+
runs-on: ubuntu-latest
|
|
51
|
+
steps:
|
|
52
|
+
- name: Setup composite action
|
|
53
|
+
uses: ./.github/actions/my-composite-action
|
|
54
|
+
|
|
55
|
+
- name: Run build
|
|
56
|
+
run: make build
|
|
57
|
+
|
|
58
|
+
# Cleanup runs even if previous steps fail or are cancelled
|
|
59
|
+
- name: Cleanup resources
|
|
60
|
+
if: always()
|
|
61
|
+
run: |
|
|
62
|
+
echo "Cleaning up resources created by composite action..."
|
|
63
|
+
# teardown commands here
|
|
64
|
+
|
|
65
|
+
- language: yaml
|
|
66
|
+
label: 'Workaround: separate cleanup job guaranteed to run after main job'
|
|
67
|
+
code: |
|
|
68
|
+
jobs:
|
|
69
|
+
build:
|
|
70
|
+
runs-on: ubuntu-latest
|
|
71
|
+
steps:
|
|
72
|
+
- uses: ./.github/actions/my-composite-action
|
|
73
|
+
- run: make build
|
|
74
|
+
|
|
75
|
+
cleanup:
|
|
76
|
+
runs-on: ubuntu-latest
|
|
77
|
+
needs: [build]
|
|
78
|
+
# Runs even when the build job fails or is cancelled
|
|
79
|
+
if: always()
|
|
80
|
+
steps:
|
|
81
|
+
- name: Teardown
|
|
82
|
+
run: |
|
|
83
|
+
echo "Running post-job cleanup..."
|
|
84
|
+
prevention:
|
|
85
|
+
- "Design composite actions to be stateless and idempotent when possible — avoid side effects that require guaranteed cleanup"
|
|
86
|
+
- "Document in the composite action README that callers must add if: always() cleanup steps"
|
|
87
|
+
- "For actions that require guaranteed pre:/post: lifecycle hooks, consider wrapping in a JavaScript action"
|
|
88
|
+
docs:
|
|
89
|
+
- url: 'https://github.com/actions/runner/issues/1478'
|
|
90
|
+
label: 'actions/runner#1478 — Support pre and post steps in Composite Actions (393 reactions)'
|
|
91
|
+
- url: 'https://docs.github.com/en/actions/creating-actions/metadata-syntax-for-github-actions#runs-for-composite-actions'
|
|
92
|
+
label: 'Metadata syntax for composite actions — GitHub Docs'
|
|
93
|
+
- url: 'https://docs.github.com/en/actions/creating-actions/metadata-syntax-for-github-actions#runspre'
|
|
94
|
+
label: 'pre: lifecycle hook — JavaScript and Docker actions only'
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
id: permissions-auth-046
|
|
2
|
+
title: 'actions/create-github-app-token output token expires after 1 hour — long-running jobs hit 401 Unauthorized'
|
|
3
|
+
category: permissions-auth
|
|
4
|
+
severity: error
|
|
5
|
+
tags:
|
|
6
|
+
- github-app
|
|
7
|
+
- installation-token
|
|
8
|
+
- token-expiry
|
|
9
|
+
- create-github-app-token
|
|
10
|
+
- long-running-jobs
|
|
11
|
+
patterns:
|
|
12
|
+
- regex: '401 Unauthorized'
|
|
13
|
+
flags: 'i'
|
|
14
|
+
- regex: 'Bad credentials'
|
|
15
|
+
flags: 'i'
|
|
16
|
+
- regex: 'installation.*token.*expired'
|
|
17
|
+
flags: 'i'
|
|
18
|
+
- regex: 'HttpError: Bad credentials'
|
|
19
|
+
flags: 'i'
|
|
20
|
+
error_messages:
|
|
21
|
+
- '401 Unauthorized'
|
|
22
|
+
- 'Bad credentials'
|
|
23
|
+
- 'HttpError: Bad credentials'
|
|
24
|
+
- 'Installation tokens expire one hour from the time you create them'
|
|
25
|
+
root_cause: |
|
|
26
|
+
GitHub App installation tokens — generated by actions/create-github-app-token — have a fixed
|
|
27
|
+
1-hour expiry enforced by the GitHub platform. The token is generated once at the step where
|
|
28
|
+
the action runs, stored as a step output, and typically exported to an env var. If subsequent
|
|
29
|
+
steps take longer than 60 minutes to reach the point where the token is used, all GitHub API
|
|
30
|
+
calls and authenticated git operations will return 401 Unauthorized or "Bad credentials".
|
|
31
|
+
|
|
32
|
+
From GitHub's REST API docs:
|
|
33
|
+
"Installation tokens expire one hour from the time you create them. Using an expired token
|
|
34
|
+
produces a status code of 401 - Unauthorized, and requires creating a new installation token."
|
|
35
|
+
|
|
36
|
+
Common affected patterns:
|
|
37
|
+
- Long test suites (>60 min) where a GitHub App token is generated upfront for a final
|
|
38
|
+
PR comment or status check post at the end.
|
|
39
|
+
- Build pipelines that generate an app token for private package registry authentication
|
|
40
|
+
at the start, then run a multi-hour compilation and upload step at the end.
|
|
41
|
+
- Self-hosted runners on slow networks where upload + deploy sequences exceed 60 minutes.
|
|
42
|
+
- Jobs that poll or sleep mid-run (e.g., waiting for an external deployment to stabilize).
|
|
43
|
+
|
|
44
|
+
Important: The built-in GITHUB_TOKEN is NOT subject to this expiry — it remains valid for
|
|
45
|
+
the entire job duration (up to the job's timeout-minutes). Only tokens from
|
|
46
|
+
actions/create-github-app-token or direct GitHub App JWT exchanges expire after 1 hour.
|
|
47
|
+
fix: |
|
|
48
|
+
Generate a fresh GitHub App token immediately before the step that needs it rather than
|
|
49
|
+
once at the beginning of the job. For jobs that have multiple distinct phases requiring
|
|
50
|
+
the token, generate a separate token for each phase. Alternatively, split long-running
|
|
51
|
+
work into shorter jobs — each job generates its own fresh token on startup.
|
|
52
|
+
fix_code:
|
|
53
|
+
- language: yaml
|
|
54
|
+
label: 'Generate token immediately before the step that needs it (not at job start)'
|
|
55
|
+
code: |
|
|
56
|
+
jobs:
|
|
57
|
+
long-build:
|
|
58
|
+
runs-on: ubuntu-latest
|
|
59
|
+
steps:
|
|
60
|
+
- name: Checkout
|
|
61
|
+
uses: actions/checkout@v4
|
|
62
|
+
|
|
63
|
+
- name: Run long test suite
|
|
64
|
+
# This may run for over an hour
|
|
65
|
+
run: make test
|
|
66
|
+
|
|
67
|
+
# Generate a fresh token just before the API call — never stale
|
|
68
|
+
- name: Generate GitHub App token
|
|
69
|
+
id: app-token
|
|
70
|
+
uses: actions/create-github-app-token@v2
|
|
71
|
+
with:
|
|
72
|
+
app-id: ${{ vars.APP_ID }}
|
|
73
|
+
private-key: ${{ secrets.APP_PRIVATE_KEY }}
|
|
74
|
+
|
|
75
|
+
- name: Post PR comment
|
|
76
|
+
env:
|
|
77
|
+
GH_TOKEN: ${{ steps.app-token.outputs.token }}
|
|
78
|
+
run: |
|
|
79
|
+
gh pr comment ${{ github.event.pull_request.number }} \
|
|
80
|
+
--body "Tests completed successfully"
|
|
81
|
+
|
|
82
|
+
- language: yaml
|
|
83
|
+
label: 'Split into sub-jobs so each job generates its own short-lived token'
|
|
84
|
+
code: |
|
|
85
|
+
jobs:
|
|
86
|
+
long-tests:
|
|
87
|
+
runs-on: ubuntu-latest
|
|
88
|
+
steps:
|
|
89
|
+
- uses: actions/checkout@v4
|
|
90
|
+
- run: make test
|
|
91
|
+
|
|
92
|
+
notify:
|
|
93
|
+
runs-on: ubuntu-latest
|
|
94
|
+
needs: [long-tests]
|
|
95
|
+
steps:
|
|
96
|
+
# Each job generates a token on startup — always fresh
|
|
97
|
+
- name: Generate GitHub App token
|
|
98
|
+
id: app-token
|
|
99
|
+
uses: actions/create-github-app-token@v2
|
|
100
|
+
with:
|
|
101
|
+
app-id: ${{ vars.APP_ID }}
|
|
102
|
+
private-key: ${{ secrets.APP_PRIVATE_KEY }}
|
|
103
|
+
|
|
104
|
+
- name: Post PR comment
|
|
105
|
+
env:
|
|
106
|
+
GH_TOKEN: ${{ steps.app-token.outputs.token }}
|
|
107
|
+
run: |
|
|
108
|
+
gh pr comment ${{ github.event.pull_request.number }} \
|
|
109
|
+
--body "Tests completed successfully"
|
|
110
|
+
prevention:
|
|
111
|
+
- "Never store a GitHub App installation token at job start assuming it stays valid — generate it immediately before use"
|
|
112
|
+
- "For jobs expected to run longer than 45 minutes, split into sub-jobs so each generates its own fresh token"
|
|
113
|
+
- "Prefer the built-in GITHUB_TOKEN for operations within the same repository — it is valid for the entire job duration"
|
|
114
|
+
- "Set job-level timeout-minutes to catch unexpectedly long jobs and keep total runtime well under the 1-hour token expiry"
|
|
115
|
+
- "The actions/create-github-app-token README prominently warns: 'An installation access token expires after 1 hour'"
|
|
116
|
+
docs:
|
|
117
|
+
- url: 'https://docs.github.com/en/apps/creating-github-apps/authenticating-with-a-github-app/generating-an-installation-access-token-for-a-github-app'
|
|
118
|
+
label: 'Generating an installation access token — expires after 1 hour (GitHub Docs)'
|
|
119
|
+
- url: 'https://github.com/actions/create-github-app-token'
|
|
120
|
+
label: 'actions/create-github-app-token — README warning about 1-hour expiry'
|
|
121
|
+
- url: 'https://github.com/actions/create-github-app-token/issues/128'
|
|
122
|
+
label: 'actions/create-github-app-token#128 — token only lasts 1 hour, long-running jobs fail'
|
|
123
|
+
- url: 'https://docs.github.com/en/actions/security-for-github-actions/security-guides/automatic-token-authentication'
|
|
124
|
+
label: 'GITHUB_TOKEN automatic token authentication — valid for entire job duration'
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
id: runner-environment-130
|
|
2
|
+
title: 'docker/build-push-action default provenance: true pushes OCI image index for single-platform builds — breaks registries that do not support image indexes'
|
|
3
|
+
category: runner-environment
|
|
4
|
+
severity: silent-failure
|
|
5
|
+
tags:
|
|
6
|
+
- docker
|
|
7
|
+
- build-push-action
|
|
8
|
+
- provenance
|
|
9
|
+
- oci
|
|
10
|
+
- manifest-index
|
|
11
|
+
patterns:
|
|
12
|
+
- regex: 'manifest unknown'
|
|
13
|
+
flags: 'i'
|
|
14
|
+
- regex: 'unexpected status: \d{3}'
|
|
15
|
+
flags: 'i'
|
|
16
|
+
error_messages:
|
|
17
|
+
- 'manifest unknown'
|
|
18
|
+
- 'unexpected status: 400'
|
|
19
|
+
- 'unexpected status: 403'
|
|
20
|
+
root_cause: |
|
|
21
|
+
Starting with docker/build-push-action v4, the provenance input defaults to mode=min,
|
|
22
|
+
which attaches an SLSA provenance attestation to every pushed image. This causes the action
|
|
23
|
+
to push an OCI image index (manifest list) to the registry instead of a plain image manifest,
|
|
24
|
+
even for single-platform builds.
|
|
25
|
+
|
|
26
|
+
Registries and tools that do not support OCI image indexes may return "manifest unknown"
|
|
27
|
+
errors, return 400/403 HTTP responses, or silently resolve to an unexpected image variant.
|
|
28
|
+
Some container registries (such as older versions of GCR) display the provenance attestation
|
|
29
|
+
as a separate image entry with a creation timestamp of 0 epoch (Unix epoch), causing
|
|
30
|
+
confusion in image management dashboards.
|
|
31
|
+
|
|
32
|
+
The behavior is silent in successful-looking cases: the push completes without error, but
|
|
33
|
+
downstream 'docker pull' or 'FROM image:tag' in a Dockerfile may resolve the image index
|
|
34
|
+
instead of a plain image manifest, causing unexpected platform or size differences.
|
|
35
|
+
fix: |
|
|
36
|
+
Set provenance: false in docker/build-push-action to restore plain image manifest behavior
|
|
37
|
+
for single-platform builds. If SLSA provenance is required, use actions/attest-build-provenance
|
|
38
|
+
as a separate step after the push.
|
|
39
|
+
fix_code:
|
|
40
|
+
- language: yaml
|
|
41
|
+
label: 'Disable provenance to push plain image manifest for single-platform builds'
|
|
42
|
+
code: |
|
|
43
|
+
- name: Build and push
|
|
44
|
+
uses: docker/build-push-action@v6
|
|
45
|
+
with:
|
|
46
|
+
context: .
|
|
47
|
+
push: true
|
|
48
|
+
tags: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:latest
|
|
49
|
+
# Disable default provenance attestation to push a plain image manifest.
|
|
50
|
+
# Required for registries that do not support OCI image indexes.
|
|
51
|
+
provenance: false
|
|
52
|
+
|
|
53
|
+
- language: yaml
|
|
54
|
+
label: 'Add SLSA provenance separately using actions/attest-build-provenance'
|
|
55
|
+
code: |
|
|
56
|
+
- name: Build and push
|
|
57
|
+
id: push
|
|
58
|
+
uses: docker/build-push-action@v6
|
|
59
|
+
with:
|
|
60
|
+
context: .
|
|
61
|
+
push: true
|
|
62
|
+
tags: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:latest
|
|
63
|
+
provenance: false
|
|
64
|
+
|
|
65
|
+
- name: Attest build provenance
|
|
66
|
+
uses: actions/attest-build-provenance@v2
|
|
67
|
+
with:
|
|
68
|
+
subject-name: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
|
|
69
|
+
subject-digest: ${{ steps.push.outputs.digest }}
|
|
70
|
+
push-to-registry: true
|
|
71
|
+
prevention:
|
|
72
|
+
- "Set 'provenance: false' in docker/build-push-action unless your target registry explicitly supports OCI image indexes"
|
|
73
|
+
- "Test image pulling from the registry after pushing to confirm the correct manifest type was stored"
|
|
74
|
+
- "Use 'docker manifest inspect' to verify the pushed image is a plain manifest and not a manifest index when provenance is enabled"
|
|
75
|
+
docs:
|
|
76
|
+
- url: 'https://github.com/docker/build-push-action/issues/755'
|
|
77
|
+
label: 'docker/build-push-action#755 — Action started to push manifest indexes instead of images for a single platform'
|
|
78
|
+
- url: 'https://docs.docker.com/build/ci/github-actions/attestations/'
|
|
79
|
+
label: 'Docker build attestations with GitHub Actions'
|
|
80
|
+
- url: 'https://docs.docker.com/reference/cli/docker/buildx/build/#provenance'
|
|
81
|
+
label: 'docker buildx build --provenance flag documentation'
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
id: runner-environment-129
|
|
2
|
+
title: 'setup-node cache: yarn fails when packageManager specifies Yarn 4 in package.json — Corepack version mismatch'
|
|
3
|
+
category: runner-environment
|
|
4
|
+
severity: error
|
|
5
|
+
tags:
|
|
6
|
+
- setup-node
|
|
7
|
+
- yarn
|
|
8
|
+
- corepack
|
|
9
|
+
- packageManager
|
|
10
|
+
- package-json
|
|
11
|
+
patterns:
|
|
12
|
+
- regex: 'Presence of the "packageManager" field indicates that the project is meant to be used with Corepack'
|
|
13
|
+
flags: 'i'
|
|
14
|
+
- regex: 'the current global version of Yarn is \d+\.\d+'
|
|
15
|
+
flags: 'i'
|
|
16
|
+
error_messages:
|
|
17
|
+
- 'This project''s package.json defines "packageManager": "yarn@4.x.x". However the current global version of Yarn is 1.22.22.'
|
|
18
|
+
- 'Presence of the "packageManager" field indicates that the project is meant to be used with Corepack'
|
|
19
|
+
root_cause: |
|
|
20
|
+
When package.json contains a "packageManager" field specifying Yarn 4 (e.g., "yarn@4.1.1"),
|
|
21
|
+
Yarn's integrity check detects the field and requires the project to be managed via Corepack.
|
|
22
|
+
The system Yarn installed on GitHub-hosted runners is Yarn Classic (1.x), which responds to
|
|
23
|
+
the "packageManager" constraint by throwing an error if Corepack is not enabled.
|
|
24
|
+
|
|
25
|
+
When actions/setup-node is configured with cache: 'yarn', it detects the "packageManager"
|
|
26
|
+
field, attempts to use the specified Yarn version via Corepack, but Corepack is not enabled
|
|
27
|
+
by default on GitHub-hosted runners. The resulting error prevents setup-node from completing
|
|
28
|
+
and fails the job before any build steps run.
|
|
29
|
+
|
|
30
|
+
This affects workflows being upgraded from Yarn Classic to Yarn 4 (Berry) via the
|
|
31
|
+
"packageManager" field in package.json, or projects that adopted the "packageManager"
|
|
32
|
+
field for strict version pinning.
|
|
33
|
+
fix: |
|
|
34
|
+
Enable Corepack before running setup-node. Two options depending on your setup-node version:
|
|
35
|
+
|
|
36
|
+
Option A: Add a 'corepack enable' run step before setup-node (works with all versions).
|
|
37
|
+
Option B: Use 'enable-node-corepack: true' input (requires setup-node v4.2+).
|
|
38
|
+
Option C: Remove 'packageManager' from package.json and manage Yarn version via .yarnrc.yml only.
|
|
39
|
+
fix_code:
|
|
40
|
+
- language: yaml
|
|
41
|
+
label: 'Enable Corepack before setup-node (Yarn 4 / Berry projects)'
|
|
42
|
+
code: |
|
|
43
|
+
- name: Enable Corepack
|
|
44
|
+
run: corepack enable
|
|
45
|
+
|
|
46
|
+
- name: Set up Node.js with Yarn cache
|
|
47
|
+
uses: actions/setup-node@v4
|
|
48
|
+
with:
|
|
49
|
+
node-version: '20'
|
|
50
|
+
cache: 'yarn'
|
|
51
|
+
|
|
52
|
+
- language: yaml
|
|
53
|
+
label: 'Alternative: enable-node-corepack input (setup-node v4.2+)'
|
|
54
|
+
code: |
|
|
55
|
+
- name: Set up Node.js
|
|
56
|
+
uses: actions/setup-node@v4
|
|
57
|
+
with:
|
|
58
|
+
node-version: '20'
|
|
59
|
+
cache: 'yarn'
|
|
60
|
+
enable-node-corepack: true
|
|
61
|
+
prevention:
|
|
62
|
+
- "Always run 'corepack enable' before setup-node when package.json has a 'packageManager' field specifying Yarn 4+"
|
|
63
|
+
- "Verify Corepack is enabled before relying on setup-node's yarn cache integration in Yarn Berry projects"
|
|
64
|
+
- "Pin the Yarn version in .yarnrc.yml in addition to the packageManager field to ensure consistent resolution"
|
|
65
|
+
docs:
|
|
66
|
+
- url: 'https://github.com/actions/setup-node/issues/1027'
|
|
67
|
+
label: 'actions/setup-node#1027 — If cache: yarn is specified, this action fails (38 reactions)'
|
|
68
|
+
- url: 'https://github.com/actions/setup-node/blob/main/docs/advanced-usage.md#corepack'
|
|
69
|
+
label: 'setup-node advanced usage — Corepack support'
|
|
70
|
+
- url: 'https://yarnpkg.com/getting-started/install'
|
|
71
|
+
label: 'Yarn Berry — Installation via Corepack'
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
id: silent-failures-068
|
|
2
|
+
title: 'steps.<id>.conclusion is always success when continue-on-error: true — using conclusion to detect step failure never triggers'
|
|
3
|
+
category: silent-failures
|
|
4
|
+
severity: silent-failure
|
|
5
|
+
tags:
|
|
6
|
+
- continue-on-error
|
|
7
|
+
- steps-context
|
|
8
|
+
- outcome
|
|
9
|
+
- conclusion
|
|
10
|
+
- if-condition
|
|
11
|
+
- step-failure
|
|
12
|
+
patterns:
|
|
13
|
+
- regex: 'steps\.\w+\.conclusion\s*==\s*[''"]failure[''"]'
|
|
14
|
+
flags: 'i'
|
|
15
|
+
- regex: 'continue-on-error.*true'
|
|
16
|
+
flags: 'i'
|
|
17
|
+
error_messages:
|
|
18
|
+
- 'steps.<id>.conclusion == ''failure'' never evaluates to true'
|
|
19
|
+
- 'cleanup step silently skipped despite step failure'
|
|
20
|
+
root_cause: |
|
|
21
|
+
GitHub Actions provides two properties on the `steps` context for inspecting a completed step:
|
|
22
|
+
|
|
23
|
+
- `steps.<id>.outcome` — the raw result BEFORE `continue-on-error` is applied. Values:
|
|
24
|
+
`success`, `failure`, `cancelled`, `skipped`.
|
|
25
|
+
- `steps.<id>.conclusion` — the final result AFTER `continue-on-error` is applied. When
|
|
26
|
+
`continue-on-error: true` and the step fails, `conclusion` is `success` (not `failure`).
|
|
27
|
+
|
|
28
|
+
When a step has `continue-on-error: true` set, a step failure does NOT propagate to
|
|
29
|
+
`conclusion`. Developers who write:
|
|
30
|
+
|
|
31
|
+
if: steps.my-step.conclusion == 'failure'
|
|
32
|
+
|
|
33
|
+
expecting to detect when `my-step` failed will find that this condition is NEVER true — even
|
|
34
|
+
when the step actually failed — because `continue-on-error` overwrites the conclusion to
|
|
35
|
+
`success`. The downstream step silently never runs.
|
|
36
|
+
|
|
37
|
+
This commonly affects:
|
|
38
|
+
- Cleanup steps that should run when a flaky test step fails (continue-on-error: true)
|
|
39
|
+
- Notification steps that should alert on specific step failures in a pipeline
|
|
40
|
+
- Conditional logic that differentiates between "step was skipped" and "step failed"
|
|
41
|
+
fix: |
|
|
42
|
+
Use `steps.<id>.outcome` (not `conclusion`) to detect the raw result of a step that has
|
|
43
|
+
`continue-on-error: true`. The `outcome` property reflects the actual step result before
|
|
44
|
+
the error suppression is applied and correctly returns `failure` even when the final
|
|
45
|
+
conclusion is `success`.
|
|
46
|
+
fix_code:
|
|
47
|
+
- language: yaml
|
|
48
|
+
label: 'WRONG: conclusion is always success when continue-on-error is true — cleanup never runs'
|
|
49
|
+
code: |
|
|
50
|
+
steps:
|
|
51
|
+
- name: Run flaky tests
|
|
52
|
+
id: tests
|
|
53
|
+
continue-on-error: true
|
|
54
|
+
run: pytest tests/
|
|
55
|
+
|
|
56
|
+
- name: Send failure notification
|
|
57
|
+
# NEVER RUNS — conclusion is 'success' even when tests fail
|
|
58
|
+
if: steps.tests.conclusion == 'failure'
|
|
59
|
+
run: echo "Tests failed — notifying team"
|
|
60
|
+
|
|
61
|
+
- language: yaml
|
|
62
|
+
label: 'CORRECT: use outcome to detect the raw step result before continue-on-error'
|
|
63
|
+
code: |
|
|
64
|
+
steps:
|
|
65
|
+
- name: Run flaky tests
|
|
66
|
+
id: tests
|
|
67
|
+
continue-on-error: true
|
|
68
|
+
run: pytest tests/
|
|
69
|
+
|
|
70
|
+
- name: Send failure notification
|
|
71
|
+
# CORRECT — outcome reflects the raw result before continue-on-error override
|
|
72
|
+
if: steps.tests.outcome == 'failure'
|
|
73
|
+
run: echo "Tests failed — notifying team"
|
|
74
|
+
|
|
75
|
+
- name: Always-run cleanup
|
|
76
|
+
# For cleanup that must run regardless, use always() + outcome check
|
|
77
|
+
if: always() && steps.tests.outcome != 'success'
|
|
78
|
+
run: echo "Running cleanup after non-success outcome"
|
|
79
|
+
prevention:
|
|
80
|
+
- "When a step has continue-on-error: true, always use steps.<id>.outcome (not conclusion) to detect failures in downstream if conditions"
|
|
81
|
+
- "steps.<id>.conclusion == 'success' is always true when continue-on-error: true — it is safe for 'did this step not skip?' checks only"
|
|
82
|
+
- "The failure() status function is also bypassed by continue-on-error — use steps.<id>.outcome != 'success' instead"
|
|
83
|
+
- "Review GitHub Docs context reference table to understand outcome vs conclusion semantics before writing conditional cleanup steps"
|
|
84
|
+
docs:
|
|
85
|
+
- url: 'https://docs.github.com/en/actions/writing-workflows/choosing-what-your-workflow-does/accessing-contextual-information-about-workflow-runs#steps-context'
|
|
86
|
+
label: 'steps context — outcome vs conclusion properties with continue-on-error (GitHub Docs)'
|
|
87
|
+
- url: 'https://github.com/actions/runner/blob/main/docs/adrs/0274-step-outcome-and-conclusion.md'
|
|
88
|
+
label: 'actions/runner ADR: outcome vs conclusion design — why two separate properties exist'
|
|
89
|
+
- url: 'https://stackoverflow.com/questions/71433926/how-do-to-run-the-next-github-action-step-even-if-the-previous-step-failed-whil'
|
|
90
|
+
label: 'Stack Overflow: using steps.outcome vs steps.conclusion with continue-on-error'
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
id: triggers-049
|
|
2
|
+
title: 'on.release types: [published] fires for pre-release creation AND pre-release-to-full promotion — deploy workflows run twice'
|
|
3
|
+
category: triggers
|
|
4
|
+
severity: silent-failure
|
|
5
|
+
tags:
|
|
6
|
+
- release
|
|
7
|
+
- published
|
|
8
|
+
- prerelease
|
|
9
|
+
- on-release
|
|
10
|
+
- double-trigger
|
|
11
|
+
- deploy
|
|
12
|
+
patterns:
|
|
13
|
+
- regex: 'github\.event\.action.*prereleased'
|
|
14
|
+
flags: 'i'
|
|
15
|
+
- regex: 'github\.event\.release\.prerelease.*true'
|
|
16
|
+
flags: 'i'
|
|
17
|
+
error_messages:
|
|
18
|
+
- 'github.event.action: prereleased'
|
|
19
|
+
- 'github.event.action: released'
|
|
20
|
+
- 'github.event.release.prerelease: true'
|
|
21
|
+
root_cause: |
|
|
22
|
+
The GitHub `release` event supports multiple activity types: `created`, `published`,
|
|
23
|
+
`prereleased`, `released`, `edited`, `deleted`, and `unpublished`. The `published` type
|
|
24
|
+
is frequently misunderstood:
|
|
25
|
+
|
|
26
|
+
- `published` fires whenever ANY release is made public — this includes both pre-releases
|
|
27
|
+
(action=prereleased) and full releases (action=released). It is a superset, not a
|
|
28
|
+
specific activity type for "final" releases.
|
|
29
|
+
|
|
30
|
+
A typical release lifecycle that triggers `on.release.types: [published]` twice:
|
|
31
|
+
1. Developer creates a pre-release (e.g., v1.2.0-rc.1) and clicks "Publish release"
|
|
32
|
+
→ workflow fires (github.event.action=prereleased, github.event.release.prerelease=true)
|
|
33
|
+
2. Developer edits the release and unchecks "Set as a pre-release" to promote it to a
|
|
34
|
+
full release → workflow fires again (github.event.action=released, github.event.release.prerelease=false)
|
|
35
|
+
|
|
36
|
+
Additionally, the GitHub docs note that `prereleased` does NOT fire for pre-releases
|
|
37
|
+
published from a draft release, but `published` DOES fire — creating another asymmetry.
|
|
38
|
+
|
|
39
|
+
Workflows using `on.release.types: [published]` for deployment, package publishing,
|
|
40
|
+
or Docker image tagging will run twice for the same version tag, potentially creating
|
|
41
|
+
duplicate deployments, duplicate npm/PyPI publishes, or conflicting artifact versions.
|
|
42
|
+
fix: |
|
|
43
|
+
For production deploy workflows that should only run on final (non-pre-release) releases,
|
|
44
|
+
add an `if:` condition to check `github.event.release.prerelease == false`. Alternatively,
|
|
45
|
+
use `types: [released]` — which fires only when a stable release is published directly
|
|
46
|
+
or when a pre-release is promoted to full release, but NOT on initial pre-release creation.
|
|
47
|
+
|
|
48
|
+
To target pre-releases explicitly, use `types: [prereleased]`, but note it does not fire
|
|
49
|
+
for pre-releases published from drafts (use `published` + `if: github.event.release.prerelease == true`
|
|
50
|
+
in that case).
|
|
51
|
+
fix_code:
|
|
52
|
+
- language: yaml
|
|
53
|
+
label: 'Guard with if condition to skip pre-release publishes'
|
|
54
|
+
code: |
|
|
55
|
+
on:
|
|
56
|
+
release:
|
|
57
|
+
types: [published]
|
|
58
|
+
|
|
59
|
+
jobs:
|
|
60
|
+
deploy:
|
|
61
|
+
runs-on: ubuntu-latest
|
|
62
|
+
# Only deploy for final (non-pre-release) releases
|
|
63
|
+
if: github.event.release.prerelease == false
|
|
64
|
+
steps:
|
|
65
|
+
- name: Deploy release
|
|
66
|
+
run: echo "Deploying ${{ github.event.release.tag_name }}"
|
|
67
|
+
|
|
68
|
+
- language: yaml
|
|
69
|
+
label: 'Use released type to target only final releases (not pre-release creation)'
|
|
70
|
+
code: |
|
|
71
|
+
on:
|
|
72
|
+
release:
|
|
73
|
+
# released: fires when a release is published as final, OR when a pre-release
|
|
74
|
+
# is promoted to a full release. Does NOT fire on initial pre-release creation.
|
|
75
|
+
types: [released]
|
|
76
|
+
|
|
77
|
+
jobs:
|
|
78
|
+
deploy:
|
|
79
|
+
runs-on: ubuntu-latest
|
|
80
|
+
steps:
|
|
81
|
+
- name: Deploy release
|
|
82
|
+
run: echo "Deploying ${{ github.event.release.tag_name }}"
|
|
83
|
+
|
|
84
|
+
- language: yaml
|
|
85
|
+
label: 'Log release event fields at the start of release workflows for debugging'
|
|
86
|
+
code: |
|
|
87
|
+
jobs:
|
|
88
|
+
release-info:
|
|
89
|
+
runs-on: ubuntu-latest
|
|
90
|
+
steps:
|
|
91
|
+
- name: Debug release event
|
|
92
|
+
run: |
|
|
93
|
+
echo "action: ${{ github.event.action }}"
|
|
94
|
+
echo "prerelease: ${{ github.event.release.prerelease }}"
|
|
95
|
+
echo "tag: ${{ github.event.release.tag_name }}"
|
|
96
|
+
echo "draft: ${{ github.event.release.draft }}"
|
|
97
|
+
prevention:
|
|
98
|
+
- "Prefer types: [released] over types: [published] for production deployment workflows"
|
|
99
|
+
- "Always guard release deploy steps with: if: github.event.release.prerelease == false"
|
|
100
|
+
- "Make deployment and publish workflows idempotent so accidental double-runs cannot cause irreversible side effects"
|
|
101
|
+
- "Log github.event.action and github.event.release.prerelease at workflow start when debugging unexpected runs"
|
|
102
|
+
docs:
|
|
103
|
+
- url: 'https://docs.github.com/en/actions/writing-workflows/choosing-when-your-workflow-runs/events-that-trigger-workflows#release'
|
|
104
|
+
label: 'release event activity types — published fires for both prereleased and released (GitHub Docs)'
|
|
105
|
+
- url: 'https://stackoverflow.com/questions/78085080/how-to-limit-a-github-action-to-fire-only-once-on-a-release-trigger'
|
|
106
|
+
label: 'Stack Overflow: release action triggered 3 times for pre-release — limit using types and if conditions'
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
id: triggers-048
|
|
2
|
+
title: 'Scheduled workflows silently disabled on forked repositories — on.schedule never fires until manually re-enabled in Settings'
|
|
3
|
+
category: triggers
|
|
4
|
+
severity: silent-failure
|
|
5
|
+
tags:
|
|
6
|
+
- schedule
|
|
7
|
+
- fork
|
|
8
|
+
- workflow-disabled
|
|
9
|
+
- cron
|
|
10
|
+
- repository-settings
|
|
11
|
+
patterns:
|
|
12
|
+
- regex: 'Scheduled workflows are disabled for this repository'
|
|
13
|
+
flags: 'i'
|
|
14
|
+
- regex: 'This workflow is disabled'
|
|
15
|
+
flags: 'i'
|
|
16
|
+
error_messages:
|
|
17
|
+
- 'Scheduled workflows are disabled for this repository'
|
|
18
|
+
- 'This workflow is disabled'
|
|
19
|
+
root_cause: |
|
|
20
|
+
When a repository containing scheduled workflows is forked, GitHub automatically disables
|
|
21
|
+
all workflow triggers in the fork. This prevents scheduled CI runs from unexpectedly
|
|
22
|
+
consuming GitHub Actions minutes from the fork owner's account and avoids unintended
|
|
23
|
+
side effects from cloned automation.
|
|
24
|
+
|
|
25
|
+
Scheduled workflows (on.schedule:) stop firing entirely on the fork, even if the workflow
|
|
26
|
+
YAML is correct and the branch is up to date. The workflow appears in the Actions tab and
|
|
27
|
+
can be triggered manually via workflow_dispatch, but on.schedule: events are suppressed.
|
|
28
|
+
|
|
29
|
+
This commonly affects:
|
|
30
|
+
- Contributors forking repos to test or contribute scheduled jobs (e.g., nightly builds,
|
|
31
|
+
dependency updates, scheduled reports)
|
|
32
|
+
- Organizations using forks as part of their branching or release strategy and expecting
|
|
33
|
+
scheduled maintenance tasks to continue
|
|
34
|
+
- CI setups where scheduled smoke tests or integration tests are part of the fork workflow
|
|
35
|
+
|
|
36
|
+
The only indicator is the workflow showing as "disabled" in the Actions tab or in the
|
|
37
|
+
workflow run history, which is easy to miss.
|
|
38
|
+
fix: |
|
|
39
|
+
Navigate to the fork repository and enable workflows manually:
|
|
40
|
+
Settings (fork repo) -> Actions -> General -> Allow all actions and reusable workflows.
|
|
41
|
+
Then enable the specific workflow in the Actions tab by clicking "Enable workflow".
|
|
42
|
+
There is no workflow-level or API-level way to force-enable schedules on a fork automatically.
|
|
43
|
+
fix_code:
|
|
44
|
+
- language: yaml
|
|
45
|
+
label: 'Add workflow_dispatch fallback so contributors can trigger manually on forks'
|
|
46
|
+
code: |
|
|
47
|
+
on:
|
|
48
|
+
# Note: scheduled runs are DISABLED on forked repositories by default.
|
|
49
|
+
# Contributors must enable workflows in Settings -> Actions -> General.
|
|
50
|
+
schedule:
|
|
51
|
+
- cron: '0 2 * * *'
|
|
52
|
+
|
|
53
|
+
# Add workflow_dispatch as a fallback for manual triggering on forks
|
|
54
|
+
workflow_dispatch:
|
|
55
|
+
inputs:
|
|
56
|
+
reason:
|
|
57
|
+
description: 'Reason for manual run (required when testing on a fork)'
|
|
58
|
+
required: false
|
|
59
|
+
default: 'manual trigger'
|
|
60
|
+
|
|
61
|
+
prevention:
|
|
62
|
+
- "Document in CONTRIBUTING.md that scheduled workflows must be manually enabled on forks via Settings -> Actions -> General"
|
|
63
|
+
- "Add workflow_dispatch to all scheduled workflows so contributors can manually trigger them on forks during development"
|
|
64
|
+
- "Use repository_dispatch or workflow_run from the upstream repo to trigger downstream automation instead of relying on fork schedules"
|
|
65
|
+
docs:
|
|
66
|
+
- url: 'https://docs.github.com/en/actions/managing-workflow-runs-and-deployments/managing-workflow-runs/disabling-and-enabling-a-workflow'
|
|
67
|
+
label: 'Disabling and enabling a workflow — GitHub Docs'
|
|
68
|
+
- url: 'https://docs.github.com/en/actions/writing-workflows/choosing-when-your-workflow-runs/events-that-trigger-workflows#schedule'
|
|
69
|
+
label: 'schedule event — GitHub Docs'
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
id: yaml-syntax-045
|
|
2
|
+
title: 'Using paths and paths-ignore together on the same event trigger causes a workflow validation error'
|
|
3
|
+
category: yaml-syntax
|
|
4
|
+
severity: error
|
|
5
|
+
tags:
|
|
6
|
+
- paths
|
|
7
|
+
- paths-ignore
|
|
8
|
+
- trigger-filter
|
|
9
|
+
- on-push
|
|
10
|
+
- validation-error
|
|
11
|
+
patterns:
|
|
12
|
+
- regex: "You can't use both 'paths' and 'paths-ignore'"
|
|
13
|
+
flags: 'i'
|
|
14
|
+
- regex: 'Invalid workflow file.*paths.*paths-ignore'
|
|
15
|
+
flags: 'i'
|
|
16
|
+
- regex: 'paths.*and.*paths-ignore.*cannot.*same event'
|
|
17
|
+
flags: 'i'
|
|
18
|
+
error_messages:
|
|
19
|
+
- "Invalid workflow file: You can't use both 'paths' and 'paths-ignore' for the same event trigger"
|
|
20
|
+
- "You can't use both 'paths' and 'paths-ignore' for the same event trigger"
|
|
21
|
+
- "Path and Path-ignore cannot be added in the same event"
|
|
22
|
+
root_cause: |
|
|
23
|
+
GitHub Actions enforces a mutual exclusivity rule: `paths` and `paths-ignore` cannot be
|
|
24
|
+
specified simultaneously for the same event trigger. The same rule applies to:
|
|
25
|
+
- `branches` and `branches-ignore`
|
|
26
|
+
- `tags` and `tags-ignore`
|
|
27
|
+
|
|
28
|
+
`paths` is an allowlist — the trigger only fires when at least one of the listed patterns
|
|
29
|
+
matches a changed file. `paths-ignore` is a denylist — the trigger is suppressed when ALL
|
|
30
|
+
changed files match patterns in the list. Because their semantics are complementary, GitHub
|
|
31
|
+
disallows combining them to avoid ambiguous filter behavior.
|
|
32
|
+
|
|
33
|
+
Attempting to use both results in a validation error at workflow parse time:
|
|
34
|
+
"Invalid workflow file: You can't use both 'paths' and 'paths-ignore' for the same event trigger"
|
|
35
|
+
|
|
36
|
+
Developers commonly hit this when they want to trigger on a specific directory but exclude
|
|
37
|
+
documentation or test-only changes within that directory, assuming both filters can be
|
|
38
|
+
stacked like CSS selectors.
|
|
39
|
+
|
|
40
|
+
Example of the problematic pattern:
|
|
41
|
+
on:
|
|
42
|
+
push:
|
|
43
|
+
paths:
|
|
44
|
+
- 'src/**'
|
|
45
|
+
paths-ignore: # INVALID: cannot combine with paths
|
|
46
|
+
- 'src/**/*.md'
|
|
47
|
+
fix: |
|
|
48
|
+
Use only one of `paths` or `paths-ignore`. To achieve both include and exclude behavior
|
|
49
|
+
within a single filter:
|
|
50
|
+
- Use the `paths` filter with `!` negation patterns to explicitly exclude paths from an
|
|
51
|
+
allowlist. List all positive patterns before negative patterns — order matters.
|
|
52
|
+
- Alternatively, use `paths-ignore` alone if you just need to exclude a small set of paths
|
|
53
|
+
and want the workflow to run for everything else.
|
|
54
|
+
fix_code:
|
|
55
|
+
- language: yaml
|
|
56
|
+
label: 'WRONG: paths and paths-ignore combined (validation error)'
|
|
57
|
+
code: |
|
|
58
|
+
# This is INVALID — causes "You can't use both" validation error
|
|
59
|
+
on:
|
|
60
|
+
push:
|
|
61
|
+
paths:
|
|
62
|
+
- 'src/**'
|
|
63
|
+
paths-ignore:
|
|
64
|
+
- 'src/**/*.md'
|
|
65
|
+
|
|
66
|
+
- language: yaml
|
|
67
|
+
label: 'CORRECT: use paths with negation pattern to exclude within an allowlist'
|
|
68
|
+
code: |
|
|
69
|
+
# Use ! negation in paths to exclude specific sub-paths from the allowlist
|
|
70
|
+
on:
|
|
71
|
+
push:
|
|
72
|
+
paths:
|
|
73
|
+
- 'src/**' # include all changes under src/
|
|
74
|
+
- '!src/**/*.md' # but exclude markdown files within src/
|
|
75
|
+
|
|
76
|
+
- language: yaml
|
|
77
|
+
label: 'CORRECT: use only paths-ignore to suppress trigger for specific paths'
|
|
78
|
+
code: |
|
|
79
|
+
# If the workflow should run for everything EXCEPT docs, use paths-ignore alone
|
|
80
|
+
on:
|
|
81
|
+
push:
|
|
82
|
+
paths-ignore:
|
|
83
|
+
- 'docs/**'
|
|
84
|
+
- '**/*.md'
|
|
85
|
+
- '**/*.txt'
|
|
86
|
+
prevention:
|
|
87
|
+
- "Never combine paths with paths-ignore (or branches/branches-ignore, or tags/tags-ignore) for the same event"
|
|
88
|
+
- "To include a directory but exclude sub-paths: use paths with ! negation entries"
|
|
89
|
+
- "Use actionlint to validate workflow YAML locally before pushing — it catches this misconfiguration"
|
|
90
|
+
- "The same mutual exclusivity rule applies to branches/branches-ignore and tags/tags-ignore"
|
|
91
|
+
docs:
|
|
92
|
+
- url: 'https://docs.github.com/en/actions/writing-workflows/workflow-syntax-for-github-actions#onpushpull_requestpull_request_targetpaths'
|
|
93
|
+
label: 'on.push.paths — cannot be used with paths-ignore for the same event (GitHub Docs)'
|
|
94
|
+
- url: 'https://stackoverflow.com/questions/64896276/github-action-trigger-pipeline-for-root-folder-except-one-sub-folder'
|
|
95
|
+
label: 'Stack Overflow: "Path and Path-ignore cannot be added in the same event" — use ! negation in paths instead'
|
|
96
|
+
- url: 'https://github.com/rhysd/actionlint'
|
|
97
|
+
label: 'actionlint — static checker for GitHub Actions workflow files'
|
package/package.json
CHANGED