@htekdev/actions-debugger 1.0.113 → 1.0.114
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/errors/caching-artifacts/cache-corrupt-on-cancel-during-restore-save-always.yml +136 -0
- package/errors/caching-artifacts/restore-keys-asterisk-literal-not-glob.yml +107 -0
- package/errors/concurrency-timing/pull-request-review-shared-concurrency-cancels-ci.yml +131 -0
- package/errors/known-unsolved/github-script-esm-not-supported.yml +111 -0
- package/errors/known-unsolved/job-outputs-string-only-no-array-object.yml +142 -0
- package/errors/permissions-auth/oidc-immutable-sub-claim-new-repo-trust-policy-mismatch.yml +122 -0
- package/errors/permissions-auth/permissions-auth-064.yml +122 -0
- package/errors/permissions-auth/permissions-auth-065.yml +97 -0
- package/errors/permissions-auth/permissions-auth-066.yml +129 -0
- package/errors/runner-environment/arc-kubernetes-checkout-circular-json-container-hook.yml +101 -0
- package/errors/runner-environment/cache-restore-windows-runner-silent-crash.yml +130 -0
- package/errors/runner-environment/git-248-fetch-tags-shallow-clone-regression.yml +100 -0
- package/errors/runner-environment/javascript-actions-alpine-arm64-not-supported.yml +121 -0
- package/errors/runner-environment/runner-environment-188.yml +96 -0
- package/errors/runner-environment/runner-environment-191.yml +147 -0
- package/errors/runner-environment/runner-environment-192.yml +144 -0
- package/errors/runner-environment/runner-environment-193.yml +136 -0
- package/errors/runner-environment/runner-environment-194.yml +86 -0
- package/errors/silent-failures/checkout-v6-clean-false-deletes-workspace-on-repo-change.yml +119 -0
- package/errors/silent-failures/queue-max-silently-ignored-with-cancel-in-progress.yml +109 -0
- package/errors/silent-failures/silent-failures-102.yml +141 -0
- package/errors/silent-failures/silent-failures-104.yml +119 -0
- package/errors/yaml-syntax/yaml-syntax-068.yml +137 -0
- package/errors/yaml-syntax/yaml-syntax-069.yml +118 -0
- package/package.json +1 -1
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
id: caching-artifacts-067
|
|
2
|
+
title: "Cache corrupted when workflow canceled mid-restore and save runs via always()"
|
|
3
|
+
category: caching-artifacts
|
|
4
|
+
severity: silent-failure
|
|
5
|
+
tags:
|
|
6
|
+
- cache
|
|
7
|
+
- cancellation
|
|
8
|
+
- corrupt
|
|
9
|
+
- restore
|
|
10
|
+
- always
|
|
11
|
+
- windows
|
|
12
|
+
- tar
|
|
13
|
+
patterns:
|
|
14
|
+
- regex: 'The operation was canceled'
|
|
15
|
+
flags: 'i'
|
|
16
|
+
- regex: 'internal error.*InstallPlan.*not in configured state'
|
|
17
|
+
flags: 'i'
|
|
18
|
+
- regex: 'Cache restored successfully'
|
|
19
|
+
flags: 'i'
|
|
20
|
+
- regex: 'Cache hit for:.*\nReceived.*\nError: The operation was canceled'
|
|
21
|
+
flags: 'is'
|
|
22
|
+
error_messages:
|
|
23
|
+
- "Error: The operation was canceled."
|
|
24
|
+
- "Cache restored successfully"
|
|
25
|
+
- "internal error in InstallPlan.completed: not in configured state: Installed"
|
|
26
|
+
root_cause: |
|
|
27
|
+
When a workflow is canceled while `actions/cache/restore` is mid-way through
|
|
28
|
+
extracting the cache archive (via `tar`), the extraction is interrupted and the
|
|
29
|
+
working directory is left in a **partially extracted, corrupt state**. The cache
|
|
30
|
+
hit was registered before extraction began, so the action considers the key
|
|
31
|
+
matched.
|
|
32
|
+
|
|
33
|
+
If a separate `actions/cache/save` step runs with `if: always()` (or the job has
|
|
34
|
+
`save-state` / explicit save at the end), it executes despite the cancellation.
|
|
35
|
+
It then compresses and saves the **partial working directory** — including the
|
|
36
|
+
incomplete files from the aborted tar extraction — under the exact same cache
|
|
37
|
+
key.
|
|
38
|
+
|
|
39
|
+
On subsequent workflow runs:
|
|
40
|
+
1. `actions/cache/restore` finds the key (exact match), downloads and extracts
|
|
41
|
+
the archive successfully ("Cache restored successfully").
|
|
42
|
+
2. The build starts with corrupt or incomplete files from the previous partial
|
|
43
|
+
extraction and fails with cryptic internal errors, not a caching error.
|
|
44
|
+
|
|
45
|
+
The symptom is particularly confusing because the restore step looks perfectly
|
|
46
|
+
healthy: it shows 100% download progress and "Cache restored successfully", but
|
|
47
|
+
the build then fails with errors like "not in configured state: Installed" (Cabal
|
|
48
|
+
Haskell), "invalid content", "module not found" (npm), or similar tool-specific
|
|
49
|
+
corruption errors.
|
|
50
|
+
|
|
51
|
+
This is distinct from `caching-artifacts-007` (cache post-step skipped on cancel
|
|
52
|
+
— where the cache is NOT saved). This entry covers the opposite scenario: an
|
|
53
|
+
explicit `always()` save step actively commits a corrupt partial state, poisoning
|
|
54
|
+
the cache for all future runs until the key expires or is manually deleted.
|
|
55
|
+
fix: |
|
|
56
|
+
1. **Delete the corrupt cache entry** via the GitHub Actions UI (Settings →
|
|
57
|
+
Actions → Caches) or the GitHub API:
|
|
58
|
+
`DELETE /repos/{owner}/{repo}/actions/caches/{cache_id}`
|
|
59
|
+
|
|
60
|
+
2. **Guard the save step** so it only runs when restoration AND the build
|
|
61
|
+
succeeded — not unconditionally:
|
|
62
|
+
|
|
63
|
+
```yaml
|
|
64
|
+
- name: Save cache
|
|
65
|
+
uses: actions/cache/save@v4
|
|
66
|
+
if: success() && steps.restore.outputs.cache-hit != 'true'
|
|
67
|
+
with:
|
|
68
|
+
path: ...
|
|
69
|
+
key: ...
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
3. **Avoid pairing `always()` with cache save** when the prior restore step
|
|
73
|
+
may have been interrupted. Use explicit `success()` or a combination
|
|
74
|
+
`if: success() || steps.restore.conclusion == 'success'`.
|
|
75
|
+
|
|
76
|
+
4. On Windows, consider using `actions/cache@v4` with `enableCrossOsArchive: false`
|
|
77
|
+
and switching to `zstd`-backed archives, which are more resilient to partial
|
|
78
|
+
extraction (partial frames are detectable by zstd checksums).
|
|
79
|
+
fix_code:
|
|
80
|
+
- language: yaml
|
|
81
|
+
label: "WRONG — always() save can commit partial corrupt state after cancellation"
|
|
82
|
+
code: |
|
|
83
|
+
- name: Restore cache
|
|
84
|
+
id: restore
|
|
85
|
+
uses: actions/cache/restore@v4
|
|
86
|
+
with:
|
|
87
|
+
path: ~/.cabal/store
|
|
88
|
+
key: cabal-${{ runner.os }}-${{ hashFiles('cabal.project.freeze') }}
|
|
89
|
+
|
|
90
|
+
# ... build steps ...
|
|
91
|
+
|
|
92
|
+
- name: Save cache
|
|
93
|
+
uses: actions/cache/save@v4
|
|
94
|
+
if: always() # ← PROBLEM: runs even if restore was interrupted
|
|
95
|
+
with:
|
|
96
|
+
path: ~/.cabal/store
|
|
97
|
+
key: cabal-${{ runner.os }}-${{ hashFiles('cabal.project.freeze') }}
|
|
98
|
+
- language: yaml
|
|
99
|
+
label: "CORRECT — only save when restore and build succeeded"
|
|
100
|
+
code: |
|
|
101
|
+
- name: Restore cache
|
|
102
|
+
id: restore
|
|
103
|
+
uses: actions/cache/restore@v4
|
|
104
|
+
with:
|
|
105
|
+
path: ~/.cabal/store
|
|
106
|
+
key: cabal-${{ runner.os }}-${{ hashFiles('cabal.project.freeze') }}
|
|
107
|
+
restore-keys: |
|
|
108
|
+
cabal-${{ runner.os }}-
|
|
109
|
+
|
|
110
|
+
# ... build steps ...
|
|
111
|
+
|
|
112
|
+
- name: Save cache
|
|
113
|
+
uses: actions/cache/save@v4
|
|
114
|
+
if: success() # ← SAFE: only runs on clean job success
|
|
115
|
+
with:
|
|
116
|
+
path: ~/.cabal/store
|
|
117
|
+
key: cabal-${{ runner.os }}-${{ hashFiles('cabal.project.freeze') }}
|
|
118
|
+
prevention:
|
|
119
|
+
- "Never use `if: always()` on a `cache/save` step — use `if: success()` or
|
|
120
|
+
an explicit condition that confirms the build completed cleanly."
|
|
121
|
+
- "After any job cancellation that hit a cache restore step, check the GitHub
|
|
122
|
+
Actions Caches list and delete the affected cache key to prevent future runs
|
|
123
|
+
from inheriting the corrupt state."
|
|
124
|
+
- "Use `actions/cache@v4` in the combined save-on-miss pattern with the default
|
|
125
|
+
post-job hook only (no explicit save step); the post-job hook is automatically
|
|
126
|
+
skipped on cancellation, avoiding the corrupt save."
|
|
127
|
+
- "Consider setting a short TTL on Windows caches or using unique keys per run
|
|
128
|
+
(`key: ...-${{ github.run_id }}`) for artifacts that are sensitive to partial
|
|
129
|
+
extraction."
|
|
130
|
+
docs:
|
|
131
|
+
- url: "https://github.com/actions/cache/issues/1729"
|
|
132
|
+
label: "actions/cache#1729: If a workflow is canceled during cache/restore then cache/save writes a corrupt cache (Windows)"
|
|
133
|
+
- url: "https://github.com/actions/toolkit/tree/main/packages/cache"
|
|
134
|
+
label: "@actions/cache package documentation"
|
|
135
|
+
- url: "https://docs.github.com/en/actions/writing-workflows/choosing-what-your-workflow-does/caching-dependencies-to-speed-up-workflows"
|
|
136
|
+
label: "Caching dependencies to speed up workflows"
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
id: caching-artifacts-066
|
|
2
|
+
title: 'restore-keys patterns are literal prefix strings — asterisk is not a glob wildcard'
|
|
3
|
+
category: caching-artifacts
|
|
4
|
+
severity: silent-failure
|
|
5
|
+
tags:
|
|
6
|
+
- cache
|
|
7
|
+
- actions/cache
|
|
8
|
+
- restore-keys
|
|
9
|
+
- prefix
|
|
10
|
+
- cache-miss
|
|
11
|
+
- glob
|
|
12
|
+
patterns:
|
|
13
|
+
- regex: 'restore-keys:.*\*'
|
|
14
|
+
flags: 'i'
|
|
15
|
+
- regex: 'Cache not found for input keys:'
|
|
16
|
+
flags: 'i'
|
|
17
|
+
- regex: 'No cache found for key'
|
|
18
|
+
flags: 'i'
|
|
19
|
+
error_messages:
|
|
20
|
+
- 'Cache not found for input keys:'
|
|
21
|
+
- 'No cache found for key'
|
|
22
|
+
- '(silent cache miss — no error, restore-keys with asterisk simply fails to match any existing cache entry)'
|
|
23
|
+
root_cause: |
|
|
24
|
+
GitHub Actions cache restore-keys are prefix strings, not glob patterns. The
|
|
25
|
+
asterisk (*) character has no special meaning — it is treated as a literal
|
|
26
|
+
character in the prefix match.
|
|
27
|
+
|
|
28
|
+
A restore-keys entry of:
|
|
29
|
+
restore-keys: npm-${{ runner.os }}-*
|
|
30
|
+
|
|
31
|
+
does NOT match cache keys like:
|
|
32
|
+
npm-Ubuntu-abc123hash
|
|
33
|
+
npm-Ubuntu-lockfile-abcdef
|
|
34
|
+
|
|
35
|
+
It only matches keys whose name starts with the literal string
|
|
36
|
+
"npm-Ubuntu-*" (including the asterisk), which virtually never exist,
|
|
37
|
+
resulting in a silent cache miss with no error message.
|
|
38
|
+
|
|
39
|
+
This mistake is widespread because:
|
|
40
|
+
1. The actions/cache documentation uses the term "prefix" but does not
|
|
41
|
+
explicitly state that glob characters are unsupported or treated literally.
|
|
42
|
+
2. Many other caching tools (Gradle, Maven, Buildkite, CircleCI) accept glob
|
|
43
|
+
or wildcard patterns in cache key configurations, leading developers to
|
|
44
|
+
apply the same syntax to GitHub Actions.
|
|
45
|
+
3. The failure mode is silent — the step reports "Cache not found for input keys"
|
|
46
|
+
and continues normally. Without inspecting the exact key string printed in the
|
|
47
|
+
step log, the asterisk in the pattern is invisible as the cause.
|
|
48
|
+
|
|
49
|
+
The correct behavior: restore-keys prefix matching finds any cache entry whose
|
|
50
|
+
key BEGINS WITH the given string. Trailing a prefix with a dash or separator
|
|
51
|
+
(e.g., npm-Ubuntu-) is all that is needed to match any entry starting with
|
|
52
|
+
that prefix — no asterisk required or supported.
|
|
53
|
+
fix: |
|
|
54
|
+
Remove any glob characters from restore-keys entries. A bare prefix string
|
|
55
|
+
ending in a dash or other separator correctly matches all cache entries whose
|
|
56
|
+
key starts with that prefix:
|
|
57
|
+
|
|
58
|
+
restore-keys: |
|
|
59
|
+
npm-${{ runner.os }}- ← correct: prefix without asterisk
|
|
60
|
+
|
|
61
|
+
The cache service automatically returns the most recently saved matching entry,
|
|
62
|
+
so multiple ordered restore-keys fallback entries can be stacked from most
|
|
63
|
+
specific to broadest.
|
|
64
|
+
fix_code:
|
|
65
|
+
- language: yaml
|
|
66
|
+
label: 'Broken: asterisk treated as literal — no cached keys match this pattern'
|
|
67
|
+
code: |
|
|
68
|
+
- uses: actions/cache@v4
|
|
69
|
+
with:
|
|
70
|
+
path: ~/.npm
|
|
71
|
+
key: npm-${{ runner.os }}-${{ hashFiles('**/package-lock.json') }}
|
|
72
|
+
restore-keys: |
|
|
73
|
+
npm-${{ runner.os }}-*
|
|
74
|
+
- language: yaml
|
|
75
|
+
label: 'Fixed: bare prefix (no asterisk) correctly matches all prior cache entries'
|
|
76
|
+
code: |
|
|
77
|
+
- uses: actions/cache@v4
|
|
78
|
+
with:
|
|
79
|
+
path: ~/.npm
|
|
80
|
+
key: npm-${{ runner.os }}-${{ hashFiles('**/package-lock.json') }}
|
|
81
|
+
restore-keys: |
|
|
82
|
+
npm-${{ runner.os }}-
|
|
83
|
+
- language: yaml
|
|
84
|
+
label: 'Multiple fallback tiers from most specific to broadest (no globs needed)'
|
|
85
|
+
code: |
|
|
86
|
+
- uses: actions/cache@v4
|
|
87
|
+
with:
|
|
88
|
+
path: |
|
|
89
|
+
~/.npm
|
|
90
|
+
~/.cache/Cypress
|
|
91
|
+
key: node-${{ runner.os }}-${{ matrix.node-version }}-${{ hashFiles('**/package-lock.json') }}
|
|
92
|
+
restore-keys: |
|
|
93
|
+
node-${{ runner.os }}-${{ matrix.node-version }}-
|
|
94
|
+
node-${{ runner.os }}-
|
|
95
|
+
node-
|
|
96
|
+
prevention:
|
|
97
|
+
- 'Never use glob characters (*, ?, []) in restore-keys — they are literal string characters; the cache key service does not interpret them as wildcards'
|
|
98
|
+
- 'End restore-keys prefix entries with a trailing dash or separator: restore-keys: npm-${{ runner.os }}- not npm-${{ runner.os }}-*'
|
|
99
|
+
- 'If cache misses persist despite restore-keys being configured, add a debug step that echoes the exact key and restore-keys strings being evaluated (using set -x or explicit echo) to spot literal asterisks in the output'
|
|
100
|
+
- 'Do not confuse restore-keys with glob support in path: — the path: field DOES support glob patterns; restore-keys does not'
|
|
101
|
+
docs:
|
|
102
|
+
- url: 'https://docs.github.com/en/actions/writing-workflows/choosing-what-your-workflow-does/caching-dependencies-to-speed-up-workflows#matching-a-cache-key'
|
|
103
|
+
label: 'GitHub Docs: Matching a cache key'
|
|
104
|
+
- url: 'https://github.com/actions/cache#inputs'
|
|
105
|
+
label: 'actions/cache README: Inputs — restore-keys'
|
|
106
|
+
- url: 'https://github.com/actions/cache/blob/main/tips-and-workarounds.md'
|
|
107
|
+
label: 'actions/cache: Tips and workarounds'
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
id: concurrency-timing-052
|
|
2
|
+
title: 'pull_request_review event in shared concurrency group cancels in-progress CI on reviewer submission'
|
|
3
|
+
category: concurrency-timing
|
|
4
|
+
severity: silent-failure
|
|
5
|
+
tags:
|
|
6
|
+
- concurrency
|
|
7
|
+
- pull_request_review
|
|
8
|
+
- pull_request
|
|
9
|
+
- cancel-in-progress
|
|
10
|
+
- code-review
|
|
11
|
+
- ci-cancellation
|
|
12
|
+
patterns:
|
|
13
|
+
- regex: 'pull_request_review'
|
|
14
|
+
flags: 'i'
|
|
15
|
+
- regex: 'Canceling since a higher priority waiting run was found'
|
|
16
|
+
flags: 'i'
|
|
17
|
+
- regex: 'This run was automatically cancelled'
|
|
18
|
+
flags: 'i'
|
|
19
|
+
error_messages:
|
|
20
|
+
- 'This run was automatically cancelled'
|
|
21
|
+
- 'Canceling since a higher priority waiting run was found'
|
|
22
|
+
root_cause: |
|
|
23
|
+
When a workflow responds to both on.pull_request and on.pull_request_review, and
|
|
24
|
+
uses a concurrency group keyed on github.event.pull_request.number, both event
|
|
25
|
+
types resolve to the same concurrency group key for a given PR.
|
|
26
|
+
|
|
27
|
+
Unlike push events where github.event.pull_request is absent, the
|
|
28
|
+
pull_request_review event payload DOES include a pull_request object with a number
|
|
29
|
+
property — so github.event.pull_request.number evaluates identically for both
|
|
30
|
+
pull_request [synchronize] and pull_request_review [submitted] events on the same PR.
|
|
31
|
+
|
|
32
|
+
As a result, every reviewer action — approvals, review comments, change requests,
|
|
33
|
+
and re-requests — triggers a new workflow run that shares the same concurrency slot
|
|
34
|
+
as the CI run from the last code push. With cancel-in-progress: true, a reviewer
|
|
35
|
+
merely submitting an approval silently cancels the running CI checks for that PR,
|
|
36
|
+
causing the PR to show pending or missing status checks despite no new commit being
|
|
37
|
+
pushed.
|
|
38
|
+
|
|
39
|
+
This catches teams off-guard because the reviewer action is unrelated to the code
|
|
40
|
+
under test — no new commit arrived — yet the running CI is terminated. The PR
|
|
41
|
+
may be left with required checks in a "Pending" state even though nothing has
|
|
42
|
+
changed, and developers must manually re-run CI to restore a passing state.
|
|
43
|
+
fix: |
|
|
44
|
+
Include github.event_name in the concurrency group key so that pull_request CI
|
|
45
|
+
runs and pull_request_review-triggered runs occupy separate, independent slots:
|
|
46
|
+
|
|
47
|
+
group: ${{ github.workflow }}-${{ github.event_name }}-${{ github.event.pull_request.number }}
|
|
48
|
+
|
|
49
|
+
Alternatively, split the pull_request_review trigger into a separate workflow file
|
|
50
|
+
that handles review-gated tasks (labeling, auto-approve, notifications) while
|
|
51
|
+
keeping CI in a dedicated pull_request-only workflow with its own concurrency group.
|
|
52
|
+
fix_code:
|
|
53
|
+
- language: yaml
|
|
54
|
+
label: 'Broken: reviewer approval cancels in-progress CI run'
|
|
55
|
+
code: |
|
|
56
|
+
on:
|
|
57
|
+
pull_request:
|
|
58
|
+
pull_request_review:
|
|
59
|
+
types: [submitted]
|
|
60
|
+
|
|
61
|
+
concurrency:
|
|
62
|
+
group: ci-${{ github.event.pull_request.number }}
|
|
63
|
+
cancel-in-progress: true
|
|
64
|
+
|
|
65
|
+
jobs:
|
|
66
|
+
test:
|
|
67
|
+
runs-on: ubuntu-latest
|
|
68
|
+
steps:
|
|
69
|
+
- uses: actions/checkout@v4
|
|
70
|
+
- run: make test
|
|
71
|
+
- language: yaml
|
|
72
|
+
label: 'Fixed: include event_name to give each event type its own concurrency slot'
|
|
73
|
+
code: |
|
|
74
|
+
on:
|
|
75
|
+
pull_request:
|
|
76
|
+
pull_request_review:
|
|
77
|
+
types: [submitted]
|
|
78
|
+
|
|
79
|
+
concurrency:
|
|
80
|
+
# pull_request pushes: ci-pull_request-42
|
|
81
|
+
# pull_request_review submissions: ci-pull_request_review-42 (independent)
|
|
82
|
+
group: ci-${{ github.event_name }}-${{ github.event.pull_request.number }}
|
|
83
|
+
cancel-in-progress: true
|
|
84
|
+
|
|
85
|
+
jobs:
|
|
86
|
+
test:
|
|
87
|
+
runs-on: ubuntu-latest
|
|
88
|
+
steps:
|
|
89
|
+
- uses: actions/checkout@v4
|
|
90
|
+
- run: make test
|
|
91
|
+
- language: yaml
|
|
92
|
+
label: 'Alternative: split CI and review automation into separate workflow files'
|
|
93
|
+
code: |
|
|
94
|
+
# .github/workflows/ci.yml — triggered by code changes only
|
|
95
|
+
on:
|
|
96
|
+
pull_request:
|
|
97
|
+
|
|
98
|
+
concurrency:
|
|
99
|
+
group: ci-${{ github.event.pull_request.number }}
|
|
100
|
+
cancel-in-progress: true
|
|
101
|
+
|
|
102
|
+
jobs:
|
|
103
|
+
test:
|
|
104
|
+
runs-on: ubuntu-latest
|
|
105
|
+
steps:
|
|
106
|
+
- uses: actions/checkout@v4
|
|
107
|
+
- run: make test
|
|
108
|
+
|
|
109
|
+
# .github/workflows/review-automation.yml — review gating only
|
|
110
|
+
on:
|
|
111
|
+
pull_request_review:
|
|
112
|
+
types: [submitted]
|
|
113
|
+
|
|
114
|
+
jobs:
|
|
115
|
+
label-approved:
|
|
116
|
+
if: github.event.review.state == 'approved'
|
|
117
|
+
runs-on: ubuntu-latest
|
|
118
|
+
steps:
|
|
119
|
+
- run: echo "PR approved — add label or trigger deploy"
|
|
120
|
+
prevention:
|
|
121
|
+
- 'Audit any workflow with multiple on: event triggers for cross-event concurrency group collisions — confirm that each event type resolves to a unique group key unless intentional sharing is desired'
|
|
122
|
+
- 'Include ${{ github.event_name }} in concurrency group keys for workflows triggered by more than one event type to prevent unintended cross-event cancellation'
|
|
123
|
+
- 'Use separate workflow files for CI checks (on.pull_request) and review-automation tasks (on.pull_request_review) to avoid shared concurrency group interference'
|
|
124
|
+
- 'When debugging unexpectedly cancelled CI runs, check the Actions tab for runs with "Cancelled" status — filter by trigger event to identify whether a pull_request_review submission was the cancelling run'
|
|
125
|
+
docs:
|
|
126
|
+
- url: 'https://docs.github.com/en/actions/writing-workflows/choosing-when-your-workflow-runs/events-that-trigger-workflows#pull_request_review'
|
|
127
|
+
label: 'GitHub Docs: pull_request_review event'
|
|
128
|
+
- url: 'https://docs.github.com/en/actions/writing-workflows/choosing-what-your-workflow-does/using-concurrency'
|
|
129
|
+
label: 'GitHub Docs: Using concurrency'
|
|
130
|
+
- url: 'https://github.com/orgs/community/discussions/26336'
|
|
131
|
+
label: 'GitHub Community: Concurrency cancel-in-progress cancels CI on review submission'
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
id: known-unsolved-061
|
|
2
|
+
title: 'actions/github-script does not support ESM — import statements and ESM-only packages fail'
|
|
3
|
+
category: known-unsolved
|
|
4
|
+
severity: error
|
|
5
|
+
tags:
|
|
6
|
+
- github-script
|
|
7
|
+
- esm
|
|
8
|
+
- commonjs
|
|
9
|
+
- import
|
|
10
|
+
- ERR_REQUIRE_ESM
|
|
11
|
+
- octokit
|
|
12
|
+
- javascript
|
|
13
|
+
patterns:
|
|
14
|
+
- regex: 'SyntaxError: Cannot use import statement in a module'
|
|
15
|
+
flags: 'i'
|
|
16
|
+
- regex: 'ERR_REQUIRE_ESM'
|
|
17
|
+
flags: 'i'
|
|
18
|
+
- regex: 'require\(\) of ES Module.*is not supported'
|
|
19
|
+
flags: 'i'
|
|
20
|
+
- regex: 'must use import to load ES Module'
|
|
21
|
+
flags: 'i'
|
|
22
|
+
error_messages:
|
|
23
|
+
- "SyntaxError: Cannot use import statement in a module"
|
|
24
|
+
- "Error [ERR_REQUIRE_ESM]: require() of ES Module /path/to/node_modules/pkg/index.mjs is not supported."
|
|
25
|
+
- "Instead change the require of ... to a dynamic import() which is available in all CommonJS modules."
|
|
26
|
+
- "Error: Must use import to load ES Module: /path/to/node_modules/pkg/index.js"
|
|
27
|
+
root_cause: |
|
|
28
|
+
actions/github-script evaluates the inline `script:` input using Node.js vm.Script in a
|
|
29
|
+
CommonJS (CJS) module context. ECMAScript Module (ESM) syntax is fundamentally incompatible
|
|
30
|
+
with this execution model:
|
|
31
|
+
|
|
32
|
+
1. Top-level `import ... from` statements → SyntaxError: "Cannot use import statement in a module"
|
|
33
|
+
because vm.Script runs in CJS mode and the `import` keyword is a syntax error at parse time.
|
|
34
|
+
|
|
35
|
+
2. require() of an ESM-only package (e.g., modern @octokit/rest ≥ 5.x, chalk ≥ 5.x,
|
|
36
|
+
node-fetch ≥ 3.x, got ≥ 12.x, axios ≥ 2.x) → ERR_REQUIRE_ESM because those packages
|
|
37
|
+
publish only .mjs or set "type": "module" in package.json, making them incompatible
|
|
38
|
+
with require().
|
|
39
|
+
|
|
40
|
+
3. External scripts loaded via `const fn = require('./my-script.js')` also run in a CJS
|
|
41
|
+
context — ESM syntax inside those files also fails.
|
|
42
|
+
|
|
43
|
+
The github-script action has no current plan to migrate to ESM execution, and supporting
|
|
44
|
+
both modes simultaneously in vm.Script is architecturally complex. This is a long-standing
|
|
45
|
+
limitation open since 2024 with 30+ reactions and no scheduled resolution.
|
|
46
|
+
|
|
47
|
+
The GitHub Actions toolkit itself (github-script's own dependencies) also pins to
|
|
48
|
+
CJS-compatible versions of @octokit packages for this reason.
|
|
49
|
+
fix: |
|
|
50
|
+
There is no built-in fix — actions/github-script does not support ESM. Use these workarounds:
|
|
51
|
+
|
|
52
|
+
WORKAROUND 1 (recommended): Use dynamic import() inside an async function
|
|
53
|
+
Dynamic import() IS supported inside github-script because it is a CJS-compatible
|
|
54
|
+
expression (not top-level ESM syntax). Wrap the entire script in an async IIFE or
|
|
55
|
+
use the top-level await that github-script already provides:
|
|
56
|
+
|
|
57
|
+
const { Octokit } = await import('@octokit/rest')
|
|
58
|
+
|
|
59
|
+
WORKAROUND 2: Use a CJS-compatible version of the package
|
|
60
|
+
Many packages (chalk, got, axios, node-fetch) have a last CJS-compatible version.
|
|
61
|
+
Pin to that in your workflow step using node or npm:
|
|
62
|
+
|
|
63
|
+
chalk: ^4.1.2 (last CJS version; chalk 5.x is ESM-only)
|
|
64
|
+
node-fetch: ^2.7.0 (last CJS version; 3.x is ESM-only)
|
|
65
|
+
got: ^11.8.6 (last CJS version; 12.x is ESM-only)
|
|
66
|
+
|
|
67
|
+
WORKAROUND 3: Create a composite or JavaScript action with a proper ESM Node runtime
|
|
68
|
+
For complex scripts that heavily use ESM packages, extract them into a standalone
|
|
69
|
+
composite or JavaScript action that declares "main": "./dist/index.mjs" in action.yml.
|
|
70
|
+
fix_code:
|
|
71
|
+
- language: yaml
|
|
72
|
+
label: 'Use dynamic import() for ESM-only packages in github-script'
|
|
73
|
+
code: |
|
|
74
|
+
- name: Use ESM package via dynamic import()
|
|
75
|
+
uses: actions/github-script@v7
|
|
76
|
+
with:
|
|
77
|
+
script: |
|
|
78
|
+
// Dynamic import() works in github-script even though top-level import does not
|
|
79
|
+
const { Octokit } = await import('@octokit/rest')
|
|
80
|
+
const { default: chalk } = await import('chalk')
|
|
81
|
+
// github, context, core are still available as usual
|
|
82
|
+
const result = await github.rest.repos.get({
|
|
83
|
+
owner: context.repo.owner,
|
|
84
|
+
repo: context.repo.repo,
|
|
85
|
+
})
|
|
86
|
+
core.info(chalk.green(`Repo: ${result.data.full_name}`))
|
|
87
|
+
- language: yaml
|
|
88
|
+
label: 'Pin to last CJS-compatible version of common packages'
|
|
89
|
+
code: |
|
|
90
|
+
- name: Install CJS-compatible package versions
|
|
91
|
+
run: npm install chalk@4 node-fetch@2 got@11
|
|
92
|
+
|
|
93
|
+
- name: Use CJS packages in github-script
|
|
94
|
+
uses: actions/github-script@v7
|
|
95
|
+
with:
|
|
96
|
+
script: |
|
|
97
|
+
const chalk = require('chalk') // chalk@4 is CJS — works fine
|
|
98
|
+
const fetch = require('node-fetch') // node-fetch@2 is CJS
|
|
99
|
+
core.info(chalk.blue('Using CJS-compatible packages'))
|
|
100
|
+
prevention:
|
|
101
|
+
- 'Check whether a package is ESM-only before using it in github-script — look for "type": "module" in its package.json'
|
|
102
|
+
- 'Use dynamic import() as the default pattern for any external package in github-script'
|
|
103
|
+
- 'For scripts that require heavy ESM ecosystem usage, consider a dedicated JavaScript action instead of github-script'
|
|
104
|
+
- 'Stay on chalk@4, node-fetch@2, got@11, axios@1 in github-script workflows — these are the last CJS-compatible majors'
|
|
105
|
+
docs:
|
|
106
|
+
- url: 'https://github.com/actions/github-script/issues/457'
|
|
107
|
+
label: 'actions/github-script#457: Feature request — ESM support (open, 30+ reactions)'
|
|
108
|
+
- url: 'https://nodejs.org/api/esm.html#interoperability-with-commonjs'
|
|
109
|
+
label: 'Node.js Docs — ESM and CommonJS interoperability'
|
|
110
|
+
- url: 'https://github.com/actions/github-script#use-scripts-with-jsdoc-support'
|
|
111
|
+
label: 'actions/github-script — README: use scripts with JSDoc support'
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
id: known-unsolved-060
|
|
2
|
+
title: 'Job outputs are strings only — arrays, objects, and booleans must be manually JSON-serialized'
|
|
3
|
+
category: known-unsolved
|
|
4
|
+
severity: limitation
|
|
5
|
+
tags:
|
|
6
|
+
- job-outputs
|
|
7
|
+
- outputs
|
|
8
|
+
- string
|
|
9
|
+
- fromJSON
|
|
10
|
+
- array
|
|
11
|
+
- known-limitation
|
|
12
|
+
- GITHUB_OUTPUT
|
|
13
|
+
patterns:
|
|
14
|
+
- regex: 'GITHUB_OUTPUT'
|
|
15
|
+
flags: 'i'
|
|
16
|
+
- regex: 'needs\.[a-z_-]+\.outputs\.'
|
|
17
|
+
flags: 'i'
|
|
18
|
+
- regex: 'fromJSON\s*\('
|
|
19
|
+
flags: 'i'
|
|
20
|
+
error_messages:
|
|
21
|
+
- '(no runtime error — arrays or objects written to GITHUB_OUTPUT are silently converted to strings like "Array" or "[object Object]" if not JSON-serialized before writing)'
|
|
22
|
+
root_cause: |
|
|
23
|
+
GitHub Actions job outputs use the GITHUB_OUTPUT file protocol which only supports
|
|
24
|
+
string key-value pairs. There is no native type system for job outputs — every
|
|
25
|
+
value passed through GITHUB_OUTPUT is stored and transmitted as a plain string,
|
|
26
|
+
regardless of the producing language or shell.
|
|
27
|
+
|
|
28
|
+
This becomes a problem in several common scenarios:
|
|
29
|
+
|
|
30
|
+
1. Passing arrays: A bash array or newline-separated list written to GITHUB_OUTPUT
|
|
31
|
+
becomes a space-separated string or a literal "[array]" — not a JSON array.
|
|
32
|
+
Downstream matrix.include: fromJSON() fails or produces a single-element matrix.
|
|
33
|
+
|
|
34
|
+
2. Passing objects: A JSON object must be written as a compact single-line string.
|
|
35
|
+
Multi-line JSON written to GITHUB_OUTPUT breaks the key=value line format and
|
|
36
|
+
corrupts the output, causing a parse error or silently reading only the first
|
|
37
|
+
line as the value.
|
|
38
|
+
|
|
39
|
+
3. Boolean comparisons: Outputs written as "true" or "false" are strings, not
|
|
40
|
+
booleans. Comparing ${{ needs.x.outputs.flag == true }} (boolean literal)
|
|
41
|
+
silently evaluates to false; only ${{ needs.x.outputs.flag == 'true' }} (string)
|
|
42
|
+
or ${{ fromJSON(needs.x.outputs.flag) }} works correctly.
|
|
43
|
+
|
|
44
|
+
This is a known platform limitation. The GitHub Actions team has acknowledged the
|
|
45
|
+
string-only constraint in multiple community discussions. There is no native typed
|
|
46
|
+
output support on the public roadmap as of mid-2026, and no timeline has been given
|
|
47
|
+
for adding array or object output types.
|
|
48
|
+
fix: |
|
|
49
|
+
Serialize arrays and objects to compact single-line JSON before writing to
|
|
50
|
+
GITHUB_OUTPUT, then deserialize with fromJSON() in consuming jobs.
|
|
51
|
+
|
|
52
|
+
Key rule: use jq -c to produce compact (one-line) JSON — multi-line JSON
|
|
53
|
+
embedded in GITHUB_OUTPUT silently truncates at the first newline.
|
|
54
|
+
|
|
55
|
+
For booleans: write the literal string "true"/"false" and compare with == 'true'
|
|
56
|
+
in if: conditions, or wrap with fromJSON() to get a native boolean for contexts
|
|
57
|
+
that require one (e.g., matrix include).
|
|
58
|
+
fix_code:
|
|
59
|
+
- language: yaml
|
|
60
|
+
label: 'Write and consume a JSON array as a job output'
|
|
61
|
+
code: |
|
|
62
|
+
jobs:
|
|
63
|
+
generate-matrix:
|
|
64
|
+
runs-on: ubuntu-latest
|
|
65
|
+
outputs:
|
|
66
|
+
targets: ${{ steps.set-matrix.outputs.targets }}
|
|
67
|
+
steps:
|
|
68
|
+
- id: set-matrix
|
|
69
|
+
run: |
|
|
70
|
+
# Use jq -c for compact single-line JSON — multi-line breaks GITHUB_OUTPUT
|
|
71
|
+
TARGETS=$(echo '[{"env":"staging"},{"env":"prod"}]' | jq -c .)
|
|
72
|
+
echo "targets=$TARGETS" >> "$GITHUB_OUTPUT"
|
|
73
|
+
|
|
74
|
+
deploy:
|
|
75
|
+
needs: generate-matrix
|
|
76
|
+
strategy:
|
|
77
|
+
matrix:
|
|
78
|
+
target: ${{ fromJSON(needs.generate-matrix.outputs.targets) }}
|
|
79
|
+
runs-on: ubuntu-latest
|
|
80
|
+
steps:
|
|
81
|
+
- run: echo "Deploying to ${{ matrix.target.env }}"
|
|
82
|
+
- language: yaml
|
|
83
|
+
label: 'Boolean output — write as string, compare correctly downstream'
|
|
84
|
+
code: |
|
|
85
|
+
jobs:
|
|
86
|
+
check-changes:
|
|
87
|
+
runs-on: ubuntu-latest
|
|
88
|
+
outputs:
|
|
89
|
+
has_changes: ${{ steps.diff.outputs.has_changes }}
|
|
90
|
+
steps:
|
|
91
|
+
- id: diff
|
|
92
|
+
run: |
|
|
93
|
+
if git diff --quiet HEAD~1; then
|
|
94
|
+
echo "has_changes=false" >> "$GITHUB_OUTPUT"
|
|
95
|
+
else
|
|
96
|
+
echo "has_changes=true" >> "$GITHUB_OUTPUT"
|
|
97
|
+
fi
|
|
98
|
+
|
|
99
|
+
build:
|
|
100
|
+
needs: check-changes
|
|
101
|
+
# Compare as string literal — NOT: == true (boolean comparison silently fails)
|
|
102
|
+
if: needs.check-changes.outputs.has_changes == 'true'
|
|
103
|
+
runs-on: ubuntu-latest
|
|
104
|
+
steps:
|
|
105
|
+
- run: make build
|
|
106
|
+
- language: yaml
|
|
107
|
+
label: 'Multi-line JSON — always compact with jq -c before writing'
|
|
108
|
+
code: |
|
|
109
|
+
jobs:
|
|
110
|
+
gather-config:
|
|
111
|
+
runs-on: ubuntu-latest
|
|
112
|
+
outputs:
|
|
113
|
+
config: ${{ steps.read-config.outputs.config }}
|
|
114
|
+
steps:
|
|
115
|
+
- uses: actions/checkout@v4
|
|
116
|
+
- id: read-config
|
|
117
|
+
run: |
|
|
118
|
+
# jq -c converts any JSON (even pretty-printed) to a single compact line
|
|
119
|
+
CONFIG=$(cat deploy-config.json | jq -c .)
|
|
120
|
+
echo "config=$CONFIG" >> "$GITHUB_OUTPUT"
|
|
121
|
+
|
|
122
|
+
deploy:
|
|
123
|
+
needs: gather-config
|
|
124
|
+
runs-on: ubuntu-latest
|
|
125
|
+
steps:
|
|
126
|
+
- run: |
|
|
127
|
+
CONFIG='${{ needs.gather-config.outputs.config }}'
|
|
128
|
+
echo "Region: $(echo "$CONFIG" | jq -r .region)"
|
|
129
|
+
prevention:
|
|
130
|
+
- 'Always pipe JSON values through jq -c before writing to GITHUB_OUTPUT — compact single-line format prevents silent value truncation at embedded newlines'
|
|
131
|
+
- 'Compare boolean-string outputs with == ''true'' (string) not == true (boolean) in if: conditions; or use fromJSON() to convert to a native boolean'
|
|
132
|
+
- 'Never write a raw bash array to GITHUB_OUTPUT — it expands to a space-separated string; always serialize through jq -c first'
|
|
133
|
+
- 'Add a debug step that prints the raw output value using cat $GITHUB_OUTPUT before a consuming job runs, to confirm the serialized form looks correct'
|
|
134
|
+
docs:
|
|
135
|
+
- url: 'https://docs.github.com/en/actions/writing-workflows/choosing-what-your-workflow-does/passing-information-between-jobs'
|
|
136
|
+
label: 'GitHub Docs: Passing information between jobs'
|
|
137
|
+
- url: 'https://docs.github.com/en/actions/writing-workflows/choosing-what-your-workflow-does/workflow-commands-for-github-actions#setting-an-output-parameter'
|
|
138
|
+
label: 'GitHub Docs: Setting an output parameter (GITHUB_OUTPUT)'
|
|
139
|
+
- url: 'https://github.com/orgs/community/discussions/17245'
|
|
140
|
+
label: 'GitHub Community: Job outputs only support strings — feature request for typed outputs'
|
|
141
|
+
- url: 'https://docs.github.com/en/actions/writing-workflows/choosing-what-your-workflow-does/evaluate-expressions-in-workflows-and-actions#fromjson'
|
|
142
|
+
label: 'GitHub Docs: fromJSON expression function'
|