Lesson 6 of 6

Common Gotchas

Each of these has cost thousands of developers an afternoon. The pattern to notice: most aren't errors at all — they're workflows silently not running, or values silently absent, with no red X to warn you. Silence is the hard failure mode.

💡 The Bottom Line

When a workflow misbehaves: is the file on the branch that needs it (and valid YAML)? Is the trigger context what you think — fork PRs get no secrets? Is the working directory where you assume? Those three questions resolve most mysteries before deep debugging.

1. The workflow that never runs

No error, no run, nothing. The checklist in likelihood order: the file isn't in .github/workflows/ at the repo root (a nested project folder doesn't count); invalid YAML (check the Actions tab for a parse-error banner); the trigger doesn't cover the branch you pushed (branches: [main], pushed to a feature branch); a paths: filter excluded your change; or — for schedule and workflow_dispatch — the file isn't on the default branch yet, which is where those triggers read from.

2. Fork PRs: secrets vanish by design

A contributor forks your public repo, opens a PR — and your CI fails on their PR because ${{ secrets.X }} is empty. That's deliberate: if fork PRs got secrets, anyone could open a PR whose "test" step exfiltrates them. Design around it: keep the default PR checks secret-free (build + test need none), and put secret-needing steps behind push to main or guard them with an if: github.event.pull_request.head.repo.fork == false condition. Related trap: pull_request_target does expose secrets — combining it with a checkout of the fork's code is a known vulnerability pattern. Don't, unless you deeply know why.

3. Where am I? Working-directory assumptions

Every run: step starts in the repo root — each step, every time. A cd frontend in one step does not carry into the next: steps are separate shell invocations. For one step, use working-directory: frontend on that step; for a whole job, set defaults: run: working-directory: frontend. The tell: "no such file or directory" for a file you can see in the repo — you're just not standing where you think.

4. The minutes bill

Three quiet multipliers on private-repo minutes: macOS runners cost 10× Linux (use macos-latest only when you truly need it); matrices multiply (3 versions × 3 OSes = 9 VMs per push); and double-triggering (Lesson 3's push + pull_request overlap) doubles everything. Also set timeout-minutes: on jobs — the default is 6 hours, and a hung test will happily bill all 360 of them.

5. Old action versions and the deprecation wall

Actions pinned years ago (actions/upload-artifact@v1) eventually stop working when GitHub retires the platform features they used — and the failure arrives without you changing anything ("it broke on its own" usually means this). Skim deprecation warnings in run logs (they appear long before removal), and let Dependabot watch your workflow files: a few lines in .github/dependabot.yml with package-ecosystem: github-actions opens PRs when your actions age.

🙋 There Are No Dumb Questions

Q: Can I run all this locally instead of debugging via push-wait-read-logs?
A: Partially. The act tool runs many workflows in local Docker containers — great for fast YAML iteration, imperfect fidelity (some events and contexts differ). The pragmatic combo: keep your pipeline's meat in scripts (npm test, ./build.sh) that run anywhere, keep the YAML a thin wrapper, and use a scratch branch with workflow_dispatch when you must test the wrapper itself.

⚠️ Watch Out

A step that fails but shouldn't block everything (posting a comment, a metrics upload) gets continue-on-error: true — but never on steps whose failure means something (tests!). A test job with continue-on-error is a pipeline that lies green — the CI/CD track's trust-erosion gotcha, self-inflicted with one line.

🔧 Try It Yourself

Prove the working-directory gotcha in your scratch repo:

jobs:
  demo:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - run: mkdir -p sub && cd sub && pwd
      - run: pwd        # back at the repo root — cd did not persist
      - run: pwd
        working-directory: sub   # the right way

Read the three pwd outputs in the log — thirty seconds of proof that saves the afternoon later.

✅ Quick Check

Why are secrets unavailable in workflows triggered by a fork's pull request?


Track complete — the recap gathers the whole toolkit, and points at continuous deployment as the natural next move.

← Back to
Next →