@htekdev/actions-debugger 1.0.85 → 1.0.87
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/upload-artifact-storage-quota-exceeded.yml +76 -0
- package/errors/permissions-auth/fine-grained-pat-resource-owner-mismatch.yml +75 -0
- package/errors/runner-environment/github-script-require-relative-path-cwd.yml +74 -0
- package/errors/runner-environment/runner-environment-151.yml +93 -0
- package/errors/silent-failures/checkout-path-github-workspace-unchanged.yml +91 -0
- package/errors/silent-failures/silent-failures-080.yml +97 -0
- package/errors/triggers/triggers-059.yml +106 -0
- package/errors/yaml-syntax/yaml-syntax-054.yml +94 -0
- package/package.json +1 -1
|
@@ -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,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,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,93 @@
|
|
|
1
|
+
id: runner-environment-151
|
|
2
|
+
title: "Docker container action creates root-owned files causing Permission denied in following steps"
|
|
3
|
+
category: runner-environment
|
|
4
|
+
severity: error
|
|
5
|
+
tags:
|
|
6
|
+
- docker
|
|
7
|
+
- container-action
|
|
8
|
+
- file-permissions
|
|
9
|
+
- root-user
|
|
10
|
+
- workspace
|
|
11
|
+
patterns:
|
|
12
|
+
- regex: 'Permission denied'
|
|
13
|
+
flags: i
|
|
14
|
+
- regex: 'EACCES: permission denied'
|
|
15
|
+
flags: i
|
|
16
|
+
- regex: 'cannot open .+ Permission denied'
|
|
17
|
+
flags: i
|
|
18
|
+
- regex: 'error: open .+ permission denied'
|
|
19
|
+
flags: i
|
|
20
|
+
error_messages:
|
|
21
|
+
- "Permission denied"
|
|
22
|
+
- "cannot create directory '...': Permission denied"
|
|
23
|
+
- "EACCES: permission denied, open '/github/workspace/...'"
|
|
24
|
+
- "error: cannot open '.git/COMMIT_EDITMSG': Permission denied"
|
|
25
|
+
- "chown: changing ownership of '...': Operation not permitted"
|
|
26
|
+
root_cause: |
|
|
27
|
+
Docker-based GitHub Actions — both `uses: docker://image:tag` inline steps and
|
|
28
|
+
actions with `runs.using: docker` in their action.yml — run their container process
|
|
29
|
+
as root (UID 0) by default unless the Dockerfile explicitly sets a USER directive.
|
|
30
|
+
|
|
31
|
+
Any files written to `GITHUB_WORKSPACE` (mounted at `/github/workspace` inside the
|
|
32
|
+
container) are created with root ownership (uid=0, gid=0). Subsequent workflow steps
|
|
33
|
+
run as the `runner` user (UID 1001) and cannot read, modify, or delete root-owned
|
|
34
|
+
files, causing "Permission denied" errors.
|
|
35
|
+
|
|
36
|
+
The failure is especially common when:
|
|
37
|
+
- A Docker action writes build artifacts, generated code, or lock files.
|
|
38
|
+
- A subsequent step tries to commit changes or run tools on the generated output.
|
|
39
|
+
- The workflow uses `actions/checkout` after a Docker action and git operations fail.
|
|
40
|
+
fix: |
|
|
41
|
+
Option 1 — Chown the workspace after the Docker action (easiest for third-party actions):
|
|
42
|
+
Add a step after the Docker action: `sudo chown -R "$USER:$(id -gn)" "$GITHUB_WORKSPACE"`
|
|
43
|
+
|
|
44
|
+
Option 2 — Set USER in the Dockerfile (best for actions you control):
|
|
45
|
+
Add `USER 1001` (runner UID) to the action's Dockerfile so all written files
|
|
46
|
+
are owned by the runner user.
|
|
47
|
+
|
|
48
|
+
Option 3 — Use --user flag in the Docker run args:
|
|
49
|
+
Set `args` in the action's action.yml to include `--user=1001:127`.
|
|
50
|
+
fix_code:
|
|
51
|
+
- language: yaml
|
|
52
|
+
label: "Fix: chown workspace after Docker action (works for any Docker-based action)"
|
|
53
|
+
code: |
|
|
54
|
+
steps:
|
|
55
|
+
- uses: actions/checkout@v4
|
|
56
|
+
|
|
57
|
+
- name: Run Docker-based action
|
|
58
|
+
uses: docker://my-build-image:latest
|
|
59
|
+
with:
|
|
60
|
+
args: '--output /github/workspace/dist'
|
|
61
|
+
|
|
62
|
+
- name: Fix file ownership after Docker action
|
|
63
|
+
run: sudo chown -R "$USER:$(id -gn)" "$GITHUB_WORKSPACE"
|
|
64
|
+
|
|
65
|
+
- name: Use build output (now accessible as runner user)
|
|
66
|
+
run: ls -la dist/ && cat dist/output.txt
|
|
67
|
+
- language: yaml
|
|
68
|
+
label: "Fix: set non-root USER in Dockerfile (preferred when controlling the action)"
|
|
69
|
+
code: |
|
|
70
|
+
# In your Docker action's Dockerfile — run as UID 1001 to match GitHub runner
|
|
71
|
+
# FROM ubuntu:22.04
|
|
72
|
+
# RUN useradd -u 1001 -g 127 runner
|
|
73
|
+
# WORKDIR /github/workspace
|
|
74
|
+
# USER 1001
|
|
75
|
+
# ENTRYPOINT ["/entrypoint.sh"]
|
|
76
|
+
|
|
77
|
+
# With this Dockerfile, no chown step is needed — files are owned by runner user
|
|
78
|
+
steps:
|
|
79
|
+
- uses: actions/checkout@v4
|
|
80
|
+
- uses: ./ # local Docker action with non-root USER
|
|
81
|
+
- run: cat generated-output.txt # accessible without permission errors
|
|
82
|
+
prevention:
|
|
83
|
+
- "Always set a non-root USER directive in Dockerfiles for actions that write to the workspace."
|
|
84
|
+
- "After any third-party Docker action, add a chown step before accessing created files."
|
|
85
|
+
- "Prefer JavaScript/TypeScript or composite actions over Docker actions when workspace I/O is needed — they run as the runner user by default."
|
|
86
|
+
- "Test Docker actions locally with UID 1001 to catch permission issues before CI."
|
|
87
|
+
docs:
|
|
88
|
+
- url: "https://docs.github.com/en/actions/sharing-automations/creating-actions/dockerfile-support-for-github-actions"
|
|
89
|
+
label: "Dockerfile support for GitHub Actions"
|
|
90
|
+
- url: "https://docs.github.com/en/actions/sharing-automations/creating-actions/creating-a-docker-container-action"
|
|
91
|
+
label: "Creating a Docker container action"
|
|
92
|
+
- url: "https://docs.github.com/en/actions/writing-workflows/choosing-what-your-workflow-does/store-information-in-variables#default-environment-variables"
|
|
93
|
+
label: "GITHUB_WORKSPACE default environment variable"
|
|
@@ -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,97 @@
|
|
|
1
|
+
id: silent-failures-080
|
|
2
|
+
title: "github.event.commits always empty array on pull_request events"
|
|
3
|
+
category: silent-failures
|
|
4
|
+
severity: silent-failure
|
|
5
|
+
tags:
|
|
6
|
+
- pull-request
|
|
7
|
+
- commits
|
|
8
|
+
- event-payload
|
|
9
|
+
- skip-ci
|
|
10
|
+
- multi-trigger
|
|
11
|
+
patterns:
|
|
12
|
+
- regex: 'github\.event\.commits\[0\]'
|
|
13
|
+
flags: i
|
|
14
|
+
- regex: 'Cannot read propert\w+ of undefined.*message'
|
|
15
|
+
flags: i
|
|
16
|
+
error_messages:
|
|
17
|
+
- "Error: Cannot read properties of undefined (reading 'message')"
|
|
18
|
+
- "github.event.commits[0] evaluates to null on pull_request events"
|
|
19
|
+
- "contains(github.event.commits[0].message, '[skip ci]') always false on PRs"
|
|
20
|
+
root_cause: |
|
|
21
|
+
The `github.event.commits` array is only populated in `push` event payloads.
|
|
22
|
+
On `pull_request` events, `github.event.commits` is always an empty array `[]`.
|
|
23
|
+
Accessing `github.event.commits[0]` returns `null`, and
|
|
24
|
+
`github.event.commits[0].message` returns an empty string without throwing an
|
|
25
|
+
expression error.
|
|
26
|
+
|
|
27
|
+
This is the most common footgun when implementing "[skip ci]" commit message
|
|
28
|
+
detection: the condition works correctly for `push` events but silently never
|
|
29
|
+
triggers (never skips) on `pull_request` events. The workflow runs for every PR
|
|
30
|
+
push regardless of the commit message.
|
|
31
|
+
|
|
32
|
+
The issue affects any multi-trigger workflow with both `push:` and `pull_request:`
|
|
33
|
+
that reads commit data from the event payload.
|
|
34
|
+
fix: |
|
|
35
|
+
Guard `github.event.commits` access by event type, or use PR-specific context:
|
|
36
|
+
1. Use `if: github.event_name == 'push'` before accessing commits.
|
|
37
|
+
2. For PR events, check `github.event.pull_request.title` or `github.event.pull_request.body`
|
|
38
|
+
for skip conditions.
|
|
39
|
+
3. Use the `actions/github-script` action to query commits via the REST API on PR events.
|
|
40
|
+
4. Consider separate workflows for push and pull_request to avoid cross-event payload confusion.
|
|
41
|
+
fix_code:
|
|
42
|
+
- language: yaml
|
|
43
|
+
label: "Wrong: commit message check silently fails on pull_request events"
|
|
44
|
+
code: |
|
|
45
|
+
on: [push, pull_request]
|
|
46
|
+
|
|
47
|
+
jobs:
|
|
48
|
+
build:
|
|
49
|
+
runs-on: ubuntu-latest
|
|
50
|
+
# github.event.commits[0].message is null on pull_request — condition always false
|
|
51
|
+
if: "!contains(github.event.commits[0].message, '[skip ci]')"
|
|
52
|
+
steps:
|
|
53
|
+
- uses: actions/checkout@v4
|
|
54
|
+
- run: npm test
|
|
55
|
+
- language: yaml
|
|
56
|
+
label: "Fix: guard by event type and use PR-appropriate skip signal"
|
|
57
|
+
code: |
|
|
58
|
+
on: [push, pull_request]
|
|
59
|
+
|
|
60
|
+
jobs:
|
|
61
|
+
build:
|
|
62
|
+
runs-on: ubuntu-latest
|
|
63
|
+
steps:
|
|
64
|
+
- uses: actions/checkout@v4
|
|
65
|
+
|
|
66
|
+
- name: Check skip condition
|
|
67
|
+
id: skip
|
|
68
|
+
run: |
|
|
69
|
+
SKIP=false
|
|
70
|
+
if [[ "${{ github.event_name }}" == "push" ]]; then
|
|
71
|
+
# Commits array is available on push events
|
|
72
|
+
if echo "${{ github.event.commits[0].message }}" | grep -q '\[skip ci\]'; then
|
|
73
|
+
SKIP=true
|
|
74
|
+
fi
|
|
75
|
+
elif [[ "${{ github.event_name }}" == "pull_request" ]]; then
|
|
76
|
+
# Check PR title or body for skip signal on PR events
|
|
77
|
+
if echo "${{ github.event.pull_request.title }}" | grep -q '\[skip ci\]'; then
|
|
78
|
+
SKIP=true
|
|
79
|
+
fi
|
|
80
|
+
fi
|
|
81
|
+
echo "skip=$SKIP" >> "$GITHUB_OUTPUT"
|
|
82
|
+
|
|
83
|
+
- name: Run tests
|
|
84
|
+
if: steps.skip.outputs.skip != 'true'
|
|
85
|
+
run: npm test
|
|
86
|
+
prevention:
|
|
87
|
+
- "Never access github.event.commits on workflows with pull_request triggers — it will always be empty."
|
|
88
|
+
- "Use separate workflows for push and pull_request if commit-message-based conditionals are required."
|
|
89
|
+
- "For PR skip conditions, use the PR title or body rather than commit messages."
|
|
90
|
+
- "Check which fields are populated for each event type in the GitHub webhook event payload documentation."
|
|
91
|
+
docs:
|
|
92
|
+
- url: "https://docs.github.com/en/webhooks/webhook-events-and-payloads#push"
|
|
93
|
+
label: "Push webhook event payload (commits array)"
|
|
94
|
+
- url: "https://docs.github.com/en/webhooks/webhook-events-and-payloads#pull_request"
|
|
95
|
+
label: "Pull request webhook event payload"
|
|
96
|
+
- url: "https://docs.github.com/en/actions/writing-workflows/choosing-what-your-workflow-does/using-conditions-to-control-job-execution"
|
|
97
|
+
label: "Using conditions to control job execution"
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
id: triggers-059
|
|
2
|
+
title: "workflow_call required: true inputs silently receive empty string when omitted by caller"
|
|
3
|
+
category: triggers
|
|
4
|
+
severity: silent-failure
|
|
5
|
+
tags:
|
|
6
|
+
- reusable-workflow
|
|
7
|
+
- workflow-call
|
|
8
|
+
- inputs
|
|
9
|
+
- required
|
|
10
|
+
- validation
|
|
11
|
+
patterns:
|
|
12
|
+
- regex: 'required.*input.*not provided'
|
|
13
|
+
flags: i
|
|
14
|
+
- regex: 'Input .+ is required but was not provided'
|
|
15
|
+
flags: i
|
|
16
|
+
error_messages:
|
|
17
|
+
- "Input 'environment' is required but was not provided"
|
|
18
|
+
- "required: true input silently receives empty string when omitted from caller with:"
|
|
19
|
+
root_cause: |
|
|
20
|
+
When a reusable workflow declares `on.workflow_call.inputs` with `required: true`,
|
|
21
|
+
GitHub does NOT enforce this requirement at call time via the REST API or from other
|
|
22
|
+
calling workflows. The `required: true` flag is informational documentation only —
|
|
23
|
+
it is not validated before the workflow runs.
|
|
24
|
+
|
|
25
|
+
When a calling workflow omits a required input from its `with:` block, the reusable
|
|
26
|
+
workflow receives an empty string `""` for the missing input with no error or warning.
|
|
27
|
+
The workflow starts and runs to whatever conclusion the empty input causes.
|
|
28
|
+
|
|
29
|
+
This leads to silent failures such as deploying to an undefined environment, running
|
|
30
|
+
against an empty target URL, or passing `""` to shell commands that produce incorrect
|
|
31
|
+
results. The bug is especially hard to find because the calling workflow shows a green
|
|
32
|
+
"success" until the silent empty input causes a downstream failure.
|
|
33
|
+
fix: |
|
|
34
|
+
Explicitly validate all required inputs at the start of the reusable workflow:
|
|
35
|
+
1. Add an input-validation step as the FIRST step of the FIRST job that checks each
|
|
36
|
+
required input for emptiness and fails with a descriptive error.
|
|
37
|
+
2. Use `default:` values in `on.workflow_call.inputs` to handle omitted inputs
|
|
38
|
+
gracefully with a fallback rather than silently using empty string.
|
|
39
|
+
3. In calling workflows, always pass all inputs explicitly with `with:` — never
|
|
40
|
+
rely on `required: true` to catch missing inputs.
|
|
41
|
+
fix_code:
|
|
42
|
+
- language: yaml
|
|
43
|
+
label: "Reusable workflow: validate required inputs at startup"
|
|
44
|
+
code: |
|
|
45
|
+
# .github/workflows/deploy.yml (reusable workflow)
|
|
46
|
+
on:
|
|
47
|
+
workflow_call:
|
|
48
|
+
inputs:
|
|
49
|
+
environment:
|
|
50
|
+
required: true # documentation only — NOT enforced by GitHub
|
|
51
|
+
type: string
|
|
52
|
+
target-url:
|
|
53
|
+
required: true
|
|
54
|
+
type: string
|
|
55
|
+
|
|
56
|
+
jobs:
|
|
57
|
+
validate-inputs:
|
|
58
|
+
runs-on: ubuntu-latest
|
|
59
|
+
steps:
|
|
60
|
+
- name: Validate required inputs
|
|
61
|
+
run: |
|
|
62
|
+
ERRORS=()
|
|
63
|
+
if [[ -z "${{ inputs.environment }}" ]]; then
|
|
64
|
+
ERRORS+=("Required input 'environment' was not provided")
|
|
65
|
+
fi
|
|
66
|
+
if [[ -z "${{ inputs.target-url }}" ]]; then
|
|
67
|
+
ERRORS+=("Required input 'target-url' was not provided")
|
|
68
|
+
fi
|
|
69
|
+
if [[ ${#ERRORS[@]} -gt 0 ]]; then
|
|
70
|
+
for ERR in "${ERRORS[@]}"; do
|
|
71
|
+
echo "::error::$ERR"
|
|
72
|
+
done
|
|
73
|
+
exit 1
|
|
74
|
+
fi
|
|
75
|
+
|
|
76
|
+
deploy:
|
|
77
|
+
needs: validate-inputs
|
|
78
|
+
runs-on: ubuntu-latest
|
|
79
|
+
steps:
|
|
80
|
+
- run: echo "Deploying to ${{ inputs.environment }}"
|
|
81
|
+
- language: yaml
|
|
82
|
+
label: "Fix: use default: values to avoid silent empty string behavior"
|
|
83
|
+
code: |
|
|
84
|
+
on:
|
|
85
|
+
workflow_call:
|
|
86
|
+
inputs:
|
|
87
|
+
environment:
|
|
88
|
+
required: false
|
|
89
|
+
default: 'staging' # explicit default instead of relying on required: true
|
|
90
|
+
type: string
|
|
91
|
+
|
|
92
|
+
jobs:
|
|
93
|
+
deploy:
|
|
94
|
+
runs-on: ubuntu-latest
|
|
95
|
+
steps:
|
|
96
|
+
- run: echo "Deploying to ${{ inputs.environment }}" # always 'staging' if omitted
|
|
97
|
+
prevention:
|
|
98
|
+
- "Treat required: true on workflow_call inputs as documentation only — always validate inside the reusable workflow."
|
|
99
|
+
- "Add a dedicated input-validation job as the first job in every reusable workflow with required inputs."
|
|
100
|
+
- "Prefer default: values over required: true when a sensible fallback exists."
|
|
101
|
+
- "In calling workflows, always explicitly pass all inputs in the with: block."
|
|
102
|
+
docs:
|
|
103
|
+
- url: "https://docs.github.com/en/actions/writing-workflows/workflow-syntax-for-github-actions#onworkflow_callinputs"
|
|
104
|
+
label: "on.workflow_call.inputs syntax reference"
|
|
105
|
+
- url: "https://docs.github.com/en/actions/sharing-automations/reusing-workflows#using-inputs-and-secrets-in-a-called-workflow"
|
|
106
|
+
label: "Using inputs in a called workflow"
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
id: yaml-syntax-054
|
|
2
|
+
title: "env context silently empty in reusable workflow with: inputs"
|
|
3
|
+
category: yaml-syntax
|
|
4
|
+
severity: silent-failure
|
|
5
|
+
tags:
|
|
6
|
+
- reusable-workflow
|
|
7
|
+
- env-context
|
|
8
|
+
- with-inputs
|
|
9
|
+
- workflow-call
|
|
10
|
+
- context-availability
|
|
11
|
+
patterns:
|
|
12
|
+
- regex: '"env" context is not available in this context'
|
|
13
|
+
flags: i
|
|
14
|
+
- regex: 'env context.*not available.*reusable'
|
|
15
|
+
flags: i
|
|
16
|
+
error_messages:
|
|
17
|
+
- '"env" context is not available in this context'
|
|
18
|
+
- "Input evaluates to empty string when env context used in reusable workflow with:"
|
|
19
|
+
root_cause: |
|
|
20
|
+
The `env` context is NOT available inside `jobs.<job_id>.with:` blocks used to
|
|
21
|
+
call reusable workflows. Unlike `jobs.<job_id>.steps.with:` (regular action steps,
|
|
22
|
+
which support `env` context), reusable workflow `with:` inputs are evaluated before
|
|
23
|
+
job-level environment variables are injected.
|
|
24
|
+
|
|
25
|
+
Using `${{ env.MY_VAR }}` in a reusable workflow `with:` block silently evaluates
|
|
26
|
+
to an empty string. No error is thrown and the reusable workflow receives `""` for
|
|
27
|
+
that input. The actionlint static linter reports:
|
|
28
|
+
`"env" context is not available in this context`.
|
|
29
|
+
|
|
30
|
+
This is explicitly documented in GitHub's context availability table — `env` is
|
|
31
|
+
available in `jobs.<job_id>.steps.with` but NOT in `jobs.<job_id>.with`.
|
|
32
|
+
fix: |
|
|
33
|
+
Replace `env` context references in reusable workflow `with:` blocks with one of:
|
|
34
|
+
1. `vars` context — for repository or organization-level configuration variables
|
|
35
|
+
(Settings → Variables). Available in reusable workflow `with:`.
|
|
36
|
+
2. Inline literal values directly in `with:`.
|
|
37
|
+
3. Workflow-level `inputs:` context if the calling workflow is itself a reusable
|
|
38
|
+
workflow (chain input passing).
|
|
39
|
+
4. A prior job that captures the value and passes it via `needs.<job>.outputs`.
|
|
40
|
+
fix_code:
|
|
41
|
+
- language: yaml
|
|
42
|
+
label: "Wrong: env context in reusable workflow with: (silently evaluates to empty string)"
|
|
43
|
+
code: |
|
|
44
|
+
env:
|
|
45
|
+
BASE_URL: 'https://api.example.com'
|
|
46
|
+
DEPLOY_ENV: 'production'
|
|
47
|
+
|
|
48
|
+
jobs:
|
|
49
|
+
call-deploy:
|
|
50
|
+
uses: ./.github/workflows/deploy.yml
|
|
51
|
+
with:
|
|
52
|
+
api-url: ${{ env.BASE_URL }} # silently empty — env unavailable in reusable with:
|
|
53
|
+
environment: ${{ env.DEPLOY_ENV }} # also silently empty
|
|
54
|
+
- language: yaml
|
|
55
|
+
label: "Fix: use vars context or inline value"
|
|
56
|
+
code: |
|
|
57
|
+
# Set BASE_URL as a repository variable in Settings → Secrets and variables → Variables
|
|
58
|
+
jobs:
|
|
59
|
+
call-deploy:
|
|
60
|
+
uses: ./.github/workflows/deploy.yml
|
|
61
|
+
with:
|
|
62
|
+
api-url: ${{ vars.BASE_URL }} # vars IS available in reusable with:
|
|
63
|
+
environment: 'production' # inline literal also works
|
|
64
|
+
- language: yaml
|
|
65
|
+
label: "Fix: pass via prior job output if value is dynamic"
|
|
66
|
+
code: |
|
|
67
|
+
jobs:
|
|
68
|
+
resolve-config:
|
|
69
|
+
runs-on: ubuntu-latest
|
|
70
|
+
outputs:
|
|
71
|
+
api-url: ${{ steps.config.outputs.url }}
|
|
72
|
+
steps:
|
|
73
|
+
- id: config
|
|
74
|
+
run: echo "url=$BASE_URL" >> "$GITHUB_OUTPUT"
|
|
75
|
+
env:
|
|
76
|
+
BASE_URL: ${{ env.BASE_URL }} # env IS available in step env:
|
|
77
|
+
|
|
78
|
+
call-deploy:
|
|
79
|
+
needs: resolve-config
|
|
80
|
+
uses: ./.github/workflows/deploy.yml
|
|
81
|
+
with:
|
|
82
|
+
api-url: ${{ needs.resolve-config.outputs.api-url }} # needs context available
|
|
83
|
+
prevention:
|
|
84
|
+
- "Consult the GitHub Actions context availability table before using contexts in reusable workflow with: blocks."
|
|
85
|
+
- "Prefer vars context (repository/org variables) over env for values shared across workflows."
|
|
86
|
+
- "Run actionlint in CI — it detects unavailable context usage and catches this before runtime."
|
|
87
|
+
- "If a reusable workflow input is silently empty, check context availability as the first debugging step."
|
|
88
|
+
docs:
|
|
89
|
+
- url: "https://docs.github.com/en/actions/writing-workflows/choosing-what-your-workflow-does/contexts#context-availability"
|
|
90
|
+
label: "GitHub Actions context availability table"
|
|
91
|
+
- url: "https://docs.github.com/en/actions/sharing-automations/reusing-workflows#passing-inputs-and-secrets-to-a-called-workflow"
|
|
92
|
+
label: "Passing inputs and secrets to a called workflow"
|
|
93
|
+
- url: "https://rhysd.github.io/actionlint/"
|
|
94
|
+
label: "actionlint — static checker for GitHub Actions"
|
package/package.json
CHANGED