Skip to content

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:

  1. Multiline secrets — only the first line is masked
  2. Structured secrets — JSON objects are partially masked
  3. Base64-encoded output — encoded form of secret is not masked
  4. 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
concurrency:  
  group: ${{ github.workflow }}-${{ github.ref }}  
  cancel-in-progress: true  # For CI  

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