DevOps Nimbus GitHub Actions

GitHub Actions errors and fixes

57 error messages from real runs. Each has the exact text, what it means, and its causes, most likely first, each with a check that proves it and the fix for that cause. Then learn it for good: break and fix each one yourself in the playground.

You have an error in your yaml syntax

Error

Invalid workflow file: .github/workflows/ci.yml#L9
You have an error in your yaml syntax on line 9

What it means

GitHub could not read the workflow file as YAML, so it cannot create any job. The line number is where the parser gave up; the mistake is on that line or the one above it. You see a red run with no jobs, or no run at all, right after changing the file.

Causes, most likely first

  1. Indentation is wrong: a key is not lined up with its siblings, or sits under the wrong parent. YAML uses indentation for structure, so the parser meets a key where it expects something else and stops.

    Check

    actionlint .github/workflows/ci.yml   # names the line and column of the bad key
    python3 -c "import yaml,sys; yaml.safe_load(open(sys.argv[1]))" .github/workflows/ci.yml   # needs PyYAML; 'did not find expected key' or 'mapping values are not allowed' means indentation

    Fix

    Line the key up with its siblings using spaces, the same number per level as the rest of the file.

  2. A tab is used for indentation. YAML does not allow tabs there, so the parser fails with found character that cannot start any token.

    Check

    grep -nP '\t' .github/workflows/ci.yml   # any line printed holds a tab

    Fix

    Replace the tabs with spaces, and set your editor to insert spaces in YAML files.

  3. A value contains : or starts with a character YAML treats as syntax (*, &, !, @, a backtick, { or [). The parser reads it as a new key or a tag instead of plain text.

    Check

    sed -n '<line-1>,<line>p' .github/workflows/ci.yml   # the reported line and the one above: an unquoted value with ': ' or a leading * & ! @ { [

    Fix

    Quote the value, or move a long command into a run: | block.

Prevent

Run actionlint in your editor or as a pre-commit hook.

Now learn it: break and fix it yourself in You have an error in your yaml syntax, then take Level 01 · Your first workflow. Open in the app.

The workflow is not valid

Error

The workflow is not valid. .github/workflows/ci.yml (Line: 4, Col: 3): Required property is missing: runs-on

What it means

The file is valid YAML, but GitHub's workflow schema rejects it, so the run fails at once without starting a job. Line: 4, Col: 3 is where the bad key is. Required property is missing names a key that must be there; Unexpected value 'x' names a key GitHub does not allow in that place.

Causes, most likely first

  1. A required key is missing, such as runs-on or steps in a job. The schema needs it, so GitHub reports Required property is missing: <key>.

    Check

    gh run view <run-id>   # the annotation repeats the line, column and missing key
    actionlint .github/workflows/ci.yml   # reports the same key locally

    Fix

    Add the key in the job the line points to, for example runs-on: ubuntu-latest.

  2. A key is misspelt or indented under the wrong parent (stepz:, or with: under the job instead of the step). The schema does not allow it there, so GitHub says Unexpected value '<key>'.

    Check

    actionlint .github/workflows/ci.yml   # 'unexpected key' lists the keys allowed there

    Fix

    Fix the spelling, or move the key under the right parent.

  3. A value is not allowed: an unknown permission name, or uses: without @ref. The schema checks values too, so the message points at that line.

    Check

    sed -n '<line>p' .github/workflows/ci.yml   # the key and value at the reported line

    Fix

    Use a valid permission name (contents, pull-requests, id-token, ...) and give every uses: a ref (owner/repo@<sha or tag>).

Prevent

actionlint knows the schema; run it in CI on workflow changes.

Now learn it: break and fix it yourself in Required property is missing: runs-on, You have an error in your yaml syntax, then take Level 01 · Your first workflow. Open in the app.

Nothing runs after a push

Error

(no run appears in the Actions tab)

What it means

No run was created, so there is no log to read. When you push or open a pull request, GitHub checks each workflow's on: filters and the repository's settings; if nothing matches, it stays silent.

Causes, most likely first

  1. A branches, tags or paths filter excludes the event: branches: [master] when the branch is main, paths that do not match the changed files, or a tag pushed to a branches-only trigger. GitHub checks filters before creating a run, so nothing appears.

    Check

    gh workflow view ci.yml --yaml | grep -n -A8 '^on:'   # compare the filters with the branch, tag and files you pushed
    git diff --name-only HEAD~1   # does any changed file match the paths filter?
    gh run list --workflow ci.yml --limit 5   # the last runs, and on which events

    Fix

    Match the filters to the real branch names and layout, and add tags: for tag pushes.

  2. The workflow or Actions is turned off: disabled by hand, a schedule disabled after 60 days without activity in a public repository, Actions disabled for the repository, or a fork where Actions was never enabled. A disabled workflow creates no runs.

    Check

    gh workflow list --all   # state disabled_manually or disabled_inactivity: the workflow is off
    gh api repos/<owner>/<repo>/actions/permissions --jq .enabled   # false: Actions is off for the repository

    Fix

    Turn it back on: gh workflow enable ci.yml, or Settings > Actions > General for the repository or the fork.

  3. GitHub skips the event by rule: a commit message with [skip ci], a pull request with a merge conflict (pull_request workflows wait for it to be fixed), or more than three tags pushed at once (no tag events at all).

    Check

    git log -1 --format=%B   # [skip ci] or [skip actions] in the message
    gh pr view <number> --json mergeable --jq .mergeable   # CONFLICTING: fix the conflict first

    Fix

    Push a commit without the skip marker, fix the merge conflict, or push tags a few at a time.

  4. The trigger is not where GitHub reads it: a schedule changed on a non-default branch (schedules run from the default branch only), or the file is outside .github/workflows or not .yml/.yaml. GitHub never sees the trigger, so it never fires.

    Check

    git ls-files .github/workflows   # the file is listed, ending in .yml or .yaml
    gh repo view --json defaultBranchRef --jq .defaultBranchRef.name   # schedules run from this branch only

    Fix

    Move the file to .github/workflows/ with a .yml extension, and merge schedule changes to the default branch.

Prevent

Use workflow_dispatch to test triggers by hand; avoid paths filters on required checks.

Now learn it: break and fix it yourself in The workflow never runs, Paths filter skips the code that changed, Pushing a tag does not release, The nightly job never runs, then take Level 02 · When workflows run. Open in the app.

Job depends on unknown job

Error

The workflow is not valid. .github/workflows/ci.yml (Line: 22, Col: 5): Job 'deploy' depends on unknown job 'tests'.

What it means

A job's needs: names a job id that does not exist in this file, so GitHub cannot work out the order of the jobs and rejects the workflow. Job 'deploy' depends on unknown job 'tests' means deploy waits for an id tests that is not under jobs:.

Causes, most likely first

  1. A job was renamed and its needs: still uses the old id. needs: is matched against ids exactly, so the old one is unknown.

    Check

    grep -n 'needs:' .github/workflows/ci.yml   # every name here must be a key under jobs:
    actionlint .github/workflows/ci.yml   # names the job ids that do exist

    Fix

    Use the current id in needs:, and rename ids and their needs together.

  2. needs: uses the job's display name: instead of its id. Only ids (the keys under jobs:) count, so a name such as Run tests is unknown.

    Check

    grep -n -B1 '    name:' .github/workflows/ci.yml   # the key on the line above name: is the id to use

    Fix

    Use the id, not the name.

  3. The job is in another workflow file. needs: only reaches jobs in the same file.

    Check

    grep -ln '^  tests:' .github/workflows/*.yml   # the id lives in another file

    Fix

    Move the job into this file, call the other workflow as a reusable workflow, or chain the two with workflow_run.

Prevent

Rename ids and their needs together.

Now learn it: break and fix it yourself in Deploy does not wait for the tests, then take Level 04 · Jobs that work together. Open in the app.

Unrecognized named-value: 'secrets'

Error

The workflow is not valid. .github/workflows/ci.yml (Line: 14, Col: 9): Unrecognized named-value: 'secrets'. Located at position 1 within expression: secrets.TOKEN != ''

What it means

An expression uses a context (secrets, env, steps, ...) in a place where it is not available, so GitHub rejects the workflow before running it. Unrecognized named-value: 'secrets' names the context; position 1 within expression points at it in the if:.

Causes, most likely first

  1. secrets is used in an if:. Secrets are never available in if: conditions, at job or step level, so the name is unknown there.

    Check

    grep -n 'if:.*secrets\.' .github/workflows/*.yml   # any line printed is this cause

    Fix

    Turn the secret into a flag in the job's env: (HAS_TOKEN: ${{ secrets.TOKEN != '' }}) and test it in a step: if: env.HAS_TOKEN == 'true'. Or test something available, such as github.event_name or github.repository.

  2. env or steps is used in a job-level if:. A job's if: is evaluated before the job has an env or any steps, so those names do not exist yet.

    Check

    grep -n -E '^    if:.*(env|steps)\.' .github/workflows/*.yml   # an if: at job indentation using env or steps

    Fix

    Use needs.<job>.outputs.<name>, vars or github in the job's if:, or move the condition to a step.

  3. The name is misspelt or is not a context (secret.TOKEN, github.events). GitHub knows only its own context names, so any other word is unrecognized.

    Check

    actionlint .github/workflows/ci.yml   # 'undefined variable' names the bad word

    Fix

    Use the right name from GitHub's list of contexts.

Prevent

Check GitHub's context availability table when writing conditions.

Now learn it: break and fix it yourself in Secret is empty on a fork pull request, then take Level 03 · Deciding at run time. Open in the app.

Unable to resolve action

Error

Error: Unable to resolve action `actions/chekout@v6`, repository not found

What it means

Before the first step, the runner downloads every action named in uses:, and it could not find this one, so the job fails in Set up job. repository not found means the owner or name is wrong or not visible; unable to find version means the repository exists but the tag, branch or SHA after @ does not.

Causes, most likely first

  1. The owner or repository name is wrong (actions/chekout). GitHub has no such repository, so the runner says repository not found.

    Check

    gh api repos/<owner>/<action-repo> --jq .full_name   # 404: no such repository, check the spelling

    Fix

    Correct the name.

  2. The action is in a private or internal repository that does not allow access from this one. The runner cannot see it, so it is reported as not found too.

    Check

    gh api repos/<owner>/<action-repo> --jq .visibility   # private or internal: check that repository's Settings > Actions > General > Access

    Fix

    In the action's repository, allow access from other repositories in the organization (Settings > Actions > General > Access).

  3. The ref after @ does not exist, or was deleted. The repository is found but not the version, so the message says unable to find version.

    Check

    git ls-remote https://github.com/<owner>/<action-repo> '<ref>'   # nothing printed: no tag or branch with that name
    gh api repos/<owner>/<action-repo>/releases/latest --jq .tag_name   # an existing version to use

    Fix

    Use an existing tag, or better a full commit SHA with the version in a comment.

Prevent

Let Dependabot (package-ecosystem: github-actions) update versions.

Now learn it: break and fix it yourself in Unable to resolve action, A third-party action's tag was moved, then take Level 01 · Your first workflow. Open in the app.

Recent account payments have failed

Error

The job was not started because recent account payments have failed or your spending limit needs to be increased. Please check the 'Billing & plans' section in your settings

What it means

GitHub refused to start the job because the account that pays for this repository's minutes cannot be charged. Nothing in the workflow is wrong and no runner was used. Jobs fail instantly in private repositories while public ones still run, because standard GitHub-hosted runners are free for public repositories.

Causes, most likely first

  1. The included minutes ran out with no payment method, or with a spending limit of zero. Private repositories (and larger runners) use paid minutes, so GitHub stops starting jobs.

    Check

    gh api /users/<user>/settings/billing/usage   # or Settings > Billing and plans: minutes used against the included amount

    Fix

    Add a payment method or raise the spending limit, or wait for the monthly reset. Making the repository public, or using a self-hosted runner, also avoids hosted minutes.

  2. A payment failed or the account is locked for billing. GitHub blocks paid usage until it is fixed, with the same message.

    Check

    # Settings > Billing and plans: a banner about a failed payment or a locked account

    Fix

    Update the payment method and retry the payment.

  3. The repository belongs to an organization, and the organization's billing has the problem, not yours. Minutes are charged to the repository's owner.

    Check

    gh api /organizations/<org>/settings/billing/usage   # the organization's usage

    Fix

    Ask an organization owner or billing manager to fix the payment method or limit.

Prevent

Cancel superseded runs with concurrency, add timeouts, and avoid macOS and Windows minutes (macOS about ten times and Windows close to twice the Linux price per minute) where Linux will do.

Now learn it: break and fix it yourself in Every push to a pull request runs to the end, The matrix makes 60 jobs, then take Level 05 · Fast and reliable builds. Open in the app.

Workflow does not have 'workflow_dispatch' trigger

Error

could not create workflow dispatch event: HTTP 422: Workflow does not have 'workflow_dispatch' trigger

What it means

gh workflow run (or the API) asked GitHub to start the workflow by hand. GitHub read the workflow file on the branch you named and found no workflow_dispatch: trigger. HTTP 422 means the request was understood but cannot be done.

Causes, most likely first

  1. The file on the branch you run from has no workflow_dispatch: under on:. GitHub reads the trigger from that branch's copy, so a trigger on another branch does not count.

    Check

    gh workflow view ci.yml --ref <branch> --yaml | grep -n workflow_dispatch   # nothing printed: this branch's file has no trigger

    Fix

    Add workflow_dispatch: to on: on that branch.

  2. No --ref was given, so GitHub used the default branch, whose file lacks the trigger. The Run workflow button also appears only once the default branch has it.

    Check

    gh workflow view ci.yml --yaml | grep -n -A4 '^on:'   # the file on the default branch

    Fix

    Merge the trigger to the default branch. Before that, if GitHub already knows the workflow (it is on the default branch or has run before), gh workflow run ci.yml --ref <branch> runs it from a branch whose file has the trigger.

Prevent

Add workflow_dispatch to workflows you may need to run by hand, with inputs for what changes between runs.

Now learn it: break and fix it yourself in workflow_dispatch input is empty, then take Level 03 · Deciding at run time. Open in the app.

Process completed with exit code 1

Error

Error: Process completed with exit code 1.

What it means

A command in a run: step exited with a code other than 0, so the runner marked the step as failed. Exit code 1 is a general failure; the real reason is in the command's own output just above this line.

Causes, most likely first

  1. A command really failed: a test, a build, a linter. Run steps use bash -e, so the first failing command stops the step and its code becomes the step's.

    Check

    gh run view <run-id> --log-failed   # the lines before the error are the failing command's own output

    Fix

    Fix what the command reports. Reproduce it locally with the same commands under bash -e script.sh.

  2. A command uses 1 for a normal result: grep with no match, diff when files differ, a test or [ ] that is false. Under bash -e that still stops the step, with no error printed.

    Check

    gh run view <run-id> --log-failed | tail -5   # the last command is a grep, diff or test, with no error message

    Fix

    Accept the expected code where you call it: grep -c x f || true, or if grep -q x f; then ...; fi. Do not use continue-on-error to hide it.

Prevent

Use shell: bash (adds pipefail) and test scripts locally.

Now learn it: break and fix it yourself in Process completed with exit code 1, continue-on-error hides a failing test, A variable set in one step is empty in the next, then take Level 01 · Your first workflow. Open in the app.

command not found (exit code 127)

Error

/home/runner/work/_temp/0f3c9b2e.sh: line 1: foo: command not found
Error: Process completed with exit code 127.

What it means

The shell looked for the program named before : command not found in every directory on PATH and did not find it. Exit code 127 is bash's code for that: the command, or a script's interpreter, was not found. The tool often works on your laptop because you installed it there.

Causes, most likely first

  1. The runner image does not include the tool. The step calls it, and there is nothing to run.

    Check

    command -v foo || echo 'foo is not on PATH'   # in the failing step

    Fix

    Install it in a step, use a setup-* action (setup-node, setup-python, ...), or run the job in a container that has it. Pin the version.

  2. It is installed in a directory that is not on PATH, or that was added to $GITHUB_PATH in the same step. $GITHUB_PATH changes apply from the next step, so the current step still cannot find it.

    Check

    echo "$PATH" | tr ':' '\n'   # is the install directory listed?
    ls -l <install-dir>/foo   # the file exists, so only PATH is missing it

    Fix

    Write the directory to $GITHUB_PATH in one step and use the tool from the next one, or call it by its full path.

  3. It was installed in another job, or by a setup step that was skipped. Each job runs on a fresh machine, so nothing carries over.

    Check

    gh run view <run-id> --log | grep -n -i 'setup\|install'   # the install step is in another job, or was skipped

    Fix

    Install the tool in every job that uses it, or pass built files between jobs as an artifact.

Prevent

Pin tool versions and install them explicitly.

Now learn it: break and fix it yourself in Process completed with exit code 1, then take Level 01 · Your first workflow. Open in the app.

Killed: exit code 137 (out of memory)

Error

Killed
Error: Process completed with exit code 137.

What it means

The process was killed with signal 9, SIGKILL: 137 is 128 + 9. On a runner this almost always means the Linux out-of-memory killer stopped it because the job used more memory than the machine has. Killed is the shell reporting that, often as the last line of the step.

Causes, most likely first

  1. The step used more memory than the runner has: parallel test workers, a large webpack or Gradle build, a Docker build. The kernel kills the biggest process to keep the machine alive, which gives 137.

    Check

    free -m   # in a step before the heavy one: the memory available
    sudo dmesg | grep -i -E 'killed process|out of memory'   # in a later step with if: always(); a line here confirms the out-of-memory killer

    Fix

    Use less memory at once: fewer parallel workers (jest --maxWorkers=2, make -j2, Gradle --max-workers=2), or split the job. Otherwise use a larger runner.

  2. Node.js reached its own heap limit, which can be lower than the machine's memory. It stops itself with JavaScript heap out of memory instead of being killed.

    Check

    gh run view <run-id> --log-failed | grep -i 'heap out of memory'   # present: Node.js's own limit, not the machine's

    Fix

    Raise the heap with NODE_OPTIONS=--max-old-space-size=4096 when the runner has that memory, or reduce parallel work.

  3. A container with a memory limit (docker run --memory, options: --memory on a service) was killed when it reached that limit. Any SIGKILL gives 137.

    Check

    docker inspect <container> --format '{{.State.OOMKilled}}'   # true: the container's own limit, not the runner's

    Fix

    Raise the container's limit to what it really needs, or reduce its memory use.

Prevent

Set worker counts in CI explicitly instead of letting tools use every core.

Now learn it: take Level 05 · Fast and reliable builds. Open in the app.

The runner lost communication with the server

Error

The hosted runner: GitHub Actions 2 lost communication with the server. Anything in your workflow that terminates the runner process, starves it for CPU/Memory, or blocks its network access can cause this error.

What it means

The runner stopped sending updates to GitHub in the middle of the job, so GitHub ended the job without a failing step. The runner process was killed, starved of CPU or memory, or cut off from the network.

Causes, most likely first

  1. The machine ran out of memory or CPU, the same pressure that gives exit code 137. The runner process itself is starved or killed, so it stops reporting.

    Check

    gh run view <run-id> --log | tail -50   # the last step that ran: a heavy build or test?

    Fix

    Reduce memory and CPU use in that step (fewer parallel workers, smaller builds) or use a larger runner.

  2. On a self-hosted runner, a step stopped or restarted the runner, or the machine shut down, rebooted or lost its network. GitHub stops hearing from it.

    Check

    # self-hosted: the runner's _diag/Runner_*.log and _diag/Worker_*.log around the time of the job
    journalctl -u 'actions.runner.*' --since '<time>'   # a stop or restart of the runner service at that time

    Fix

    Check the machine's memory, disk and network, and make sure nothing (updates, autoscaling, a reboot) stops the runner service during a job.

  3. A hosted runner was lost for a reason outside your workflow. It is rare, and the same job passes when run again.

    Check

    gh run view <run-id> --log | tail -20   # the last step was a light one, and other runs of this job pass

    Fix

    Re-run the job once (gh run rerun <run-id> --failed). If it happens again, treat it as the first cause.

Prevent

Set timeout-minutes and explicit worker counts; watch self-hosted runners' memory and disk.

Now learn it: take Level 05 · Fast and reliable builds. Open in the app.

The operation was canceled

Error

Error: The operation was canceled.

What it means

Something stopped the job from outside; it did not fail by itself. The reason is elsewhere: in another job of the same run, in a newer run, or with a person.

Causes, most likely first

  1. fail-fast: another job in the same matrix failed, and GitHub canceled the rest (the default). The canceled jobs show this message, not the real failure.

    Check

    gh run view <run-id>   # one matrix job failed (X), the others show as canceled

    Fix

    Fix the first failure. Set fail-fast: false to see every matrix result.

  2. A newer run in the same concurrency group with cancel-in-progress: true replaced this one.

    Check

    grep -n -A2 concurrency .github/workflows/*.yml   # cancel-in-progress: true
    gh run list --workflow ci.yml --limit 5   # a newer run on the same branch

    Fix

    Nothing to fix if the newer run is the one you want; read that run instead.

  3. Someone canceled the run in the Actions tab or with gh run cancel.

    Check

    gh run view <run-id>   # the annotation says: The run was canceled by <user>

    Fix

    Ask why, and re-run it if it should finish.

Prevent

Use timeouts and concurrency deliberately.

Now learn it: break and fix it yourself in The operation was canceled, The job ran for 6 hours, then take Level 04 · Jobs that work together. Open in the app.

Exceeded the maximum execution time

Error

The job running on runner GitHub Actions 3 has exceeded the maximum execution time of 360 minutes.

What it means

The job ran until its time limit and GitHub killed it. 360 minutes is the default timeout-minutes for a job on a GitHub-hosted runner; a smaller number in the message is your own setting.

Causes, most likely first

  1. A process never exits: a server started in the foreground, a watch mode, a test runner waiting for file changes. The step waits for it until the limit.

    Check

    gh run view <run-id> --log | tail -50   # the last output is a server 'listening on' or 'watching for changes'

    Fix

    Start servers in the background (npm start &) and wait for them with a health check; run tests without watch mode (CI=true, --watchAll=false).

  2. A command waits for input that never comes: a confirmation prompt, a password, a licence question. A job has no terminal, so it waits until the limit.

    Check

    gh run view <run-id> --log | tail -20   # the last line is a question, such as 'Continue? [y/N]'

    Fix

    Pass the non-interactive flag (-y, --yes, --non-interactive, DEBIAN_FRONTEND=noninteractive).

  3. The work is slow or deadlocked and there is no tighter timeout, so it uses the full 360 minutes.

    Check

    gh run list --workflow ci.yml --limit 10   # usual durations: this run took far longer

    Fix

    Set timeout-minutes close to the real duration so a hang fails fast, and split or cache the slow work.

Prevent

Every job gets timeout-minutes.

Now learn it: break and fix it yourself in The job ran for 6 hours, then take Level 05 · Fast and reliable builds. Open in the app.

Waiting for a runner to pick up this job

Error

Requested labels: ubuntu-lastest
Waiting for a runner to pick up this job...

What it means

The job is queued and no runner has taken it. Requested labels are the labels from runs-on:; a runner must have all of them, be online and idle, and be allowed to run jobs for this repository. The job can wait for minutes or hours.

Causes, most likely first

  1. No runner has all the requested labels: a typo (ubuntu-lastest) or a self-hosted label no runner has. GitHub waits for a runner that never comes.

    Check

    gh run view <run-id> --log | grep -i 'requested labels'   # compare with a real label such as ubuntu-latest
    gh api repos/<owner>/<repo>/actions/runners --jq '.runners[] | {name, status, busy, labels: [.labels[].name]}'   # needs admin access; no runner has all the labels

    Fix

    Fix the label in runs-on:.

  2. The self-hosted runner with those labels is offline or busy with another job. The job waits until it is free.

    Check

    gh api repos/<owner>/<repo>/actions/runners --jq '.runners[] | {name, status, busy}'   # status offline, or busy true

    Fix

    Start the runner service, or add runners.

  3. The organization's runner group does not include this repository (public repositories are left out unless allowed). The runner exists but may not take this job.

    Check

    gh api orgs/<org>/actions/runner-groups --jq '.runner_groups[] | {name, visibility, allows_public_repositories}'   # limited to selected repositories, or not allowing public ones

    Fix

    Add the repository to the runner group, or allow public repositories if that is intended.

  4. A concurrency limit is reached: the plan's limit on jobs running at once, or a larger runner's maximum. Jobs wait for a free slot.

    Check

    gh run list --status in_progress --limit 50   # many runs in progress at once

    Fix

    Wait, cancel superseded runs with concurrency:, or raise the limits.

Prevent

Keep runner labels in one place and review them.

Now learn it: break and fix it yourself in Waiting for a runner to pick up this job, Fork pull requests run on your self-hosted runner, then take Level 05 · Fast and reliable builds. Open in the app.

No space left on device

Error

ERROR: failed to build: failed to solve: ... no space left on device
You are running out of disk space. The runner will stop working when the machine runs out of disk space.

What it means

A step tried to write to disk and the runner's disk was full. Builds, Docker images, caches and dependencies all share the runner's free space, and a big build can use it up halfway through.

Causes, most likely first

  1. The build, its images and its caches need more space than is free. Large Docker builds and dependency caches are the usual cause.

    Check

    df -h   # in a step before the build: little space left on /
    du -sh ~/.cache ~/.npm 2>/dev/null; docker system df   # in a later step with if: always(): what grew

    Fix

    Slim the build (multi-stage images, a .dockerignore), and remove images and build cache you no longer need between steps.

  2. Preinstalled toolchains you do not use take several GB of a hosted runner's disk, leaving less for your build.

    Check

    du -sh /usr/local/lib/android /usr/share/dotnet /opt/ghc   # several GB each that your build does not need

    Fix

    Remove unused preinstalled toolchains before the build (for example sudo rm -rf /usr/local/lib/android /usr/share/dotnet /opt/ghc on a hosted runner), or use a larger runner.

  3. On a self-hosted runner, work directories, images and caches from earlier jobs pile up. Nothing removes them by default.

    Check

    df -h; docker system df   # self-hosted: images and build cache left by earlier jobs

    Fix

    Clean the work directory and Docker on a schedule, or use ephemeral runners that start clean for every job.

Prevent

Keep images small; watch build size.

Now learn it: break and fix it yourself in No space left on device, then take Level 05 · Fast and reliable builds. Open in the app.

fatal: No names found, cannot describe anything

Error

fatal: No names found, cannot describe anything.

What it means

git describe names the current commit after the nearest tag in its history, such as v1.4.0-3-gabc1234. No names found means it found no tag at all to start from. Versioning and changelog steps fail only in CI because CI clones differently from your laptop.

Causes, most likely first

  1. actions/checkout makes a shallow clone of one commit (fetch-depth: 1) without tags. describe has no tags and no history to search.

    Check

    git rev-parse --is-shallow-repository   # true: only the last commit was fetched
    git tag | wc -l   # 0: no tags were fetched

    Fix

    Set fetch-depth: 0 on actions/checkout. git fetch --tags alone is not enough in a shallow clone, because the history back to the tag is still missing; use git fetch --unshallow --tags.

  2. The repository has only lightweight tags. git describe without --tags uses annotated tags only, so it says No annotated tags can describe.

    Check

    git describe --tags   # works: the tags are lightweight

    Fix

    Call git describe --tags, or create annotated tags (git tag -a v1.0.0 -m v1.0.0).

  3. The repository has no tags yet (a new repository, or a fork without tags). There is nothing to describe from.

    Check

    git ls-remote --tags origin | head   # nothing printed: no tags on the remote

    Fix

    Create the first tag, or use git describe --tags --always to fall back to the commit id.

Prevent

Fetch history only in jobs that need it.

Now learn it: break and fix it yourself in fatal: No names found, cannot describe anything, then take Level 02 · When workflows run. Open in the app.

Service container unreachable

Error

Error: getaddrinfo EAI_AGAIN postgres
Error: connect ECONNREFUSED 127.0.0.1:5432

What it means

The tests could not connect to the database service. getaddrinfo EAI_AGAIN postgres means the name postgres could not be turned into an address. ECONNREFUSED 127.0.0.1:5432 means nothing accepted the connection on that port on the runner.

Causes, most likely first

  1. The steps run on the runner but connect to the service by its name. Service names resolve only inside a job container (the job's Docker network), so the lookup fails.

    Check

    getent hosts postgres || echo 'no such name: this step runs on the runner, use localhost'

    Fix

    On the runner, connect to localhost with the port mapped; in a job with container:, use the service name.

  2. The port is not mapped to the runner. Without ports:, the service listens only on its Docker network, so localhost refuses the connection.

    Check

    docker ps   # PORTS column empty for the service: the port is not mapped

    Fix

    Add ports: ["5432:5432"] to the service.

  3. The service is not ready yet. The container starts, but the database needs a few seconds before it accepts connections, so early connections are refused.

    Check

    pg_isready -h localhost -p 5432   # 'no response' at first, then 'accepting connections': it was not ready yet

    Fix

    Add a health check in options (--health-cmd pg_isready --health-interval 5s --health-retries 10); the job then waits for it before the steps.

Prevent

Write the connection settings for where the steps run.

Now learn it: break and fix it yourself in getaddrinfo EAI_AGAIN postgres, then take Level 05 · Fast and reliable builds. Open in the app.

toomanyrequests: You have reached your pull rate limit

Error

Error response from daemon: toomanyrequests: You have reached your pull rate limit. You may increase the limit by authenticating and upgrading: https://www.docker.com/increase-rate-limit

What it means

Docker Hub refused to send the image because too many pulls came from this client in a short time. toomanyrequests is Docker Hub's rate-limit answer; signing in raises the limit. It fails now and then, in a build, a services: container or a container: job.

Causes, most likely first

  1. The pull is anonymous. Docker Hub limits anonymous pulls per IP address, and a hosted runner's address is shared with many other jobs, so the limit can be used up before your job starts.

    Check

    gh run view <run-id> --log | grep -i -B3 toomanyrequests   # which pull failed: a build, a services: or a container: image
    grep -n -A3 'docker/login-action\|credentials:' .github/workflows/*.yml   # nothing printed: every pull is anonymous

    Fix

    Log in before pulling: docker/login-action with a Docker Hub username and access token stored as secrets. services: and container: images are pulled before any step, so give them credentials: too.

  2. You are logged in, but the account's own pull limit is used up: many jobs or matrix legs pull the same images with one account.

    Check

    gh run list --limit 20   # many runs at once, all pulling the same images

    Fix

    Pull less: cache layers, reuse built images, or keep copies of your base images in ghcr.io or your own registry.

Prevent

Keep copies of the base images you depend on in your own registry, and cache layers so builds pull less.

Now learn it: take Level 05 · Fast and reliable builds. Open in the app.

detected dubious ownership in repository

Error

fatal: detected dubious ownership in repository at '/__w/shop/shop'

What it means

Git refuses to work in a repository whose directory is owned by a different user from the one running git, a safety check since Git 2.35.2. /__w/shop/shop is the workspace path inside a job container. Checkout passed, but later git commands fail.

Causes, most likely first

  1. The job runs in a container as a different user from the owner of the workspace. The workspace is mounted from the runner, so its owner id differs, and git refuses.

    Check

    id -u; stat -c %u "$GITHUB_WORKSPACE"   # in the failing step: two different numbers

    Fix

    Mark the workspace safe before the git commands: git config --global --add safe.directory "$GITHUB_WORKSPACE".

  2. actions/checkout marked the directory safe only for its own git commands, in a temporary config. Later steps do not have that setting.

    Check

    git config --show-origin --get-all safe.directory   # nothing printed: no safe.directory for this step's git

    Fix

    Add the same safe.directory line once in the job's first step after checkout.

  3. On a self-hosted runner, the work folder was created by another user: a different service account, or a step that ran as root.

    Check

    ls -ldn "$GITHUB_WORKSPACE" "$GITHUB_WORKSPACE/.git"   # the owner uid differs from the runner's user

    Fix

    Run the runner service as one user and give that user ownership of its work folder (chown -R), instead of running steps as root.

Prevent

Add the safe.directory line once in the job's first step instead of in each script.

Now learn it: take Level 05 · Fast and reliable builds. Open in the app.

Unable to process file command 'env' successfully

Error

Error: Unable to process file command 'env' successfully.
Error: Invalid format 'second line of the value'

What it means

Steps pass values to later steps by writing lines to the files $GITHUB_ENV and $GITHUB_OUTPUT. After the step, the runner read the file and found a line it could not parse. Invalid format 'second line of the value' quotes that line, usually the second line of a multi-line value. The step itself passed.

Causes, most likely first

  1. A value with line breaks (JSON, a key, command output) was written as name=value. Every line must be name=value, so the second line has no name.

    Check

    grep -n -v -E '^[A-Za-z_][A-Za-z0-9_]*(=|<<)' "$GITHUB_ENV" | cut -d: -f1   # at the end of the step: line numbers that are not NAME=value or NAME<<DELIMITER, without printing values

    Fix

    Use the delimiter form: { echo 'CONFIG<<EOF'; cat config.json; echo 'EOF'; } >> "$GITHUB_ENV", or make JSON one line with jq -c.

  2. The delimiter form is used, but the closing delimiter is not alone on its own line: the value has no final line break, so EOF lands at the end of its last line, or the value itself contains the delimiter. The runner never finds the end (Matching delimiter not found).

    Check

    tail -c 1 config.json | od -c   # no \n: the file has no final line break

    Fix

    Print a line break before the delimiter (echo before echo 'EOF'), and use a random delimiter for content you did not write: d=$(openssl rand -hex 8).

Prevent

Keep outputs to one line where you can; use a random delimiter for content you did not write.

Now learn it: break and fix it yourself in The set-output command is deprecated, needs.<job>.outputs is empty, then take Level 03 · Deciding at run time. Open in the app.

The version '3.1' was not found

Error

The version '3.1' with architecture 'x64' was not found for Ubuntu 24.04.
The list of all available versions can be found here: https://raw.githubusercontent.com/actions/python-versions/main/versions-manifest.json

What it means

actions/setup-python looked for the requested Python in its list of builds for this runner image and architecture and did not find it. The version in quotes is the one it received, which may not be the one you wrote.

Causes, most likely first

  1. YAML read an unquoted 3.10 as the number 3.1. Numbers lose their trailing zero, so setup-python asks for 3.1, which does not exist.

    Check

    grep -n -A2 'python-version' .github/workflows/*.yml   # 3.10 without quotes becomes 3.1

    Fix

    Quote versions: "3.10".

  2. The version you wrote has no build for this runner image or architecture: a very old or very new Python, or a new image the builds do not cover yet.

    Check

    # the version in the error is exactly the one you wrote: look it up in the versions-manifest.json linked in the message

    Fix

    Choose a version listed for this image, or run on an image that has it.

Prevent

Always quote version numbers in YAML.

Now learn it: break and fix it yourself in The version '3.1' was not found, then take Level 04 · Jobs that work together. Open in the app.

Resource not accessible by integration

Error

GraphQL: Resource not accessible by integration (addComment)

What it means

A step called the GitHub API with the GITHUB_TOKEN and GitHub answered 403: the token is valid but not allowed to do this. (addComment) names the operation that was refused. by integration means the token belongs to an app, here GitHub Actions.

Causes, most likely first

  1. A permissions: block leaves out the scope the call needs. Every scope not listed becomes none, so the token cannot make this call.

    Check

    gh run view <run-id> --log | grep -A15 'GITHUB_TOKEN Permissions'   # Set up job: the scopes the token really had; the needed scope is missing or read
    grep -n -A6 'permissions:' .github/workflows/<file>.yml   # the scopes asked for

    Fix

    Add the one scope needed to that job: pull-requests: write to comment on a pull request, contents: write to create a release.

  2. There is no permissions: block and the repository or organization default is read, so every scope is read-only.

    Check

    gh api repos/<owner>/<repo>/actions/permissions/workflow --jq .default_workflow_permissions   # read

    Fix

    Add a permissions: block with the one scope needed, rather than changing the default to write.

  3. The run is a pull request from a fork, or a Dependabot run. Forks always get a read-only token, whatever the file asks; Dependabot runs get read-only by default.

    Check

    gh api repos/<owner>/<repo>/actions/runs/<run-id> --jq '{event, actor: .actor.login, head: .head_repository.full_name}'   # another head repository: a fork; dependabot[bot]: Dependabot

    Fix

    For Dependabot runs, a permissions: block can raise it. For forks, comment from a separate trusted workflow (workflow_run) that never runs the fork's code.

  4. The call targets another repository. The GITHUB_TOKEN works only on the repository the workflow runs in.

    Check

    gh run view <run-id> --log-failed | grep -i 'repos/'   # the API path names another owner/repo

    Fix

    Use a GitHub App token for that repository (actions/create-github-app-token) with only the permission needed.

  5. The step uses a personal access token that lacks the scope or the repository. The message then says by personal access token.

    Check

    gh auth status   # with GH_TOKEN set to that token: its scopes, not the token

    Fix

    Give the token the one permission on that repository (fine-grained), or use the GITHUB_TOKEN where it can do the job.

Prevent

Declare permissions per workflow, least privilege first.

Now learn it: break and fix it yourself in Resource not accessible by integration, then take Level 07 · Least privilege. Open in the app.

denied: permission_denied: write_package

Error

denied: permission_denied: write_package

What it means

The login to ghcr.io worked, but the registry refused the push: the token may read this package but not write it. write_package is the permission that was missing.

Causes, most likely first

  1. The GITHUB_TOKEN has no packages: write in this job (fork pull requests never get it). Login only needs a valid token; the push needs write.

    Check

    gh run view <run-id> --log | grep -A15 'GITHUB_TOKEN Permissions'   # Set up job: the scopes the token really had; Packages: write?

    Fix

    Add packages: write to the publishing job only.

  2. The package already exists and this repository has no write access to it. A package created by another repository or by hand is not writable by this repository's token until you grant it.

    Check

    gh api orgs/<org>/packages/container/<name> --jq .repository.full_name   # with a token that can read packages; another or no repository: not linked to this one

    Fix

    Open the package's settings, Manage Actions access, and add this repository with the Write role.

  3. The image name is under another owner (ghcr.io/other-org/app). The GITHUB_TOKEN can publish only under the repository's owner.

    Check

    grep -n 'ghcr.io/' .github/workflows/*.yml   # the name after ghcr.io/ must be this repository's owner, in lower case

    Fix

    Push to ghcr.io/<owner>/<name> with the repository's owner.

Prevent

Grant packages: write only in the publishing job.

Now learn it: break and fix it yourself in denied: permission_denied: write_package, then take Level 07 · Least privilege. Open in the app.

A secret is empty

Error

deploy: DEPLOY_TOKEN is empty; cannot authenticate to production

What it means

The step's own check found the secret empty. A secret the run cannot read is not an error in GitHub Actions: ${{ secrets.NAME }} just becomes an empty string, so the step fails later as if the secret were not set, although it is set in the settings.

Causes, most likely first

  1. The run is a pull request from a fork, or a Dependabot run. GitHub gives them no Actions secrets, so untrusted code cannot read them.

    Check

    gh api repos/<owner>/<repo>/actions/runs/<run-id> --jq '{event, actor: .actor.login, head: .head_repository.full_name}'   # another head repository: a fork; dependabot[bot]: Dependabot
    gh secret list --app dependabot   # Dependabot runs read only these

    Fix

    Skip the step for forks (if: github.event.pull_request.head.repo.full_name == github.repository); add what Dependabot needs to Dependabot secrets.

  2. It is an environment secret and the job has no environment:. Only jobs that name the environment receive its secrets.

    Check

    gh secret list --env production   # the secret is here
    grep -n 'environment:' .github/workflows/<file>.yml   # not set on this job

    Fix

    Add environment: production to the job.

  3. The step is in a reusable workflow and the caller did not pass the secret. Called workflows receive only the secrets passed to them.

    Check

    grep -n -A4 'uses: .*\.github/workflows/' .github/workflows/*.yml   # no secrets: under the calling job

    Fix

    Pass it (secrets: DEPLOY_TOKEN: ${{ secrets.DEPLOY_TOKEN }}) or use secrets: inherit (same organization or enterprise).

  4. The name is misspelt, or it is an organization secret not shared with this repository. Either way the expression finds nothing and gives an empty string, with no error.

    Check

    [ -n "$DEPLOY_TOKEN" ] && echo set || echo empty   # in the step, with env: DEPLOY_TOKEN: ${{ secrets.DEPLOY_TOKEN }}; presence only, never the value
    gh secret list   # the exact name
    gh secret list --org <org>   # VISIBILITY selected: is this repository on its list?

    Fix

    Fix the name, or share the organization secret with this repository.

Prevent

Fail early with a clear message when a required secret is empty.

Now learn it: break and fix it yourself in Secret is empty on a fork pull request, Secrets are empty on Dependabot pull requests, Environment secret is empty, Reusable workflow gets empty secrets, then take Level 06 · Keeping secrets secret. Open in the app.

gh needs GH_TOKEN

Error

gh: To use GitHub CLI in a GitHub Actions workflow, set the GH_TOKEN environment variable. Example:
  env:
    GH_TOKEN: ${{ github.token }}

What it means

The GitHub CLI needs a token, and in a workflow it reads one only from the GH_TOKEN or GITHUB_TOKEN environment variable. Neither was set (or the one set was empty) in this step, so gh stopped before calling GitHub.

Causes, most likely first

  1. The step does not set GH_TOKEN. Actions does not put the token in a step's environment by itself.

    Check

    [ -n "$GH_TOKEN" ] && echo 'GH_TOKEN set' || echo 'GH_TOKEN empty'   # in the step; shows presence, not the token

    Fix

    Set GH_TOKEN: ${{ github.token }} in the step's env:.

  2. GH_TOKEN comes from a secret that is empty in this run, such as a fork or Dependabot pull request. An empty variable counts as not set.

    Check

    grep -n 'GH_TOKEN' .github/workflows/*.yml   # set from secrets.<name>?
    gh api repos/<owner>/<repo>/actions/runs/<run-id> --jq '{event, actor: .actor.login, head: .head_repository.full_name}'   # another head repository: a fork; dependabot[bot]: Dependabot

    Fix

    Use ${{ github.token }} where it is enough, or skip the step for forks.

Prevent

Use a shared step template for gh calls.

Now learn it: break and fix it yourself in Resource not accessible by integration, Pushing a tag does not release, then take Level 07 · Least privilege. Open in the app.

Permission to repo.git denied to github-actions[bot]

Error

remote: Permission to nimbus/shop.git denied to github-actions[bot].
fatal: unable to access 'https://github.com/nimbus/shop/': The requested URL returned error: 403

What it means

Git sent the push with the job's token and GitHub refused it. denied to github-actions[bot] means the GITHUB_TOKEN was used and has no write access here; 403 means the token was recognised but not allowed. Branch protection gives a different error (GH006: Protected branch update failed or GH013: Repository rule violations found).

Causes, most likely first

  1. The job's GITHUB_TOKEN has contents: read: the default for repositories and organizations created since 2023, or a permissions: block without contents: write. Reading is enough to check out, not to push.

    Check

    gh run view <run-id> --log | grep -A15 'GITHUB_TOKEN Permissions'   # Set up job: the scopes the token really had; Contents: read means this job cannot push
    gh api repos/<owner>/<repo>/actions/permissions/workflow --jq .default_workflow_permissions   # read: the default

    Fix

    Grant contents: write to that job only. If the push then fails with GH006 or GH013, the branch is protected: open a pull request instead.

  2. The run is a pull request from a fork. Its token is always read-only, whatever the file asks.

    Check

    gh api repos/<owner>/<repo>/actions/runs/<run-id> --jq '{event, actor: .actor.login, head: .head_repository.full_name}'   # another head repository: a fork; dependabot[bot]: Dependabot

    Fix

    Do not push from fork pull requests; push from a trusted workflow after the merge.

  3. The push goes to another repository. The GITHUB_TOKEN can write only to the repository the workflow runs in.

    Check

    git remote get-url origin   # in the step: another owner/repo

    Fix

    Use a GitHub App token with Contents: write on that repository.

Prevent

Prefer pull requests from bots over direct pushes.

Now learn it: break and fix it yourself in permissions: write-all on a lint job, then take Level 07 · Least privilege. Open in the app.

A push by the workflow starts no other workflow

Error

(no run appears after a commit or tag pushed with the GITHUB_TOKEN)

What it means

Nothing failed: GitHub chose not to start the workflow because the push or tag was made with the GITHUB_TOKEN. Events caused by that token (except workflow_dispatch and repository_dispatch) start no new runs, so workflows cannot trigger each other in a loop.

Causes, most likely first

  1. The commit or tag was pushed with the GITHUB_TOKEN, which actions/checkout sets up for git by default. By design that push starts no runs.

    Check

    gh run list --event push --limit 5   # no run for the pushed commit or tag
    grep -n -B3 'git push\|gh release create' .github/workflows/*.yml   # pushed with checkout's credentials or GH_TOKEN: ${{ github.token }}

    Fix

    Call the next workflow directly (workflow_call, or gh workflow run, which the GITHUB_TOKEN may start), or push with a GitHub App token.

  2. The push happened, but the target workflow's filters do not match it (a branches-only trigger for a tag). It would not run with any token.

    Check

    gh workflow view release.yml --yaml | grep -n -A6 '^on:'   # tags: missing

    Fix

    Add tags: under on: push: in that workflow.

Prevent

Design release pipelines as one workflow, or chain with workflow_run.

Now learn it: break and fix it yourself in A tag pushed by the workflow starts nothing, Pushing a tag does not release, then take Level 07 · Least privilege. Open in the app.

API rate limit exceeded for installation

Error

HTTP 403: API rate limit exceeded for installation ID 41806354.

What it means

GitHub refused the API call because this token used up its quota. for installation ID ... means the token belongs to an app installation; in a workflow that is usually the GITHUB_TOKEN. Steps fail after many calls.

Causes, most likely first

  1. The hourly limit is used up: the GITHUB_TOKEN allows 1,000 REST requests per hour per repository (15,000 for repositories in a GitHub Enterprise Cloud account). A loop calling the API once per item uses it up fast.

    Check

    gh api rate_limit --jq .resources.core   # remaining 0 and a reset time: the hourly quota (this call does not count)

    Fix

    Use list endpoints with per_page=100 and --paginate, or GraphQL (gh pr list --json), cache results, or use a GitHub App token with a higher limit.

  2. A secondary rate limit: too many requests at once, or content created too fast. It applies whatever is left of the hourly quota, and the message says exceeded a secondary rate limit.

    Check

    gh api rate_limit --jq .resources.core   # requests still left: a secondary limit
    gh run view <run-id> --log | grep -i 'secondary rate limit\|retry-after'

    Fix

    Make calls one at a time, wait as long as the retry-after header says, and avoid parallel jobs writing at the same moment.

Prevent

Never call the API once per item in a loop.

Now learn it: break and fix it yourself in API rate limit exceeded for installation, then take Level 07 · Least privilege. Open in the app.

Expected — Waiting for status to be reported

Error

test  Expected — Waiting for status to be reported

What it means

Branch protection or a ruleset requires a check with this name, and nothing has reported it for the pull request's latest commit. Expected means GitHub is still waiting; it waits forever if no workflow reports that name, so the pull request cannot merge.

Causes, most likely first

  1. A paths or branches filter skipped the whole workflow. A skipped workflow reports nothing, so the check stays Expected (a job skipped by if: reports success instead).

    Check

    gh pr checks <number>   # the checks that did report: this one is missing
    gh workflow view ci.yml --yaml | grep -n -A6 'pull_request:'   # paths or branches that exclude this pull request

    Fix

    Let the workflow run for every pull request and skip work inside the job with if:.

  2. The required name does not match any job's check name: the job was renamed, or a matrix job's name includes its values (test (20)). GitHub matches names exactly.

    Check

    gh api repos/<owner>/<repo>/branches/main/protection/required_status_checks --jq '.contexts'   # required by branch protection
    gh api repos/<owner>/<repo>/rules/branches/main --jq '.[] | select(.type == "required_status_checks") | .parameters.required_status_checks'   # required by rulesets

    Fix

    Make the required name match the job's check name exactly, or rename the job back.

  3. The workflow does not run on this event (push only, not pull_request), or the pull request has a merge conflict, so pull_request workflows did not run.

    Check

    gh pr view <number> --json mergeable --jq .mergeable   # CONFLICTING: no pull_request runs until it is fixed
    grep -n -A4 '^on:' .github/workflows/*.yml   # pull_request: listed?

    Fix

    Add pull_request: to the workflow, or fix the merge conflict.

Prevent

Do not use paths filters on workflows that provide required checks; keep job names stable.

Now learn it: break and fix it yourself in Required check stuck at Expected, Merge queue waits for a check that never runs, then take Level 02 · When workflows run. Open in the app.

The merge queue never gets its checks

Error

test  Expected — Waiting for status to be reported   (on gh-readonly-queue/main/pr-42-...)

What it means

The merge queue tests each pull request on a temporary branch named gh-readonly-queue/main/... and waits for the required checks there. They stay Expected because no workflow ran on that branch, so the queue gives up and removes the pull request without merging.

Cause

  1. The workflows that report required checks do not listen to merge_group. The queue sends that event, not pull_request or push, so they never run on its branch.

    Check

    grep -n -A6 '^on:' .github/workflows/*.yml   # merge_group: missing from a workflow that reports a required check

    Fix

    Add merge_group: to the on: of every workflow that provides a required check.

Prevent

Add merge_group before turning the queue on.

Now learn it: break and fix it yourself in Merge queue waits for a check that never runs, then take Level 02 · When workflows run. Open in the app.

A step fails only on Windows

Error

Remove-Item: A parameter cannot be found that matches parameter name 'rf'.
Error: Process completed with exit code 1.

What it means

The step ran in PowerShell, not bash, because run: steps on Windows runners use pwsh unless told otherwise. Remove-Item: A parameter cannot be found that matches parameter name 'rf' means rm -rf became PowerShell's Remove-Item, which has no -rf option. The same script passes on Linux and macOS.

Causes, most likely first

  1. The step has no shell:, so Windows uses pwsh. Many Unix commands there are aliases for cmdlets with different options.

    Check

    # the step log shows: shell: C:\Program Files\PowerShell\7\pwsh.EXE -command ". '{0}'"

    Fix

    Set shell: bash on the step or in defaults: run: shell:; Git Bash is on Windows runners. Or write the step in PowerShell.

  2. The command does not exist on Windows at all, so PowerShell or cmd says it is not recognized.

    Check

    Get-Command <tool> -ErrorAction SilentlyContinue   # in a pwsh step: nothing printed, the tool is not on this runner

    Fix

    Use shell: bash (Git Bash has the common Unix tools), or install the tool.

  3. shell: cmd is used. cmd reports only the last command's exit code, so earlier errors pass silently and the failure shows up later.

    Check

    grep -n 'shell: cmd' .github/workflows/*.yml

    Fix

    Use pwsh or bash, which stop on errors.

Prevent

Use one default shell per workflow; test the matrix on pull requests.

Now learn it: break and fix it yourself in The same step fails only on Windows, then take Level 05 · Fast and reliable builds. Open in the app.

Files contain version https://git-lfs.github.com/spec/v1

Error

version https://git-lfs.github.com/spec/v1
oid sha256:4d7a2146...
size 18432

What it means

The file holds a Git LFS pointer, a small text stub that names the real file by its hash, instead of the data. The real content was never downloaded, so tests or builds read the stub.

Causes, most likely first

  1. actions/checkout does not fetch LFS objects by default, so only the pointers are checked out.

    Check

    head -1 data/prices.csv   # in a step: 'version https://git-lfs.github.com/spec/v1'
    git lfs ls-files   # the file is tracked by LFS

    Fix

    Set lfs: true on actions/checkout.

  2. LFS is fetched, but the download failed: the object was never pushed, or the account's LFS quota is used up. The pointer stays in place.

    Check

    gh run view <run-id> --log | grep -i 'lfs'   # errors about a missing object or a quota

    Fix

    Push the missing objects from a clone that has them (git lfs push --all origin), or raise the LFS quota.

Prevent

Cache .git/lfs, and keep large data out of jobs that do not need it.

Now learn it: break and fix it yourself in Tests read a Git LFS pointer, not the file, then take Level 02 · When workflows run. Open in the app.

Skip output since it may contain secret

Error

Skip output 'image' since it may contain secret.

What it means

The runner refused to pass a job output to the next job because its value contains the value of a secret. Secrets are masked everywhere, so the runner drops the output, and the next job gets an empty value. The job that set it shows this warning.

Causes, most likely first

  1. The output really contains a secret: a token in a URL, a connection string.

    Check

    gh run view <run-id>   # ANNOTATIONS: Skip output 'image' since it may contain secret

    Fix

    Pass only the non-secret part, and add the secret in the job that uses it.

  2. A secret holds a value that is not secret and also appears in the output: a registry host, a region, an account id. The runner masks any match, so the output is dropped.

    Check

    gh secret list   # settings such as REGISTRY or REGION stored as secrets

    Fix

    Keep non-sensitive settings in vars instead of secrets.

Prevent

Store only real credentials as secrets.

Now learn it: break and fix it yourself in Skip output since it may contain secret, then take Level 04 · Jobs that work together. Open in the app.

Bad credentials (HTTP 401)

Error

HttpError: Bad credentials

What it means

GitHub did not accept the token at all (HTTP 401): it is expired, revoked or malformed. That is different from 403, where the token is valid but not allowed. It usually fails at once, though it worked before.

Causes, most likely first

  1. A personal access token expired or was revoked, or a GitHub App token was used after it expired (they last one hour).

    Check

    gh auth status   # in the step, with GH_TOKEN set to the same token; shows the account, not the token

    Fix

    Create a new token, store it with gh secret set NAME, and re-run. Where the GITHUB_TOKEN can do the job, use it: it is new for every run. For other repositories, prefer a GitHub App token from actions/create-github-app-token, created in the job that uses it.

  2. The secret holds the wrong value: extra spaces, a line break, or a different token pasted.

    Check

    printf %s "$TOKEN" | wc -c; case "$TOKEN" in *[[:space:]]*) echo 'has whitespace';; esac   # length and stray spaces, not the value

    Fix

    Set the secret again with gh secret set NAME, which prompts for the value, so no newline is added.

  3. A secret with the same name in the environment or the organization holds an older token. Environment secrets win over repository secrets, which win over organization ones.

    Check

    gh secret list; gh secret list --env <env>; gh secret list --org <org>   # the same name in more than one place, and when each was updated

    Fix

    Update or delete the stale copy, so one place holds the token.

Prevent

Note each token's expiry date and renew it before then; prefer GitHub App tokens to personal ones.

Now learn it: take Level 07 · Least privilege. Open in the app.

Input required and not supplied

Error

Error: Input required and not supplied: github-token

What it means

The action declares this input as required in its action.yml and got no value, so it fails on its first line. github-token is the input's name. An empty value counts as not supplied.

Causes, most likely first

  1. The with: key is missing or spelt differently from the action's action.yml.

    Check

    grep -n -A6 'uses: <the action>' .github/workflows/*.yml   # the input is missing under with:, or spelt differently

    Fix

    Pass the input under with: with the exact name from the action's action.yml.

  2. The value comes from a secret that is empty in this run. Pull requests from forks and Dependabot get no Actions secrets, and a misspelt secret name is empty without an error.

    Check

    gh secret list   # the secret named in with: exists, with that exact name
    gh api repos/<owner>/<repo>/actions/runs/<run-id> --jq '{event, actor: .actor.login, head: .head_repository.full_name}'   # another head repository: a fork; dependabot[bot]: Dependabot

    Fix

    Make sure the secret exists for this run, and skip the step for forks with if: github.event.pull_request.head.repo.full_name == github.repository. For a GitHub token, ${{ github.token }} is always set.

Prevent

Read an action's inputs in its action.yml when you add it; fail early when a needed secret is empty.

Now learn it: break and fix it yourself in Secret is empty on a fork pull request, Secrets are empty on Dependabot pull requests, then take Level 06 · Keeping secrets secret. Open in the app.

could not read Username: terminal prompts disabled

Error

fatal: could not read Username for 'https://github.com': terminal prompts disabled
Error: The process '/usr/bin/git' failed with exit code 128

What it means

Git tried to clone or fetch another repository over HTTPS, had no credentials for it, and could not ask for a user name because a job has no terminal. Exit code 128 is git's code for a fatal error. It shows up when checking out a second repository, a private submodule or a private Go module.

Causes, most likely first

  1. The other repository is private and the GITHUB_TOKEN can read only the repository the workflow runs in. Git has no other credentials, so it wants a user name, or GitHub answers repository not found.

    Check

    cat .gitmodules   # submodule URLs pointing to other private repositories
    gh api repos/<owner>/<other-repo> --jq .full_name   # with the token the step uses; 404: it cannot see that repository

    Fix

    Give checkout a token that can read the other repository, token: ${{ secrets.READ_TOKEN }} (a fine-grained token with Contents: read on it, or a GitHub App token), with submodules: recursive for submodules; or an SSH deploy key with ssh-key:.

  2. A package manager, such as Go modules, fetches the private repository with its own git call, which does not use checkout's credentials.

    Check

    go env GOPRIVATE   # empty: Go treats the module as public

    Fix

    Set GOPRIVATE, and give git credentials for github.com in that step, for example gh auth setup-git with GH_TOKEN set to a read-only token.

  3. checkout ran with persist-credentials: false, so later git commands in the job have no credentials.

    Check

    grep -n 'persist-credentials' .github/workflows/*.yml   # false

    Fix

    Give credentials to the one step that needs them, rather than persisting them for every step.

Prevent

Give each workflow read access only to the repositories it needs, and prefer GitHub App tokens to personal ones.

Now learn it: take Level 07 · Least privilege. Open in the app.

Refusing to update a workflow without workflows permission

Error

! [remote rejected] HEAD -> update-deps (refusing to allow a GitHub App to create or update workflow `.github/workflows/ci.yml` without `workflows` permission)

What it means

The push includes a change to a file under .github/workflows, and the token used has no permission to change workflow files. GitHub guards those files separately, because changing them changes what runs with the repository's secrets.

Causes, most likely first

  1. The push used the GITHUB_TOKEN, which can never be given the workflows permission, so any commit touching a workflow file is rejected.

    Check

    git diff --name-only origin/main...HEAD | grep '^.github/workflows/'   # any file listed needs the workflows permission

    Fix

    Leave workflow files out of the bot's commits, or push with a GitHub App token that has Workflows: write. Changes to workflows deserve a human review anyway.

  2. A GitHub App without Workflows: write, or a personal access token without the workflow scope (classic) or Workflows: write (fine-grained). The message then names the app or a personal access token.

    Check

    gh auth status   # with GH_TOKEN set to that token: the scopes it has, without 'workflow'

    Fix

    Grant Workflows: write (or the workflow scope) only to the token that should change workflows.

Prevent

Let Dependabot update action versions in workflows: it has that permission.

Now learn it: break and fix it yourself in permissions: write-all on a lint job, then take Level 07 · Least privilege. Open in the app.

Cache not found for input keys

Error

Cache not found for input keys: npm-3e8f0f6a1c2b...

What it means

actions/cache looked for an entry with this exact key, then for any restore-keys prefix, and found nothing this run can read. The job carries on without a cache, so every run downloads everything again.

Causes, most likely first

  1. The key changes every run, because it includes github.sha or run_id. No run ever looks for a key an earlier run saved.

    Check

    grep -n 'key:' .github/workflows/*.yml   # github.sha or run_id in the key

    Fix

    Key on the contents: npm-${{ runner.os }}-${{ hashFiles('package-lock.json') }}, with restore-keys for a prefix match.

  2. The cache was saved on a branch this run cannot read. A run reads its own branch, the default branch and, for a pull request, its base branch; caches saved by pull requests are not shared back.

    Check

    gh cache list --key npm- --json key,ref,lastAccessedAt   # entries exist, but only on another ref

    Fix

    Let the default branch save the cache (run the workflow on push to main), so every branch can read it.

  3. The entry was removed: after 7 days unused, or when the repository passed its cache size limit and older entries were evicted.

    Check

    gh cache list --sort last_accessed_at --order asc --limit 5   # few or no entries left

    Fix

    Keep caches small and few, and save from the default branch regularly.

  4. path: differs from the step that saved it. The path is part of the cache version, so the same key still misses.

    Check

    gh run view <run-id> --log | grep -i cache   # the key it looked for, and whether a run saved one
    grep -n -A4 'actions/cache' .github/workflows/*.yml   # the same path: everywhere?

    Fix

    Use the same path: in every step that saves and restores that cache.

Prevent

Let setup-node or setup-python manage the cache with cache:.

Now learn it: break and fix it yourself in Cache not found for input keys, The build takes 20 minutes, then take Level 05 · Fast and reliable builds. Open in the app.

Unable to reserve cache: another job may be creating this cache

Error

Failed to save: Unable to reserve cache with key npm-Linux-3e8f0f6a, another job may be creating this cache.

What it means

At the end of the job, actions/cache tried to save an entry with this key and could not, because an entry with the same key exists or is being saved. Cache entries cannot be overwritten. It is a warning in the cache's Post step; the job still passes.

Causes, most likely first

  1. An earlier run already saved an entry with this exact key.

    Check

    gh cache list --key npm-   # an entry with this exact key already exists

    Fix

    Nothing is broken if the content is the same. To store new content, change the key: include hashFiles(...) of the lockfile.

  2. Several jobs in this run save the same key at once, often matrix jobs that share one key while building different content.

    Check

    grep -n 'key:' .github/workflows/*.yml   # no matrix values in the key of a matrix job

    Fix

    Add the matrix values to the key (${{ runner.os }}-${{ matrix.node }}).

Prevent

Key caches on what they contain, with restore-keys for a partial match.

Now learn it: break and fix it yourself in Cache not found for input keys, then take Level 05 · Fast and reliable builds. Open in the app.

No file matched to [**/requirements.txt]

Error

Error: No file in /home/runner/work/GitHub-Actions/GitHub-Actions matched to [**/requirements.txt or **/pyproject.toml], make sure you have checked out the target repository

What it means

setup-python with cache: pip needs a dependency file to build the cache key from, and found none matching its default patterns in the workspace. It stops before any test. This repository's own CI failed this way.

Causes, most likely first

  1. The dependency file has another name or path than the defaults (requirements.txt, pyproject.toml).

    Check

    git ls-files '*requirements*' pyproject.toml   # your file has another name or sits elsewhere

    Fix

    Add cache-dependency-path: requirements-dev.txt (your file), or add a requirements file.

  2. actions/checkout did not run before setup-python, so the workspace is empty.

    Check

    grep -n -B8 'setup-python' .github/workflows/*.yml   # no actions/checkout before it

    Fix

    Run actions/checkout first.

Prevent

Keep dependencies in a file even for one package.

Now learn it: break and fix it yourself in No file matched to [**/requirements.txt], then take Level 05 · Fast and reliable builds. Open in the app.

