GitHub Actions study materials
GitHub Actions study resources

GitHub Actions common mistakes

GitHub Actions · Understand the traps and how to avoid them.

Mistake 1

Writing `on: [push, pull_request]` and then expecting a job that reads repository secrets to run safely on pull requests from forks; the candidate is surprised when the secret is empty or the workflow fails on a fork PR.

Candidates conflate the trigger event with the security model. For fork pull requests, GitHub does not pass repository secrets to the workflow and gives the GITHUB_TOKEN read-only permissions, so secret-dependent steps fail or are skipped. The behavior is by design, not a bug.

What to do instead

Decide deliberately: use pull_request_target only with extreme care (it runs in the base repo context with secrets - never check out and execute untrusted PR code), or split workflows so fork PRs run a secret-free build/test job and a separate trusted workflow (e.g., workflow_run or a maintainer-approved environment) handles deployment. Document which events get secrets.

Mistake 2

Assuming `needs: build` makes the downstream job run only when `build` succeeds, then being unable to explain why a `deploy` job ran after a matrix leg failed, or why it was skipped when the candidate expected it to run.

The default condition is success(), which evaluates the whole dependency chain including all matrix legs. Candidates do not know that a failed matrix leg fails the parent job, and they also do not know about if: always() / if: failure() / if: cancelled() and the needs.<job>.result outputs.

What to do instead

Learn the job status functions (success(), failure(), always(), cancelled()) and how they interact with needs. Use if: always() for cleanup/notification jobs, if: failure() for alerting, and read needs.build.result when branching on outcomes. Remember a matrix job's aggregate result is failure if any leg fails.

Mistake 3

Putting a secret in a workflow `env:` block or in a step `run:` string and then wondering why the value is visible in logs or why the workflow is rejected/flagged.

Candidates treat env: as a safe place for credentials. Secrets must come from secrets.* or vars.* context, and even then they are masked only when used as secrets; interpolating a secret directly into a shell command can leak it via command echoing or error output.

What to do instead

Reference secrets only via ${{ secrets.NAME }} and pass them to steps as environment variables rather than inlining into run: scripts. Avoid set -x around secret use, avoid echo of secrets, and use ::add-mask:: for derived values. Prefer OIDC federation to long-lived cloud credentials where possible.

Mistake 4

Writing a cache step with `actions/cache` but no correct `key`, or using a key that changes every run (e.g., includes `github.run_id`), so the cache never hits; the candidate concludes caching is broken.

Candidates copy a cache snippet without understanding that the key must be stable across runs and derived from lockfiles (hashFiles('**/package-lock.json')). Including run-specific values or a timestamp guarantees a miss, and restore-keys is often omitted.

What to do instead

Build the key from a lockfile hash plus OS/runtime version, e.g. key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }}, and add ordered restore-keys for partial fallback. Verify hits in the job log ('Cache restored from key...') and remember caches are scoped by branch and evicted after inactivity.

Mistake 5

Using `actions/checkout` and then running a script that needs a different ref, submodules, or full history, and getting 'not a git repository', missing submodule files, or shallow-clone failures.

Candidates assume checkout behaves like a full local clone. By default it fetches a single commit (depth 1) for the triggering ref, does not fetch submodules, and does not fetch tags or other branches.

What to do instead

Set fetch-depth: 0 when you need history (versioning, changelog, merge-base), submodules: recursive when needed, and ref:/repository: when checking out a different ref or repo. For PRs, remember the default checkout is the merge commit, not the head branch.

Mistake 6

Hardcoding `runs-on: ubuntu-latest` and assuming a specific image, tool version, or preinstalled software (e.g., a particular Java, Node, or Docker version) that later disappears or differs.

Candidates treat -latest as a fixed environment. GitHub periodically updates the -latest image and its preinstalled tools, so builds that depend on implicit versions break without any change to the workflow.

What to do instead

Pin the runner image (ubuntu-22.04, windows-2022) and pin tool versions explicitly with setup actions (actions/setup-node with node-version, actions/setup-java with distribution and java-version). Check the runner-images releases for what is preinstalled rather than assuming.

Mistake 7

Writing a reusable workflow or composite action and then failing to pass inputs/secrets correctly, or being unable to explain why `secrets: inherit` is needed or why `uses:` for a local workflow is rejected.

Candidates mix up the three reuse mechanisms (reusable workflows via workflow_call, composite actions, and workflow_dispatch) and their input/secret scoping. Reusable workflows do not automatically receive caller secrets, and local reusable workflows must be referenced with a path and a ref (./.github/workflows/x.yml at the top level, owner/repo/.github/workflows/x.yml@ref for remote).

What to do instead

Declare on: workflow_call with typed inputs and secrets, pass them explicitly from the caller, and use secrets: inherit only when appropriate. Remember nested reusable workflows are limited (one level of nesting) and a reusable workflow cannot call another reusable workflow more than that. Test with a workflow_dispatch entry point.

Mistake 8

Assuming a workflow triggered by a push made with the default `GITHUB_TOKEN` will trigger other workflows (e.g., a deploy workflow), and then debugging why the downstream workflow never ran.

Candidates do not know that events created with the repository's GITHUB_TOKEN do not trigger new workflow runs, to prevent recursive runs. They also may not realize the default token permissions may be read-only depending on repository/org settings.

What to do instead

If you need chained workflows, use a GitHub App token or a PAT with the needed scopes (and understand the security trade-off), or restructure into a single workflow with needs. Set explicit permissions: in the workflow and verify the org/repo default token permission setting.

Mistake 9

Debugging a failing job by adding `echo` statements and re-running the whole pipeline, without using step debug logging, job summaries, artifacts, or `ACTIONS_STEP_DEBUG`.

Candidates do not know the diagnostic tooling: re-running with debug logging enabled, $GITHUB_STEP_SUMMARY, uploading logs/artifacts, continue-on-error for probing steps, and tmate/SSH-style debugging for interactive sessions. They also re-run failed jobs without checking whether the failure is flaky or environmental.

What to do instead

Enable step debug logging (repository secret ACTIONS_STEP_DEBUG=true or re-run with debug), write diagnostics to $GITHUB_STEP_SUMMARY, upload artifacts for post-mortem, and use continue-on-error on exploratory steps. Re-run only failed jobs when the failure is isolated, and compare runner image and dependency versions before assuming a code bug.

Mistake 10

Writing a matrix that explodes combinatorially or includes an invalid combination, and then being unable to exclude the bad pair or explain why `fail-fast` cancelled sibling jobs.

Candidates build matrix: with include/exclude without understanding ordering rules: exclude removes combinations, include adds or augments them, and fail-fast: true (the default) cancels in-progress siblings when one leg fails. They also miss max-parallel for controlling concurrency.

What to do instead

Use exclude for known-invalid pairs, include to add one-off configurations, set fail-fast: false when you want all legs to report, and max-parallel to limit concurrent runners. Keep the matrix small and purposeful; move long-running variants to a separate scheduled workflow.