@htekdev/actions-debugger 1.0.84 → 1.0.86
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/docker-buildx-gha-cache-mode-min-partial-layers.yml +91 -0
- package/errors/caching-artifacts/upload-artifact-storage-quota-exceeded.yml +76 -0
- package/errors/concurrency-timing/schedule-cron-no-concurrency-run-overlap.yml +87 -0
- package/errors/permissions-auth/fine-grained-pat-resource-owner-mismatch.yml +75 -0
- package/errors/permissions-auth/github-token-no-cross-org-private-action-access.yml +95 -0
- package/errors/runner-environment/github-script-require-relative-path-cwd.yml +74 -0
- package/errors/silent-failures/checkout-path-github-workspace-unchanged.yml +91 -0
- package/errors/yaml-syntax/step-output-boolean-string-coercion-if-condition.yml +100 -0
- package/package.json +1 -1
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
id: caching-artifacts-049
|
|
2
|
+
title: 'Docker buildx GHA cache mode:min only caches final layer — rebuilds remain slow'
|
|
3
|
+
category: caching-artifacts
|
|
4
|
+
severity: silent-failure
|
|
5
|
+
tags:
|
|
6
|
+
- docker
|
|
7
|
+
- buildx
|
|
8
|
+
- gha-cache
|
|
9
|
+
- layer-cache
|
|
10
|
+
- mode-min
|
|
11
|
+
- silent-failure
|
|
12
|
+
patterns:
|
|
13
|
+
- regex: 'cache-to.*type=gha'
|
|
14
|
+
flags: 'i'
|
|
15
|
+
- regex: 'importing\s+cache\s+manifest\s+from\s+ghcr\.io'
|
|
16
|
+
flags: 'i'
|
|
17
|
+
- regex: 'exporting\s+cache\s+.*\s+type=gha'
|
|
18
|
+
flags: 'i'
|
|
19
|
+
error_messages:
|
|
20
|
+
- 'cache-to: type=gha'
|
|
21
|
+
- 'exporting cache to GitHub Actions Cache Service'
|
|
22
|
+
- 'CACHED [stage 2/4]'
|
|
23
|
+
root_cause: |
|
|
24
|
+
docker/build-push-action uses cache-to: type=gha with mode=min by default when
|
|
25
|
+
no explicit mode is specified. In min mode, BuildKit exports ONLY the layers of
|
|
26
|
+
the final image (the last build stage), discarding all intermediate layer cache
|
|
27
|
+
data. This means:
|
|
28
|
+
|
|
29
|
+
- Multi-stage Dockerfiles get no benefit from the GHA cache for base/dependency
|
|
30
|
+
stages (the expensive ones) — only the final artifact stage is cached.
|
|
31
|
+
- Subsequent runs re-execute all intermediate stages from scratch while the
|
|
32
|
+
cache appears to "work" (export/import steps succeed in the logs).
|
|
33
|
+
- Developers see 80-90% cache miss rates and slow builds despite believing the
|
|
34
|
+
GHA cache is active.
|
|
35
|
+
|
|
36
|
+
The silent aspect: the workflow logs show cache export and import succeeding,
|
|
37
|
+
and BuildKit reports "CACHED" for layers it finds, with no warning that intermediate
|
|
38
|
+
stages were excluded from the cache.
|
|
39
|
+
fix: |
|
|
40
|
+
Explicitly set mode=max on the cache-to option. This instructs BuildKit to export
|
|
41
|
+
ALL layer metadata — including every intermediate build stage — into the GHA cache.
|
|
42
|
+
Combined with cache-from: type=gha, all previously-built stages will be restored
|
|
43
|
+
on the next run.
|
|
44
|
+
|
|
45
|
+
Note: mode=max requires more GHA cache storage (subject to the 10 GB repository
|
|
46
|
+
limit). Use separate cache scopes per Dockerfile or build target to prevent
|
|
47
|
+
cross-contamination and LRU eviction pressure.
|
|
48
|
+
fix_code:
|
|
49
|
+
- language: yaml
|
|
50
|
+
label: 'Set mode=max for full layer caching in multi-stage builds'
|
|
51
|
+
code: |
|
|
52
|
+
- name: Build and push
|
|
53
|
+
uses: docker/build-push-action@v6
|
|
54
|
+
with:
|
|
55
|
+
context: .
|
|
56
|
+
push: true
|
|
57
|
+
tags: ghcr.io/${{ github.repository }}:latest
|
|
58
|
+
cache-from: type=gha,scope=main
|
|
59
|
+
cache-to: type=gha,scope=main,mode=max
|
|
60
|
+
- language: yaml
|
|
61
|
+
label: 'Use separate scopes per Dockerfile to isolate caches'
|
|
62
|
+
code: |
|
|
63
|
+
- name: Build API image
|
|
64
|
+
uses: docker/build-push-action@v6
|
|
65
|
+
with:
|
|
66
|
+
context: ./api
|
|
67
|
+
push: true
|
|
68
|
+
tags: ghcr.io/${{ github.repository }}/api:latest
|
|
69
|
+
cache-from: type=gha,scope=api
|
|
70
|
+
cache-to: type=gha,scope=api,mode=max
|
|
71
|
+
|
|
72
|
+
- name: Build Frontend image
|
|
73
|
+
uses: docker/build-push-action@v6
|
|
74
|
+
with:
|
|
75
|
+
context: ./frontend
|
|
76
|
+
push: true
|
|
77
|
+
tags: ghcr.io/${{ github.repository }}/frontend:latest
|
|
78
|
+
cache-from: type=gha,scope=frontend
|
|
79
|
+
cache-to: type=gha,scope=frontend,mode=max
|
|
80
|
+
prevention:
|
|
81
|
+
- 'Always explicitly set mode=max in cache-to: type=gha for multi-stage Dockerfiles'
|
|
82
|
+
- 'Use the scope= parameter to isolate caches per Dockerfile or build target'
|
|
83
|
+
- 'Monitor cache size with the GitHub Actions Cache API if approaching the 10 GB repository limit'
|
|
84
|
+
- 'Verify cache effectiveness by checking that intermediate build stages show CACHED in buildx output'
|
|
85
|
+
docs:
|
|
86
|
+
- url: 'https://docs.docker.com/build/cache/backends/gha/'
|
|
87
|
+
label: 'Docker BuildKit — GitHub Actions cache backend'
|
|
88
|
+
- url: 'https://github.com/docker/build-push-action#cache'
|
|
89
|
+
label: 'docker/build-push-action — Cache inputs'
|
|
90
|
+
- url: 'https://docs.github.com/en/actions/writing-workflows/choosing-what-your-workflow-does/caching-dependencies-to-speed-up-workflows#usage-limits-and-eviction-policy'
|
|
91
|
+
label: 'GitHub Actions — Cache usage limits and eviction policy'
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
id: caching-artifacts-050
|
|
2
|
+
title: "upload-artifact@v4 fails when artifact storage quota is exceeded"
|
|
3
|
+
category: caching-artifacts
|
|
4
|
+
severity: error
|
|
5
|
+
tags:
|
|
6
|
+
- upload-artifact
|
|
7
|
+
- storage-quota
|
|
8
|
+
- v4
|
|
9
|
+
- billing
|
|
10
|
+
- artifact-cleanup
|
|
11
|
+
patterns:
|
|
12
|
+
- regex: 'Artifact storage quota has been hit'
|
|
13
|
+
flags: i
|
|
14
|
+
- regex: 'unable to upload any new artifacts'
|
|
15
|
+
flags: i
|
|
16
|
+
- regex: 'storage limit.*exceeded'
|
|
17
|
+
flags: i
|
|
18
|
+
error_messages:
|
|
19
|
+
- "Artifact storage quota has been hit, unable to upload any new artifacts. Please remove some old artifacts or increase storage for the repo."
|
|
20
|
+
root_cause: |
|
|
21
|
+
GitHub Actions artifact storage has per-account limits (500 MB for free plans, 2 GB for Pro,
|
|
22
|
+
50 GB for Teams, and custom limits for Enterprise). Unlike actions/upload-artifact@v3 which
|
|
23
|
+
used a legacy backend, v4 strictly enforces storage quotas and fails hard when the limit
|
|
24
|
+
is exceeded. Old artifacts from previous workflow runs accumulate over time and are not
|
|
25
|
+
automatically purged unless a retention policy is set. Once the quota is hit, all subsequent
|
|
26
|
+
artifact uploads fail immediately with no partial upload.
|
|
27
|
+
fix: |
|
|
28
|
+
1. Set retention-days on all upload-artifact steps to automatically expire old artifacts.
|
|
29
|
+
2. Delete old artifacts programmatically using the GitHub REST API via actions/github-script.
|
|
30
|
+
3. Increase artifact and log storage in GitHub billing settings (Org/User Settings -> Billing -> Storage).
|
|
31
|
+
4. Audit artifact size — only upload what is necessary for debugging or downstream jobs.
|
|
32
|
+
fix_code:
|
|
33
|
+
- language: yaml
|
|
34
|
+
label: "Set retention-days to auto-expire artifacts"
|
|
35
|
+
code: |
|
|
36
|
+
- name: Upload build artifacts
|
|
37
|
+
uses: actions/upload-artifact@v4
|
|
38
|
+
with:
|
|
39
|
+
name: build-output
|
|
40
|
+
path: dist/
|
|
41
|
+
retention-days: 7 # auto-delete after 7 days; default is 90
|
|
42
|
+
|
|
43
|
+
- language: yaml
|
|
44
|
+
label: "Delete artifacts older than 30 days via GitHub API script"
|
|
45
|
+
code: |
|
|
46
|
+
- name: Clean up old artifacts
|
|
47
|
+
uses: actions/github-script@v7
|
|
48
|
+
with:
|
|
49
|
+
script: |
|
|
50
|
+
const cutoff = new Date();
|
|
51
|
+
cutoff.setDate(cutoff.getDate() - 30);
|
|
52
|
+
const artifacts = await github.paginate(
|
|
53
|
+
github.rest.actions.listArtifactsForRepo,
|
|
54
|
+
{ owner: context.repo.owner, repo: context.repo.repo, per_page: 100 }
|
|
55
|
+
);
|
|
56
|
+
for (const artifact of artifacts) {
|
|
57
|
+
if (new Date(artifact.created_at) < cutoff) {
|
|
58
|
+
await github.rest.actions.deleteArtifact({
|
|
59
|
+
owner: context.repo.owner,
|
|
60
|
+
repo: context.repo.repo,
|
|
61
|
+
artifact_id: artifact.id,
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
prevention:
|
|
66
|
+
- "Always set retention-days on upload-artifact steps — default is 90 days which fills storage quickly"
|
|
67
|
+
- "Upload only the minimum files needed for debugging or downstream jobs, not entire build directories"
|
|
68
|
+
- "Add a weekly scheduled workflow to delete artifacts older than your retention window"
|
|
69
|
+
- "Monitor storage usage under GitHub Settings -> Billing & plans -> Storage"
|
|
70
|
+
docs:
|
|
71
|
+
- url: "https://docs.github.com/en/actions/writing-workflows/choosing-what-your-workflow-does/storing-workflow-data-as-artifacts#configuring-a-custom-artifact-retention-period"
|
|
72
|
+
label: "GitHub Docs: Custom artifact retention period"
|
|
73
|
+
- url: "https://docs.github.com/en/billing/managing-billing-for-your-products/managing-billing-for-github-actions/about-billing-for-github-actions#included-storage-and-minutes"
|
|
74
|
+
label: "GitHub Docs: Included storage and minutes"
|
|
75
|
+
- url: "https://github.com/actions/upload-artifact/issues/577"
|
|
76
|
+
label: "actions/upload-artifact#577: Storage quota exceeded on v4"
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
id: concurrency-timing-043
|
|
2
|
+
title: 'Scheduled cron jobs overlap when no concurrency group is configured'
|
|
3
|
+
category: concurrency-timing
|
|
4
|
+
severity: silent-failure
|
|
5
|
+
tags:
|
|
6
|
+
- schedule
|
|
7
|
+
- cron
|
|
8
|
+
- overlap
|
|
9
|
+
- race-condition
|
|
10
|
+
- concurrency
|
|
11
|
+
- data-corruption
|
|
12
|
+
patterns:
|
|
13
|
+
- regex: 'on:\s*schedule'
|
|
14
|
+
flags: 'i'
|
|
15
|
+
- regex: 'triggered by:\s*schedule'
|
|
16
|
+
flags: 'i'
|
|
17
|
+
error_messages:
|
|
18
|
+
- 'Multiple workflow runs triggered by schedule are running concurrently'
|
|
19
|
+
- 'triggered by: schedule'
|
|
20
|
+
root_cause: |
|
|
21
|
+
When a scheduled workflow has no concurrency group configured, GitHub Actions
|
|
22
|
+
starts a new run at each scheduled time regardless of whether a previous run is
|
|
23
|
+
still in progress. Two conditions make this especially common:
|
|
24
|
+
|
|
25
|
+
1. The job takes longer than the cron interval (e.g., a 10-minute job on a
|
|
26
|
+
every-5-minute schedule).
|
|
27
|
+
2. GitHub's scheduler fires queued runs in rapid succession after a platform
|
|
28
|
+
outage or maintenance window, catching up on missed intervals.
|
|
29
|
+
|
|
30
|
+
Both scenarios result in multiple simultaneous runs sharing external resources—
|
|
31
|
+
databases, S3 buckets, deployment targets, or branch state—causing race
|
|
32
|
+
conditions, duplicate data writes, and data corruption. No error is surfaced
|
|
33
|
+
in the workflow logs; the runs both appear green while the downstream system
|
|
34
|
+
silently degrades.
|
|
35
|
+
fix: |
|
|
36
|
+
Add a concurrency group to the scheduled workflow. The correct cancel-in-progress
|
|
37
|
+
value depends on whether the job is idempotent:
|
|
38
|
+
|
|
39
|
+
- Non-idempotent jobs (database migrations, state mutations): set
|
|
40
|
+
cancel-in-progress: false to queue runs sequentially.
|
|
41
|
+
- Idempotent jobs (report generation, cache refreshes): set
|
|
42
|
+
cancel-in-progress: true to drop stale queued runs and keep only the latest.
|
|
43
|
+
fix_code:
|
|
44
|
+
- language: yaml
|
|
45
|
+
label: 'Queue scheduled runs for non-idempotent jobs (e.g., database migrations)'
|
|
46
|
+
code: |
|
|
47
|
+
on:
|
|
48
|
+
schedule:
|
|
49
|
+
- cron: '*/5 * * * *'
|
|
50
|
+
|
|
51
|
+
concurrency:
|
|
52
|
+
group: ${{ github.workflow }}-scheduled
|
|
53
|
+
cancel-in-progress: false
|
|
54
|
+
|
|
55
|
+
jobs:
|
|
56
|
+
migrate:
|
|
57
|
+
runs-on: ubuntu-latest
|
|
58
|
+
steps:
|
|
59
|
+
- uses: actions/checkout@v4
|
|
60
|
+
- run: ./scripts/db-migrate.sh
|
|
61
|
+
- language: yaml
|
|
62
|
+
label: 'Cancel stale runs for idempotent jobs (e.g., report generation)'
|
|
63
|
+
code: |
|
|
64
|
+
on:
|
|
65
|
+
schedule:
|
|
66
|
+
- cron: '*/5 * * * *'
|
|
67
|
+
|
|
68
|
+
concurrency:
|
|
69
|
+
group: ${{ github.workflow }}-scheduled
|
|
70
|
+
cancel-in-progress: true
|
|
71
|
+
|
|
72
|
+
jobs:
|
|
73
|
+
report:
|
|
74
|
+
runs-on: ubuntu-latest
|
|
75
|
+
steps:
|
|
76
|
+
- uses: actions/checkout@v4
|
|
77
|
+
- run: ./scripts/generate-report.sh
|
|
78
|
+
prevention:
|
|
79
|
+
- 'Add a concurrency group to every workflow with an on.schedule trigger'
|
|
80
|
+
- 'For jobs longer than the cron interval, always use cancel-in-progress: false to serialize execution'
|
|
81
|
+
- 'Monitor the Actions tab for multiple simultaneous scheduled runs as an early warning sign'
|
|
82
|
+
- 'Use actionlint to audit workflows that have on.schedule but no concurrency group'
|
|
83
|
+
docs:
|
|
84
|
+
- url: 'https://docs.github.com/en/actions/using-jobs/using-concurrency'
|
|
85
|
+
label: 'GitHub Actions — Using concurrency'
|
|
86
|
+
- url: 'https://docs.github.com/en/actions/writing-workflows/choosing-when-your-workflow-runs/events-that-trigger-workflows#schedule'
|
|
87
|
+
label: 'GitHub Actions — Schedule event'
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
id: permissions-auth-050
|
|
2
|
+
title: "Fine-grained PAT with wrong resource owner causes 'repository not found' in checkout"
|
|
3
|
+
category: permissions-auth
|
|
4
|
+
severity: error
|
|
5
|
+
tags:
|
|
6
|
+
- fine-grained-pat
|
|
7
|
+
- checkout
|
|
8
|
+
- resource-owner
|
|
9
|
+
- PAT
|
|
10
|
+
- authentication
|
|
11
|
+
patterns:
|
|
12
|
+
- regex: 'repository.*not found'
|
|
13
|
+
flags: i
|
|
14
|
+
- regex: 'remote: Repository not found'
|
|
15
|
+
flags: i
|
|
16
|
+
- regex: 'fatal: unable to access.*403'
|
|
17
|
+
flags: i
|
|
18
|
+
error_messages:
|
|
19
|
+
- "fatal: repository 'https://github.com/org/repo.git/' not found"
|
|
20
|
+
- "remote: Repository not found."
|
|
21
|
+
- "Error: fatal: unable to access 'https://github.com/org/repo.git/': The requested URL returned error: 403"
|
|
22
|
+
root_cause: |
|
|
23
|
+
Fine-grained personal access tokens (PATs) require selecting a resource owner when created —
|
|
24
|
+
either your personal account or a specific organization. A token scoped to a personal account
|
|
25
|
+
(e.g., user alice) cannot authenticate to repositories owned by an organization (e.g., myorg),
|
|
26
|
+
even if alice is a member of myorg with full access. Attempting to use such a PAT in
|
|
27
|
+
actions/checkout, actions/github-script REST calls, or any GitHub API call targeting the
|
|
28
|
+
organization's repos results in a misleading "repository not found" or HTTP 403 error. The
|
|
29
|
+
repository is accessible through the web UI because browser sessions use OAuth-based auth —
|
|
30
|
+
but the fine-grained PAT token is strictly limited to its configured resource owner scope.
|
|
31
|
+
Classic PATs (without granular resource scope) do not have this restriction, which is why
|
|
32
|
+
the problem only appears after migrating to fine-grained PATs.
|
|
33
|
+
fix: |
|
|
34
|
+
Regenerate the fine-grained PAT selecting the correct resource owner — the organization or
|
|
35
|
+
user account that owns the target repository. If you need to access repositories across
|
|
36
|
+
multiple organizations, create one PAT per organization, or use a GitHub App installation
|
|
37
|
+
token which supports cross-repo access without resource-owner restrictions.
|
|
38
|
+
fix_code:
|
|
39
|
+
- language: yaml
|
|
40
|
+
label: "Checkout org repo — PAT must have org as resource owner"
|
|
41
|
+
code: |
|
|
42
|
+
# The secret ORG_SCOPED_PAT must be a fine-grained PAT created with
|
|
43
|
+
# Resource owner = myorg (not your personal account)
|
|
44
|
+
- uses: actions/checkout@v4
|
|
45
|
+
with:
|
|
46
|
+
repository: myorg/private-repo
|
|
47
|
+
token: ${{ secrets.ORG_SCOPED_PAT }}
|
|
48
|
+
|
|
49
|
+
- language: yaml
|
|
50
|
+
label: "Use a GitHub App installation token to avoid resource-owner scope issues"
|
|
51
|
+
code: |
|
|
52
|
+
- name: Generate app installation token
|
|
53
|
+
id: app-token
|
|
54
|
+
uses: actions/create-github-app-token@v2
|
|
55
|
+
with:
|
|
56
|
+
app-id: ${{ vars.APP_ID }}
|
|
57
|
+
private-key: ${{ secrets.APP_PRIVATE_KEY }}
|
|
58
|
+
owner: myorg
|
|
59
|
+
|
|
60
|
+
- uses: actions/checkout@v4
|
|
61
|
+
with:
|
|
62
|
+
repository: myorg/private-repo
|
|
63
|
+
token: ${{ steps.app-token.outputs.token }}
|
|
64
|
+
prevention:
|
|
65
|
+
- "When creating a fine-grained PAT, verify the Resource owner dropdown matches the organization or user that OWNS the target repository"
|
|
66
|
+
- "Name secrets descriptively: ORG_SCOPED_PAT vs USER_SCOPED_PAT to avoid mixing tokens with different owners"
|
|
67
|
+
- "Prefer GitHub App installation tokens for multi-repo or cross-org access — they have no resource-owner scoping restriction"
|
|
68
|
+
- "Classic PATs (repo scope) remain an option if fine-grained token resource-owner scoping is causing confusion"
|
|
69
|
+
docs:
|
|
70
|
+
- url: "https://docs.github.com/en/authentication/keeping-your-account-and-data-secure/managing-your-personal-access-tokens#creating-a-fine-grained-personal-access-token"
|
|
71
|
+
label: "GitHub Docs: Creating a fine-grained personal access token"
|
|
72
|
+
- url: "https://docs.github.com/en/authentication/keeping-your-account-and-data-secure/managing-your-personal-access-tokens#about-fine-grained-personal-access-tokens"
|
|
73
|
+
label: "GitHub Docs: About fine-grained PATs and resource owner scope"
|
|
74
|
+
- url: "https://github.com/actions/checkout?tab=readme-ov-file#checkout-a-different-private-repository"
|
|
75
|
+
label: "actions/checkout: Checkout a different private repository"
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
id: permissions-auth-049
|
|
2
|
+
title: 'GITHUB_TOKEN cannot resolve private actions from a different organization'
|
|
3
|
+
category: permissions-auth
|
|
4
|
+
severity: error
|
|
5
|
+
tags:
|
|
6
|
+
- github-token
|
|
7
|
+
- cross-org
|
|
8
|
+
- private-action
|
|
9
|
+
- enterprise
|
|
10
|
+
- repository-not-found
|
|
11
|
+
patterns:
|
|
12
|
+
- regex: 'Unable to resolve action .* repository not found'
|
|
13
|
+
flags: 'i'
|
|
14
|
+
- regex: 'Error: Unable to resolve action `[^`]+`'
|
|
15
|
+
flags: 'i'
|
|
16
|
+
- regex: 'remote: Repository not found'
|
|
17
|
+
flags: 'i'
|
|
18
|
+
error_messages:
|
|
19
|
+
- "Error: Unable to resolve action `other-org/private-action@v1`, repository not found."
|
|
20
|
+
- "Error: remote: Repository not found."
|
|
21
|
+
- "fatal: repository 'https://github.com/other-org/private-action/' not found"
|
|
22
|
+
root_cause: |
|
|
23
|
+
GITHUB_TOKEN is automatically scoped to the repository where the workflow runs.
|
|
24
|
+
It cannot authenticate against repositories in a different GitHub organization,
|
|
25
|
+
even when both organizations are in the same GitHub Enterprise instance.
|
|
26
|
+
|
|
27
|
+
When a workflow references a private action from a different org
|
|
28
|
+
(e.g., uses: other-org/my-action@v1), the Actions runner attempts to clone
|
|
29
|
+
that action repository during the job setup phase using GITHUB_TOKEN. The
|
|
30
|
+
clone fails with "repository not found" because the token has no cross-org
|
|
31
|
+
read access. This is not a permissions: block misconfiguration — no amount of
|
|
32
|
+
permissions granted in the workflow YAML will fix it, since the token is
|
|
33
|
+
fundamentally scoped to the calling org.
|
|
34
|
+
|
|
35
|
+
This also affects actions/checkout when checking out code from a private repo
|
|
36
|
+
in another org unless a suitable alternative token is provided.
|
|
37
|
+
fix: |
|
|
38
|
+
Choose the appropriate strategy based on your setup:
|
|
39
|
+
|
|
40
|
+
1. Make the action repository public — simplest if the action contains no secrets.
|
|
41
|
+
2. Vendor the action into your organization — copy the action code into a repo
|
|
42
|
+
in the same org or into .github/actions/ in the calling repo and reference it
|
|
43
|
+
as a local path action.
|
|
44
|
+
3. Use a GitHub App installation token with cross-org permissions for
|
|
45
|
+
actions/checkout calls to cross-org repos (does NOT fix job-level uses: loading).
|
|
46
|
+
4. Use a Personal Access Token (PAT) with access to both orgs for checkout
|
|
47
|
+
(same limitation: cannot fix uses: loading, only checkout steps).
|
|
48
|
+
|
|
49
|
+
Note: The uses: field at the job-steps level (for external actions) is resolved
|
|
50
|
+
before the workflow starts executing, so token injection via steps is not possible
|
|
51
|
+
for the action loading stage — vendoring or making the action public is the only
|
|
52
|
+
reliable fix for job-level uses:.
|
|
53
|
+
fix_code:
|
|
54
|
+
- language: yaml
|
|
55
|
+
label: 'Vendor private action as a local path action'
|
|
56
|
+
code: |
|
|
57
|
+
# Copy the action code to .github/actions/my-action/ in your repo.
|
|
58
|
+
# Then reference it as a local action — no cross-org token required:
|
|
59
|
+
jobs:
|
|
60
|
+
build:
|
|
61
|
+
runs-on: ubuntu-latest
|
|
62
|
+
steps:
|
|
63
|
+
- uses: actions/checkout@v4
|
|
64
|
+
- uses: ./.github/actions/my-action
|
|
65
|
+
with:
|
|
66
|
+
param: value
|
|
67
|
+
- language: yaml
|
|
68
|
+
label: 'Use GitHub App token for cross-org repository checkout (not uses: loading)'
|
|
69
|
+
code: |
|
|
70
|
+
jobs:
|
|
71
|
+
cross-org-checkout:
|
|
72
|
+
runs-on: ubuntu-latest
|
|
73
|
+
steps:
|
|
74
|
+
- uses: actions/create-github-app-token@v2
|
|
75
|
+
id: app-token
|
|
76
|
+
with:
|
|
77
|
+
app-id: ${{ vars.APP_ID }}
|
|
78
|
+
private-key: ${{ secrets.APP_PRIVATE_KEY }}
|
|
79
|
+
owner: other-org
|
|
80
|
+
- uses: actions/checkout@v4
|
|
81
|
+
with:
|
|
82
|
+
repository: other-org/private-repo
|
|
83
|
+
token: ${{ steps.app-token.outputs.token }}
|
|
84
|
+
prevention:
|
|
85
|
+
- 'Prefer public actions for shared tooling to avoid cross-org token scope issues'
|
|
86
|
+
- 'Vendor private cross-org actions into .github/actions/ in the calling repo'
|
|
87
|
+
- 'Create an internal shared-actions org accessible to all teams where GITHUB_TOKEN works'
|
|
88
|
+
- 'Document which repos depend on cross-org private actions and track them for access reviews'
|
|
89
|
+
docs:
|
|
90
|
+
- url: 'https://docs.github.com/en/actions/security-for-github-actions/security-guides/automatic-token-authentication#permissions-for-the-github_token'
|
|
91
|
+
label: 'GitHub Actions — Automatic token authentication permissions'
|
|
92
|
+
- url: 'https://docs.github.com/en/actions/sharing-automations/creating-actions/sharing-actions-and-workflows-with-your-organization'
|
|
93
|
+
label: 'GitHub Actions — Sharing actions with your organization'
|
|
94
|
+
- url: 'https://docs.github.com/en/actions/writing-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsuses'
|
|
95
|
+
label: 'GitHub Actions — Workflow syntax for uses'
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
id: runner-environment-150
|
|
2
|
+
title: "actions/github-script relative require() fails — CWD is not GITHUB_WORKSPACE"
|
|
3
|
+
category: runner-environment
|
|
4
|
+
severity: error
|
|
5
|
+
tags:
|
|
6
|
+
- github-script
|
|
7
|
+
- require
|
|
8
|
+
- nodejs
|
|
9
|
+
- working-directory
|
|
10
|
+
- module-not-found
|
|
11
|
+
patterns:
|
|
12
|
+
- regex: 'Cannot find module'
|
|
13
|
+
flags: i
|
|
14
|
+
- regex: 'MODULE_NOT_FOUND'
|
|
15
|
+
flags: ''
|
|
16
|
+
error_messages:
|
|
17
|
+
- "Error: Cannot find module './my-helper'"
|
|
18
|
+
- "Error: Cannot find module '../utils/helper'"
|
|
19
|
+
- "{ code: 'MODULE_NOT_FOUND' }"
|
|
20
|
+
root_cause: |
|
|
21
|
+
The actions/github-script action evaluates the script: block in a Node.js context where the
|
|
22
|
+
current working directory (CWD) is a temporary internal directory used by the action runtime —
|
|
23
|
+
NOT $GITHUB_WORKSPACE. As a result, relative require() calls like require('./helpers/my-util')
|
|
24
|
+
fail with "Cannot find module" even when the file exists in the repository workspace. This
|
|
25
|
+
surprises developers who assume the script evaluates from the repository root directory.
|
|
26
|
+
Note: This is distinct from missing npm packages (runner-environment-136) — the file exists
|
|
27
|
+
on disk but is not found because Node.js resolves the relative path from the wrong base directory.
|
|
28
|
+
fix: |
|
|
29
|
+
Construct an absolute path using process.env.GITHUB_WORKSPACE before calling require(). The
|
|
30
|
+
GITHUB_WORKSPACE environment variable is always set to the repository root in hosted runners.
|
|
31
|
+
Alternatively, use the recommended pattern from the actions/github-script docs: point to the
|
|
32
|
+
absolute path via a template literal.
|
|
33
|
+
fix_code:
|
|
34
|
+
- language: yaml
|
|
35
|
+
label: "Use absolute path via process.env.GITHUB_WORKSPACE"
|
|
36
|
+
code: |
|
|
37
|
+
- uses: actions/github-script@v7
|
|
38
|
+
with:
|
|
39
|
+
script: |
|
|
40
|
+
const myHelper = require(`${process.env.GITHUB_WORKSPACE}/scripts/my-helper.js`);
|
|
41
|
+
await myHelper.run(github, context);
|
|
42
|
+
|
|
43
|
+
- language: yaml
|
|
44
|
+
label: "Pass workspace as env var for explicit clarity"
|
|
45
|
+
code: |
|
|
46
|
+
- uses: actions/github-script@v7
|
|
47
|
+
env:
|
|
48
|
+
WORKSPACE: ${{ github.workspace }}
|
|
49
|
+
with:
|
|
50
|
+
script: |
|
|
51
|
+
const helper = require(`${process.env.WORKSPACE}/scripts/helper.js`);
|
|
52
|
+
const result = helper.compute();
|
|
53
|
+
core.setOutput('result', result);
|
|
54
|
+
|
|
55
|
+
- language: yaml
|
|
56
|
+
label: "Use path.resolve for cross-platform safety"
|
|
57
|
+
code: |
|
|
58
|
+
- uses: actions/github-script@v7
|
|
59
|
+
with:
|
|
60
|
+
script: |
|
|
61
|
+
const path = require('path');
|
|
62
|
+
const utils = require(path.resolve(process.env.GITHUB_WORKSPACE, 'lib', 'utils.js'));
|
|
63
|
+
utils.run();
|
|
64
|
+
prevention:
|
|
65
|
+
- "Never use relative require() paths in github-script — always construct absolute paths with process.env.GITHUB_WORKSPACE"
|
|
66
|
+
- "Print process.cwd() in debug runs to confirm the actual CWD — it will not be your repo root"
|
|
67
|
+
- "For complex shared logic, consider a composite action or a dedicated JS action that has proper module resolution"
|
|
68
|
+
docs:
|
|
69
|
+
- url: "https://github.com/actions/github-script?tab=readme-ov-file#run-a-separate-file"
|
|
70
|
+
label: "actions/github-script: Run a separate file (recommended pattern)"
|
|
71
|
+
- url: "https://docs.github.com/en/actions/writing-workflows/choosing-what-your-workflow-does/store-information-in-variables#default-environment-variables"
|
|
72
|
+
label: "GitHub Docs: GITHUB_WORKSPACE default environment variable"
|
|
73
|
+
- url: "https://github.com/actions/github-script/issues/390"
|
|
74
|
+
label: "actions/github-script#390: Cannot find module with relative path"
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
id: silent-failures-079
|
|
2
|
+
title: "actions/checkout path: input doesn't change GITHUB_WORKSPACE — subsequent steps use wrong directory"
|
|
3
|
+
category: silent-failures
|
|
4
|
+
severity: silent-failure
|
|
5
|
+
tags:
|
|
6
|
+
- checkout
|
|
7
|
+
- path
|
|
8
|
+
- GITHUB_WORKSPACE
|
|
9
|
+
- working-directory
|
|
10
|
+
- subdirectory
|
|
11
|
+
patterns:
|
|
12
|
+
- regex: 'No such file or directory'
|
|
13
|
+
flags: i
|
|
14
|
+
- regex: 'ENOENT.*no such file'
|
|
15
|
+
flags: i
|
|
16
|
+
error_messages:
|
|
17
|
+
- "No such file or directory"
|
|
18
|
+
- "ENOENT: no such file or directory"
|
|
19
|
+
root_cause: |
|
|
20
|
+
When actions/checkout is used with a path: input (e.g., path: app), the repository is
|
|
21
|
+
checked out into $GITHUB_WORKSPACE/app. However, the GITHUB_WORKSPACE environment variable
|
|
22
|
+
continues to point to the root workspace directory (/home/runner/work/repo-name/repo-name),
|
|
23
|
+
not to the path: subdirectory. Any subsequent run: steps that use ${{ github.workspace }}
|
|
24
|
+
or rely on the default working directory will NOT operate inside the checkout subdirectory.
|
|
25
|
+
This causes file-not-found errors that are hard to debug because the checkout step succeeds
|
|
26
|
+
and the files do exist — just not at the location subsequent steps expect. This is a
|
|
27
|
+
particularly common footgun when checking out multiple repositories into different subdirs.
|
|
28
|
+
fix: |
|
|
29
|
+
Explicitly specify working-directory on all run: steps that operate on the checked-out code,
|
|
30
|
+
OR set a job-level defaults.run.working-directory. Alternatively, avoid path: unless you
|
|
31
|
+
need multiple checkouts — the default checkout places files directly at $GITHUB_WORKSPACE.
|
|
32
|
+
fix_code:
|
|
33
|
+
- language: yaml
|
|
34
|
+
label: "Use working-directory to point to the checkout subdirectory"
|
|
35
|
+
code: |
|
|
36
|
+
- uses: actions/checkout@v4
|
|
37
|
+
with:
|
|
38
|
+
path: app
|
|
39
|
+
|
|
40
|
+
- name: Build
|
|
41
|
+
working-directory: ${{ github.workspace }}/app
|
|
42
|
+
run: npm install && npm run build
|
|
43
|
+
|
|
44
|
+
- language: yaml
|
|
45
|
+
label: "Set job-level default working-directory"
|
|
46
|
+
code: |
|
|
47
|
+
jobs:
|
|
48
|
+
build:
|
|
49
|
+
runs-on: ubuntu-latest
|
|
50
|
+
defaults:
|
|
51
|
+
run:
|
|
52
|
+
working-directory: ./app
|
|
53
|
+
steps:
|
|
54
|
+
- uses: actions/checkout@v4
|
|
55
|
+
with:
|
|
56
|
+
path: app
|
|
57
|
+
- run: npm install && npm run build
|
|
58
|
+
|
|
59
|
+
- language: yaml
|
|
60
|
+
label: "Multiple checkouts — use explicit paths for each"
|
|
61
|
+
code: |
|
|
62
|
+
steps:
|
|
63
|
+
- uses: actions/checkout@v4
|
|
64
|
+
with:
|
|
65
|
+
repository: myorg/frontend
|
|
66
|
+
path: frontend
|
|
67
|
+
|
|
68
|
+
- uses: actions/checkout@v4
|
|
69
|
+
with:
|
|
70
|
+
repository: myorg/backend
|
|
71
|
+
path: backend
|
|
72
|
+
|
|
73
|
+
- name: Build frontend
|
|
74
|
+
working-directory: frontend
|
|
75
|
+
run: npm ci && npm run build
|
|
76
|
+
|
|
77
|
+
- name: Build backend
|
|
78
|
+
working-directory: backend
|
|
79
|
+
run: go build ./...
|
|
80
|
+
prevention:
|
|
81
|
+
- "Prefer the default checkout (no path:) unless checking out multiple repos in the same job"
|
|
82
|
+
- "When path: is used, always add defaults.run.working-directory at the job level"
|
|
83
|
+
- "Never use ${{ github.workspace }} to reference files from a path:-redirected checkout without appending the path value"
|
|
84
|
+
- "Use echo $GITHUB_WORKSPACE and ls $GITHUB_WORKSPACE in debug steps to verify directory contents"
|
|
85
|
+
docs:
|
|
86
|
+
- url: "https://github.com/actions/checkout?tab=readme-ov-file#usage"
|
|
87
|
+
label: "actions/checkout: path input documentation"
|
|
88
|
+
- url: "https://docs.github.com/en/actions/writing-workflows/workflow-syntax-for-github-actions#jobsjob_iddefaultsrun"
|
|
89
|
+
label: "GitHub Docs: jobs.defaults.run.working-directory"
|
|
90
|
+
- url: "https://docs.github.com/en/actions/writing-workflows/choosing-what-your-workflow-does/store-information-in-variables#default-environment-variables"
|
|
91
|
+
label: "GitHub Docs: GITHUB_WORKSPACE default environment variable"
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
id: yaml-syntax-053
|
|
2
|
+
title: 'Step output boolean string coercion — comparing to bare true/false silently never matches'
|
|
3
|
+
category: yaml-syntax
|
|
4
|
+
severity: silent-failure
|
|
5
|
+
tags:
|
|
6
|
+
- step-outputs
|
|
7
|
+
- GITHUB_OUTPUT
|
|
8
|
+
- boolean
|
|
9
|
+
- string-coercion
|
|
10
|
+
- if-condition
|
|
11
|
+
- silent-failure
|
|
12
|
+
patterns:
|
|
13
|
+
- regex: 'outputs\.[a-z_][a-z0-9_]*\s*==\s*true\b'
|
|
14
|
+
flags: 'i'
|
|
15
|
+
- regex: 'outputs\.[a-z_][a-z0-9_]*\s*==\s*false\b'
|
|
16
|
+
flags: 'i'
|
|
17
|
+
- regex: 'needs\.[a-z_][a-z0-9_-]*\.outputs\.[a-z_]+ ==\s*(true|false)\b'
|
|
18
|
+
flags: 'i'
|
|
19
|
+
error_messages:
|
|
20
|
+
- "if: steps.check.outputs.changed == true"
|
|
21
|
+
- "if: needs.build.outputs.has_changes == false"
|
|
22
|
+
- "if: steps.detect.outputs.skip == true"
|
|
23
|
+
root_cause: |
|
|
24
|
+
All values written to GITHUB_OUTPUT (via echo "key=value" >> $GITHUB_OUTPUT) are
|
|
25
|
+
stored and transmitted as strings, regardless of the intended type. When a step
|
|
26
|
+
outputs a boolean-like value such as echo "changed=true" >> $GITHUB_OUTPUT, the
|
|
27
|
+
output steps.id.outputs.changed is the string 'true', not the boolean true.
|
|
28
|
+
|
|
29
|
+
In GitHub Actions expression syntax, type comparison is strict:
|
|
30
|
+
- 'true' == true → false (string 'true' does not equal boolean true)
|
|
31
|
+
- 'false' == false → false (string 'false' does not equal boolean false)
|
|
32
|
+
- '' == false → true (empty string does equal boolean false — a separate gotcha)
|
|
33
|
+
|
|
34
|
+
This means if: steps.check.outputs.changed == true silently evaluates to false
|
|
35
|
+
even when the output is the string "true", and the step is silently skipped with
|
|
36
|
+
no error. The workflow log shows the step as skipped with no indication that the
|
|
37
|
+
condition evaluation was the cause.
|
|
38
|
+
|
|
39
|
+
The same pattern applies to job outputs (needs.job.outputs.flag) passed between
|
|
40
|
+
jobs via the outputs: block.
|
|
41
|
+
fix: |
|
|
42
|
+
Always compare step and job outputs as strings using single-quoted string literals:
|
|
43
|
+
- For true check: == 'true'
|
|
44
|
+
- For false check: != 'true' (preferred over == 'false' because empty string also needs handling)
|
|
45
|
+
- For numeric outputs: use fromJSON() to parse before arithmetic comparison
|
|
46
|
+
|
|
47
|
+
For boolean output authors: document that consumers must compare as strings, or
|
|
48
|
+
use '1'/'0' string conventions to make the string nature explicit.
|
|
49
|
+
fix_code:
|
|
50
|
+
- language: yaml
|
|
51
|
+
label: 'Correct: compare step outputs as strings'
|
|
52
|
+
code: |
|
|
53
|
+
steps:
|
|
54
|
+
- id: check
|
|
55
|
+
run: echo "changed=true" >> $GITHUB_OUTPUT
|
|
56
|
+
|
|
57
|
+
# WRONG — silently skipped because 'true' != true (string != boolean)
|
|
58
|
+
# - if: steps.check.outputs.changed == true
|
|
59
|
+
# run: echo "This never runs"
|
|
60
|
+
|
|
61
|
+
# CORRECT — compare output string to string literal
|
|
62
|
+
- if: steps.check.outputs.changed == 'true'
|
|
63
|
+
run: echo "Files changed, running deploy"
|
|
64
|
+
|
|
65
|
+
# CORRECT — inverse check
|
|
66
|
+
- if: steps.check.outputs.changed != 'true'
|
|
67
|
+
run: echo "No changes, skipping deploy"
|
|
68
|
+
- language: yaml
|
|
69
|
+
label: 'Job-to-job boolean output — compare as string in downstream job'
|
|
70
|
+
code: |
|
|
71
|
+
jobs:
|
|
72
|
+
detect:
|
|
73
|
+
outputs:
|
|
74
|
+
has_changes: ${{ steps.diff.outputs.has_changes }}
|
|
75
|
+
runs-on: ubuntu-latest
|
|
76
|
+
steps:
|
|
77
|
+
- id: diff
|
|
78
|
+
run: |
|
|
79
|
+
# Output is always a string
|
|
80
|
+
echo "has_changes=true" >> $GITHUB_OUTPUT
|
|
81
|
+
|
|
82
|
+
deploy:
|
|
83
|
+
needs: detect
|
|
84
|
+
# CORRECT: compare job output as string literal
|
|
85
|
+
if: needs.detect.outputs.has_changes == 'true'
|
|
86
|
+
runs-on: ubuntu-latest
|
|
87
|
+
steps:
|
|
88
|
+
- run: echo "Deploying"
|
|
89
|
+
prevention:
|
|
90
|
+
- "Always use single-quoted string literals in if: conditions when comparing step or job outputs: == 'true' not == true"
|
|
91
|
+
- 'Use fromJSON() to parse numeric step outputs before arithmetic or numeric comparisons'
|
|
92
|
+
- 'Audit workflows for bare == true or == false comparisons on steps.*.outputs.* and needs.*.outputs.*'
|
|
93
|
+
- 'Use actionlint — it detects type mismatches in expression comparisons involving outputs'
|
|
94
|
+
docs:
|
|
95
|
+
- url: 'https://docs.github.com/en/actions/writing-workflows/choosing-what-your-workflow-does/passing-information-between-jobs'
|
|
96
|
+
label: 'GitHub Actions — Passing information between jobs'
|
|
97
|
+
- url: 'https://docs.github.com/en/actions/writing-workflows/choosing-what-your-workflow-does/evaluate-expressions-in-workflows-and-actions#literals'
|
|
98
|
+
label: 'GitHub Actions — Expression literals and type coercion'
|
|
99
|
+
- url: 'https://docs.github.com/en/actions/writing-workflows/choosing-what-your-workflow-does/setting-an-output-parameter'
|
|
100
|
+
label: 'GitHub Actions — Setting an output parameter'
|
package/package.json
CHANGED