Dependencies lock file is not found

Error

Error: Dependencies lock file is not found in /home/runner/work/shop/shop. Supported file patterns: package-lock.json,npm-shrinkwrap.json,yarn.lock

What it means

setup-node with cache: npm (or yarn, pnpm) builds its cache key from the lockfile, and found none in the repository root. The supported file names are listed in the message.

Causes, most likely first

  1. No lockfile is committed. The cache has nothing to key on.

    Check

    git ls-files | grep -E 'package-lock|yarn.lock|pnpm-lock'   # nothing printed

    Fix

    Commit the lockfile.

  2. The lockfile is in a subfolder. setup-node looks in the root unless told otherwise.

    Check

    git ls-files '*package-lock.json'   # e.g. app/package-lock.json

    Fix

    Set cache-dependency-path: app/package-lock.json.

  3. actions/checkout did not run before setup-node, so the workspace is empty.

    Check

    grep -n -B8 'setup-node' .github/workflows/*.yml   # no actions/checkout before it

    Fix

    Run actions/checkout first.

Prevent

Always commit lockfiles.

Now learn it: break and fix it yourself in The build takes 20 minutes, then take Level 05 · Fast and reliable builds. Open in the app.

Unable to download artifact(s): Artifact not found

Error

Error: Unable to download artifact(s): Artifact 'build' not found

What it means

download-artifact asked for an artifact with this name and found none it can reach. By default it looks only in the current workflow run.

Causes, most likely first

  1. The name differs from the one used at upload.

    Check

    gh api repos/<owner>/<repo>/actions/runs/<run-id>/artifacts --jq '.artifacts[] | {name, expired}'   # the names that exist

    Fix

    Use the same name in both steps, best from one env variable.

  2. The uploading job did not run, failed, or found no files, so nothing was uploaded.

    Check

    gh run view <run-id>   # the upload job was skipped or failed, or its log warns 'No files were found'

    Fix

    Fix the upload job first.

  3. The download runs before the upload has finished, because the downloading job has no needs: on the uploading one. Jobs run in parallel by default.

    Check

    grep -n -B2 -A2 'needs:' .github/workflows/<file>.yml   # the downloading job does not need the uploading one

    Fix

    Add needs: <upload-job> to the downloading job.

  4. The artifact is from another run, or it expired (90 days by default, or the upload's retention-days).

    Check

    gh api repos/<owner>/<repo>/actions/runs/<other-run-id>/artifacts --jq '.artifacts[] | {name, expired}'   # expired true, or a different run

    Fix

    For another run, pass run-id and github-token to download-artifact. Raise retention-days if it must live longer.

Prevent

Keep artifact names in one env variable.

Now learn it: break and fix it yourself in Unable to download artifact(s): Artifact not found, then take Level 05 · Fast and reliable builds. Open in the app.

(409) Conflict: an artifact with this name already exists

Error

Failed to CreateArtifact: Received non-retryable error: Failed request: (409) Conflict: an artifact with this name already exists on the workflow run

What it means

upload-artifact tried to create an artifact whose name is already used in this run. Since v4, artifact names are unique per run and an uploaded artifact cannot be changed, so the second upload is refused with 409 Conflict.

Causes, most likely first

  1. Matrix jobs upload with the same name. The first one wins and the rest fail.

    Check

    grep -n -A3 upload-artifact .github/workflows/*.yml   # name: without matrix values in a matrix job

    Fix

    Put the matrix values in the name; merge later with download-artifact and pattern: plus merge-multiple: true.

  2. The same job uploads the name again on purpose: a second upload step, or a re-run of the job.

    Check

    grep -c 'name: build' .github/workflows/<file>.yml   # more than one upload with that name

    Fix

    Set overwrite: true on the upload that should replace it.

Prevent

Name artifacts after their matrix job.

Now learn it: break and fix it yourself in (409) Conflict: an artifact with this name already exists, then take Level 05 · Fast and reliable builds. Open in the app.

No files were found with the provided path

Error

Warning: No files were found with the provided path: dist/. No artifacts will be uploaded.

What it means

upload-artifact found nothing at the given path, so it uploaded nothing. With the default if-no-files-found: warn the step still passes, and later jobs find nothing.

Causes, most likely first

  1. The build wrote somewhere else, or did not run.

    Check

    ls -la "$GITHUB_WORKSPACE/dist"   # in a step before the upload: missing or empty

    Fix

    Fix the path, or the build step.

  2. path: is relative to the workspace. working-directory and defaults.run.working-directory apply to run: steps only, not to uses: steps, so a build in app/ writes to app/dist.

    Check

    grep -n 'working-directory' .github/workflows/<file>.yml   # set for run steps: add it to path:

    Fix

    Write the full path from the workspace: path: app/dist.

  3. Since upload-artifact v4.4, files and folders whose names start with a dot are left out unless include-hidden-files: true. If everything is hidden, nothing is found.

    Check

    ls -A dist   # only names starting with a dot

    Fix

    Set include-hidden-files: true, after checking that no secrets (such as .env) are inside.

Prevent

Fail loudly on missing build output.

Now learn it: break and fix it yourself in Unable to download artifact(s): Artifact not found, then take Level 05 · Fast and reliable builds. Open in the app.

Automatically failed: deprecated version

Error

This request has been automatically failed because it uses a deprecated version of `actions/upload-artifact: v3`. Learn more: https://github.blog/changelog/2024-04-16-deprecation-notice-v3-of-the-artifact-actions/

What it means

GitHub failed the job in Set up job because it uses an action version that has been shut down. The workflow did not change; the old version stopped working on GitHub's side.

Causes, most likely first

  1. upload-artifact or download-artifact v3 or older. Those versions used a storage service that no longer exists.

    Check

    grep -rn 'artifact@' .github/workflows   # @v3, @v2 or @v1, or the version comment beside a SHA

    Fix

    Move to a supported major (upload-artifact v6, download-artifact v7 or later) and check the changes in its notes: since v4, names are unique per run.

  2. actions/cache older than v3.4.0 or v4.2.0, including SHAs pinned to those versions. The old cache service they called was shut down.

    Check

    grep -rn 'actions/cache@' .github/workflows   # check each version, or the version comment beside a SHA

    Fix

    Move to a current actions/cache major.

Prevent

Dependabot for github-actions, as in this repository's dependabot.yml.

Now learn it: break and fix it yourself in This request has been automatically failed, then take Level 09 · Write it once. Open in the app.

Node.js 20 actions are deprecated

Error

Node.js 20 actions are deprecated. The following actions are running on Node.js 20 and may not work as expected: actions/checkout@v4.

What it means

A warning, not a failure: these actions declare using: node20, and runners are moving actions to Node.js 24. The message lists each action and version that still uses Node.js 20, on every run.

Causes, most likely first

  1. You use an older major of an official action that runs on Node.js 20.

    Check

    gh run view <run-id>   # ANNOTATIONS: the actions and versions listed

    Fix

    Use majors that run on Node.js 24: checkout v5+, setup-node v5+, setup-python v6+, cache v5+, upload-artifact v6+, download-artifact v7+.

  2. A third-party or your own action declares using: node20 in its action.yml.

    Check

    grep -rn "using: *.node20" .github/actions action.yml 2>/dev/null   # your own actions

    Fix

    Change it to node24 and test it, or update the third-party action to a release that does.

Prevent

Dependabot updates for github-actions.

Now learn it: break and fix it yourself in Node.js 20 actions are deprecated, then take Level 09 · Write it once. Open in the app.

The set-output command is deprecated

Error

The `set-output` command is deprecated and will be disabled soon. Please upgrade to using Environment Files.

What it means

A step printed ::set-output or ::save-state, old workflow commands that GitHub replaced with environment files. The warning appears on runs of old workflows or actions.

Causes, most likely first

  1. Your own workflow or script still prints the old command.

    Check

    grep -rn 'set-output\|save-state' .github   # your files

    Fix

    Write name=value to $GITHUB_OUTPUT (state: $GITHUB_STATE).

  2. An action you use prints it, from an old version.

    Check

    gh run view <run-id>   # ANNOTATIONS: the step that printed it

    Fix

    Update that action.

Prevent

Keep actions current.

Now learn it: break and fix it yourself in The set-output command is deprecated, then take Level 03 · Deciding at run time. Open in the app.

Credentials could not be loaded (OIDC)

Error

It looks like you might be trying to authenticate with OIDC. Did you mean to set the `id-token` permission?
Error: Credentials could not be loaded, please check your action inputs: Could not load credentials from any providers

What it means

configure-aws-credentials tried to get an OIDC token from GitHub to exchange for AWS credentials, and the job was not allowed to request one, so it fell back to other credential sources and found none. It fails before contacting AWS.

Causes, most likely first

  1. The job's permissions lack id-token: write: a permissions: block that leaves it out. Without it, the runner does not offer an OIDC token to the job.

    Check

    [ -n "$ACTIONS_ID_TOKEN_REQUEST_URL" ] && echo 'OIDC available' || echo 'no id-token: write in this job'   # in the job, before the action
    gh run view <run-id> --log | grep -A15 'GITHUB_TOKEN Permissions'   # Set up job: the scopes the token really had; IdToken: write?

    Fix

    Add id-token: write to the deploy job's permissions only.

  2. The job is in a reusable workflow, and the calling job did not grant id-token: write. A called workflow cannot have more permissions than its caller.

    Check

    grep -n -B2 -A6 'uses: .*\.github/workflows/' .github/workflows/*.yml   # the calling job's permissions

    Fix

    Grant id-token: write in the calling job too.

  3. The run is a pull request from a fork, which never gets an OIDC token.

    Check

    gh api repos/<owner>/<repo>/actions/runs/<run-id> --jq '{event, actor: .actor.login, head: .head_repository.full_name}'   # another head repository: a fork; dependabot[bot]: Dependabot

    Fix

    Skip the deploy for fork pull requests.

  4. role-to-assume is empty, for example from a variable or secret this run cannot read. The action then tries other providers and fails with the second line alone.

    Check

    grep -n -A6 'configure-aws-credentials' .github/workflows/*.yml   # where role-to-assume comes from

    Fix

    Set role-to-assume from a value the run can read (a vars value, or the ARN written in the file).

Prevent

Grant id-token: write only to deploy jobs.

Now learn it: break and fix it yourself in Not authorized to perform sts:AssumeRoleWithWebIdentity, then take Level 10 · No stored cloud keys. Open in the app.

Not authorized to perform sts:AssumeRoleWithWebIdentity

Error

Error: Could not assume role with OIDC: Not authorized to perform sts:AssumeRoleWithWebIdentity

What it means

GitHub issued the OIDC token, but AWS refused to exchange it for the role, because the token's claims do not match the role's trust policy. sub (who is asking: repository, branch or environment) and aud (who the token is for) are the claims checked.

Causes, most likely first

  1. The sub claim differs from the policy. A job with environment: sends repo:<owner>/<repo>:environment:<name> instead of repo:<owner>/<repo>:ref:refs/heads/<branch>; pull requests send :pull_request, tags :ref:refs/tags/<tag>; and an organization or repository can customise the format.

    Check

    aws iam get-role --role-name <role> --query Role.AssumeRolePolicyDocument   # the sub and aud it expects
    curl -sS -H "Authorization: bearer $ACTIONS_ID_TOKEN_REQUEST_TOKEN" "$ACTIONS_ID_TOKEN_REQUEST_URL&audience=sts.amazonaws.com" | jq -r '.value | split(".")[1] | gsub("-";"+") | gsub("_";"/") | @base64d | fromjson | {sub, aud}'   # in the job: the claims it sends, not the token
    gh api repos/<owner>/<repo>/actions/oidc/customization/sub   # use_default false: a custom sub format

    Fix

    Make the job match the trust policy (often environment: production), or narrow the policy to the job's exact sub. Do not widen it with wildcards.

  2. A pattern with * sits under StringEquals, which compares literally, so it never matches.

    Check

    aws iam get-role --role-name <role> --query Role.AssumeRolePolicyDocument   # StringEquals with a * in the sub

    Fix

    Write the exact sub under StringEquals, or use StringLike for a deliberate, narrow pattern.

  3. aud is not sts.amazonaws.com, or the account has no OIDC provider for token.actions.githubusercontent.com, or the role is in another account than the provider.

    Check

    aws iam list-open-id-connect-providers   # token.actions.githubusercontent.com in the role's account?

    Fix

    Create the provider in the role's account, and keep the audience sts.amazonaws.com (the action's default).

Prevent

One role per environment, trusting one repository and environment.

Now learn it: break and fix it yourself in Not authorized to perform sts:AssumeRoleWithWebIdentity, OIDC trust policy allows any repository, then take Level 10 · No stored cloud keys. Open in the app.

Branch is not allowed to deploy

Error

Branch "feature/login" is not allowed to deploy to production due to environment protection rules.

What it means

The job names an environment whose deployment policy allows only some branches or tags, and this run's branch is not one of them, so GitHub fails the job before it starts.

Causes, most likely first

  1. The branch is not in the environment's list of allowed branches.

    Check

    gh api repos/<owner>/<repo>/environments/production --jq .deployment_branch_policy
    gh api repos/<owner>/<repo>/environments/production/deployment-branch-policies --jq '.branch_policies[] | {name, type}'   # the branch and tag patterns allowed

    Fix

    Deploy from an allowed branch, or add the branch to the policy if it really should deploy.

  2. The run is for a tag and the policy lists only branches. The message then says Tag "v1.2.0" is not allowed.

    Check

    gh api repos/<owner>/<repo>/environments/production/deployment-branch-policies --jq '.branch_policies[] | select(.type == "tag")'   # nothing printed: no tag allowed

    Fix

    Add a tag pattern (such as v*) to the policy.

Prevent

Pair environment rules with job if: conditions so runs skip instead of failing.

Now learn it: break and fix it yourself in Production deploys without approval, then take Level 08 · Shipping safely. Open in the app.

Canceling since a higher priority waiting request exists

Error

Canceling since a higher priority waiting request for 'deploy' exists

What it means

Two runs share a concurrency: group, and GitHub keeps at most one running and one waiting per group. This run was canceled to make room for a newer one.

Causes, most likely first

  1. The group has cancel-in-progress: true, so a new run cancels the running one.

    Check

    grep -n -A2 concurrency .github/workflows/*.yml   # cancel-in-progress: true

    Fix

    That is the setting working. For deploys, use cancel-in-progress: false so a running deploy finishes.

  2. This run was waiting, and a newer run replaced it. Only one run waits per group, even with cancel-in-progress: false.

    Check

    gh run list --workflow <file>.yml --limit 5   # a newer run in the same group started after this one

    Fix

    Nothing to fix: the newest change is deployed; for deploys that is usually what you want.

  3. The group name is too broad (no branch in it), so runs on unrelated branches cancel each other.

    Check

    grep -n 'group:' .github/workflows/*.yml   # no github.ref in the group

    Fix

    Include the branch: group: ${{ github.workflow }}-${{ github.ref }}.

Prevent

Include the branch in the group so unrelated runs do not cancel each other.

Now learn it: break and fix it yourself in Two deploys at once: the old version wins, Every push to a pull request runs to the end, then take Level 08 · Shipping safely. Open in the app.

Waiting for review

Error

Waiting for review: production needs approval to start deploying changes.

What it means

The job deploys to an environment with required reviewers, and it waits until one of them approves in the run's Review deployments. It does not start on its own.

Causes, most likely first

  1. No required reviewer has approved yet.

    Check

    gh api repos/<owner>/<repo>/actions/runs/<run-id>/pending_deployments   # the environment and who can approve

    Fix

    Approve from the run page (or the API) as a required reviewer; pending deployments expire after 30 days.

  2. The person who could approve is the one who started the run, and Prevent self-review is on.

    Check

    gh api repos/<owner>/<repo>/environments/production --jq '.protection_rules[] | select(.type == "required_reviewers")'   # prevent_self_review true

    Fix

    Ask another reviewer to approve.

Prevent

Notify reviewers; keep the group small but never one person.

Now learn it: break and fix it yourself in Production deploys without approval, then take Level 08 · Shipping safely. Open in the app.

Can't find 'action.yml'

Error

Can't find 'action.yml', 'action.yaml' or 'Dockerfile' under '/home/runner/work/shop/shop/.github/actions/setup'. Did you forget to run actions/checkout before running your local action?

What it means

A step uses a local action (uses: ./.github/actions/setup), and the runner found no action.yml, action.yaml or Dockerfile at that path in the workspace.

Causes, most likely first

  1. The repository is not checked out yet, so the workspace is empty.

    Check

    grep -n -B6 'uses: ./' .github/workflows/<file>.yml   # no actions/checkout before it

    Fix

    Run actions/checkout first.

  2. The path is wrong. It is relative to the workspace root and must start with ./.

    Check

    ls .github/actions/setup   # in a step: action.yml exists?

    Fix

    Use the path from the repository root starting with ./

  3. checkout used path:, so the repository is in a subfolder of the workspace.

    Check

    grep -n -A3 'actions/checkout' .github/workflows/<file>.yml   # path: set

    Fix

    Include that folder in uses: (./src/.github/actions/setup), or check out to the root.

Prevent

Keep checkout as the first step.

Now learn it: break and fix it yourself in Can't find 'action.yml', then take Level 09 · Write it once. Open in the app.

Required property is missing: shell

Error

Error: /home/runner/work/shop/shop/./.github/actions/setup/action.yml (Line: 11, Col: 7): Required property is missing: shell

What it means

A run: step inside a composite action has no shell:. Composite actions have no default shell, so every workflow that uses the action fails at once. The path and line point into the action's action.yml.

Cause

  1. A run step in the composite action has no shell: key.

    Check

    grep -n -B1 -A2 'run:' .github/actions/*/action.yml   # a run step without shell:

    Fix

    Add shell: bash to each run step in action.yml.

