GitHub Actions — Pitfalls¶
OWNER: alex, tjitte
ALSO_USED_BY: leon, marta, iwona
LAST_VERIFIED: 2026-03-26
GE_STACK_VERSION: GitHub Actions (github-hosted runners, ubuntu-latest)
Overview¶
Known failure modes and sharp edges with GitHub Actions in GE.
Every item here has caused real CI/CD failures, security issues,
or wasted runner minutes. This page grows as agents discover new pitfalls.
Runner Environment Inconsistencies¶
Severity: MEDIUM
ubuntu-latest periodically points to a new Ubuntu version.
When GitHub rolls forward (e.g., 22.04 to 24.04), pre-installed
tool versions change and workflows can break.
IF: workflow suddenly fails after no code changes
THEN: check if ubuntu-latest was updated
RUN: check the runner image release notes on GitHub
IF: workflow depends on specific tool versions
THEN: pin them explicitly in setup steps
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
with:
node-version: "22.12.0" # Exact version, not just "22"
ANTI_PATTERN: relying on pre-installed tool versions
FIX: always install tools explicitly with pinned versions
ADDED_FROM: ci-break-2026-01, pnpm version changed on runner image update
Secret Masking Failures¶
Severity: HIGH
GitHub masks secrets in logs, but masking fails in several cases:
- Multiline secrets — only the first line is masked
- Structured secrets — JSON objects are partially masked
- Base64-encoded output — encoded form of secret is not masked
- Short secrets — secrets under 4 characters may not be masked
IF: secret appears in logs
THEN: rotate the secret immediately
THEN: fix the masking issue before re-running
# Mask each line of a multiline secret
- run: |
echo "${{ secrets.MULTILINE_SECRET }}" | while IFS= read -r line; do
echo "::add-mask::$line"
done
CHECK: secrets are never less than 8 characters
CHECK: multiline secrets have each line masked individually
CHECK: never echo a secret for debugging — use ::add-mask:: first
ADDED_FROM: secret-leak-2026-02, base64-encoded API key visible in build logs
Concurrent Workflow Conflicts¶
Severity: MEDIUM
Without concurrency groups, multiple workflow runs can execute
simultaneously on the same branch. This causes:
- Race conditions in deployments
- Duplicate Docker image pushes
- Conflicting Terraform applies
CHECK: every workflow has a concurrency group
CHECK: deployment workflows use cancel-in-progress: false
IF: deployment is in progress and a new commit is pushed
THEN: the new workflow queues (does not cancel the in-progress deploy)
THEN: deployment completes, then new deployment starts
ANTI_PATTERN: cancel-in-progress: true on deployment workflows
FIX: set to false — cancelling a mid-deploy can leave infrastructure in broken state
ADDED_FROM: partial-deploy-2026-02, cancelled deployment left staging half-updated
Billing Gotchas¶
Severity: MEDIUM
Runner Minutes¶
GitHub-hosted runners are billed by the minute (after free tier).
Wasted minutes add up:
| Waste Source | Impact |
|---|---|
| No caching | +5 min per run (dependency install) |
| No concurrency cancel | Duplicate runs on rapid commits |
| Long-running tests without timeout | Hung jobs consume minutes |
| Matrix builds without fail-fast consideration | All combos run even when one fails early |
CHECK: every job has timeout-minutes set
CHECK: caching configured for node_modules, pip, Docker layers
CHECK: concurrency groups cancel redundant CI runs
Cache Storage¶
Repository cache has a limit (10+ GB). Old caches are evicted LRU.
If cache is constantly evicted, every run re-downloads dependencies.
IF: cache miss rate is high
THEN: check cache usage
THEN: ensure cache keys are stable (use lockfile hashes)
ANTI_PATTERN: using volatile cache keys that change every run
FIX: key on lockfile hash, not timestamp or commit SHA
ADDED_FROM: billing-spike-2026-03, cache eviction caused 3x longer CI runs
Pull Request Target Trap¶
Severity: CRITICAL
pull_request_target runs with the BASE branch's workflow definition
but has write permissions and access to secrets. If the PR modifies
any files that the workflow reads at runtime, the PR author can
exfiltrate secrets.
ANTI_PATTERN: on: pull_request_target with actions/checkout@{sha} of PR head
FIX: use on: pull_request — it runs with read-only permissions and no secrets
IF: fork PRs absolutely need secret access
THEN: use a two-workflow pattern (PR workflow + workflow_run trigger)
READ_ALSO: wiki/docs/stack/github-actions/security.md
ADDED_FROM: security-review-2026-01, audit flagged pull_request_target usage
Expression Injection¶
Severity: HIGH
GitHub Actions expressions (${{ }}) in run: steps are vulnerable
to injection if they contain user-controlled values.
# VULNERABLE — PR title can contain shell commands
- run: echo "PR: ${{ github.event.pull_request.title }}"
# SAFE — use environment variable
- run: echo "PR: $PR_TITLE"
env:
PR_TITLE: ${{ github.event.pull_request.title }}
CHECK: user-controlled values (PR title, branch name, commit message)
are passed via env:, never interpolated directly in run:
ANTI_PATTERN: run: echo "${{ github.event.issue.body }}"
FIX: assign to env var first, reference env var in run
ADDED_FROM: security-audit-2026-02, potential injection in PR comment workflow
Workflow File Changes in PRs¶
Severity: LOW
When a PR modifies .github/workflows/*.yml, the PR uses the
existing workflow from the base branch (not the modified one).
This means workflow changes cannot be tested on the PR itself.
IF: testing workflow changes
THEN: create a temporary workflow with workflow_dispatch trigger
THEN: test on the feature branch manually
THEN: remove the temporary workflow before merging
GITHUB_TOKEN Permission Escalation¶
Severity: MEDIUM
The default GITHUB_TOKEN permissions depend on repository settings.
If the repo default is write-all, every workflow gets full write access.
CHECK: repository default token permissions are set to read
CHECK: workflows explicitly declare needed permissions
READ_ALSO: wiki/docs/stack/github-actions/security.md
Cross-References¶
READ_ALSO: wiki/docs/stack/github-actions/index.md
READ_ALSO: wiki/docs/stack/github-actions/patterns.md
READ_ALSO: wiki/docs/stack/github-actions/security.md
READ_ALSO: wiki/docs/stack/github-actions/checklist.md