Prevent

Run actionlint on action.yml files too.

Now learn it: break and fix it yourself in Required property is missing: shell, then take Level 09 · Write it once. Open in the app.

Invalid secret, X is not defined in the referenced workflow

Error

Invalid secret, DEPLOY_TOKEN is not defined in the referenced workflow.

What it means

The calling workflow passes a named secret to a reusable workflow, and the called workflow does not declare a secret with that name, so GitHub rejects the call at once.

Causes, most likely first

  1. The called workflow does not list the secret under on.workflow_call.secrets.

    Check

    grep -n -A8 workflow_call .github/workflows/deploy.yml   # DEPLOY_TOKEN missing under secrets:

    Fix

    Declare the secret in the called workflow, or use secrets: inherit.

  2. The names differ in spelling between caller and called workflow.

    Check

    grep -n -A4 'secrets:' .github/workflows/*.yml   # compare the names on both sides

    Fix

    Use the same name on both sides.

Prevent

Document a reusable workflow's inputs and secrets at the top of its file.

Now learn it: break and fix it yourself in Reusable workflow gets empty secrets, then take Level 09 · Write it once. Open in the app.

error parsing called workflow: workflow was not found

Error

error parsing called workflow ".github/workflows/ci.yml" -> "nimbus/shared/.github/workflows/build.yml@main" : workflow was not found.

What it means

A job's uses: points at a reusable workflow that GitHub could not load, so the whole workflow fails before any job. The arrow shows the calling file and the workflow it tried to load.

Causes, most likely first

  1. The uses: path is wrong. It must be owner/repo/.github/workflows/file.yml@ref, or ./.github/workflows/file.yml in the same repository.

    Check

    gh api 'repos/<owner>/<repo>/contents/.github/workflows/build.yml?ref=main' --jq .path   # 404: no such file at that path

    Fix

    Correct the path.

  2. The ref after @ does not exist.

    Check

    git ls-remote https://github.com/<owner>/<repo> '<ref>'   # nothing printed: no such branch or tag

    Fix

    Use an existing tag or commit SHA.

  3. The called workflow is in a private or internal repository that does not allow access from this one.

    Check

    gh api repos/<owner>/<repo> --jq .visibility   # private or internal: check its Settings > Actions > General > Access

    Fix

    In the called repository, allow access under Settings > Actions > General > Access.

  4. The file has no on: workflow_call, so it is not reusable (the message then says is not reusable as it is missing).

    Check

    grep -n -A3 workflow_call .github/workflows/build.yml   # nothing printed

    Fix

    Add on: workflow_call: to the called file.

Prevent

Pin reusable workflows to a tag or commit SHA, as you would actions.

Now learn it: break and fix it yourself in Reusable workflow gets empty secrets, then take Level 09 · Write it once. Open in the app.