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
-
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 indentationFix
Line the key up with its siblings using spaces, the same number per level as the rest of the file.
-
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 tabFix
Replace the tabs with spaces, and set your editor to insert spaces in YAML files.
-
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
-
A required key is missing, such as
runs-onorstepsin a job. The schema needs it, so GitHub reportsRequired 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 locallyFix
Add the key in the job the line points to, for example
runs-on: ubuntu-latest. -
A key is misspelt or indented under the wrong parent (
stepz:, orwith:under the job instead of the step). The schema does not allow it there, so GitHub saysUnexpected value '<key>'.Check
actionlint .github/workflows/ci.yml # 'unexpected key' lists the keys allowed thereFix
Fix the spelling, or move the key under the right parent.
-
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 lineFix
Use a valid permission name (
contents,pull-requests,id-token, ...) and give everyuses: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
-
A branches, tags or paths filter excludes the event:
branches: [master]when the branch is main,pathsthat 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 eventsFix
Match the filters to the real branch names and layout, and add
tags:for tag pushes. -
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 repositoryFix
Turn it back on:
gh workflow enable ci.yml, or Settings > Actions > General for the repository or the fork. -
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 firstFix
Push a commit without the skip marker, fix the merge conflict, or push tags a few at a time.
-
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/workflowsor 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 onlyFix
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
-
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 existFix
Use the current id in
needs:, and rename ids and their needs together. -
needs:uses the job's displayname:instead of its id. Only ids (the keys underjobs:) count, so a name such asRun testsis unknown.Check
grep -n -B1 ' name:' .github/workflows/ci.yml # the key on the line above name: is the id to useFix
Use the id, not the name.
-
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 fileFix
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
-
secretsis used in anif:. Secrets are never available inif: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 causeFix
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 asgithub.event_nameorgithub.repository. -
envorstepsis used in a job-levelif:. A job'sif: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 stepsFix
Use
needs.<job>.outputs.<name>,varsorgithubin the job'sif:, or move the condition to a step. -
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 wordFix
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
-
The owner or repository name is wrong (
actions/chekout). GitHub has no such repository, so the runner saysrepository not found.Check
gh api repos/<owner>/<action-repo> --jq .full_name # 404: no such repository, check the spellingFix
Correct the name.
-
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 > AccessFix
In the action's repository, allow access from other repositories in the organization (Settings > Actions > General > Access).
-
The ref after
@does not exist, or was deleted. The repository is found but not the version, so the message saysunable 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 useFix
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
-
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 amountFix
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.
-
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 accountFix
Update the payment method and retry the payment.
-
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 usageFix
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
-
The file on the branch you run from has no
workflow_dispatch:underon:. 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 triggerFix
Add
workflow_dispatch:toon:on that branch. -
No
--refwas 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 branchFix
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
-
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 outputFix
Fix what the command reports. Reproduce it locally with the same commands under
bash -e script.sh. -
A command uses 1 for a normal result: grep with no match, diff when files differ, a
testor[ ]that is false. Underbash -ethat 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 messageFix
Accept the expected code where you call it:
grep -c x f || true, orif grep -q x f; then ...; fi. Do not usecontinue-on-errorto 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
-
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 stepFix
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.
-
It is installed in a directory that is not on PATH, or that was added to
$GITHUB_PATHin the same step.$GITHUB_PATHchanges 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 itFix
Write the directory to
$GITHUB_PATHin one step and use the tool from the next one, or call it by its full path. -
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 skippedFix
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
-
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 killerFix
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. -
Node.js reached its own heap limit, which can be lower than the machine's memory. It stops itself with
JavaScript heap out of memoryinstead 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'sFix
Raise the heap with
NODE_OPTIONS=--max-old-space-size=4096when the runner has that memory, or reduce parallel work. -
A container with a memory limit (
docker run --memory,options: --memoryon 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'sFix
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
-
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.
-
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 timeFix
Check the machine's memory, disk and network, and make sure nothing (updates, autoscaling, a reboot) stops the runner service during a job.
-
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 passFix
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
-
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 canceledFix
Fix the first failure. Set
fail-fast: falseto see every matrix result. -
A newer run in the same concurrency group with
cancel-in-progress: truereplaced 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 branchFix
Nothing to fix if the newer run is the one you want; read that run instead.
-
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
-
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). -
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). -
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 longerFix
Set
timeout-minutesclose 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
-
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 labelsFix
Fix the label in
runs-on:. -
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 trueFix
Start the runner service, or add runners.
-
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 onesFix
Add the repository to the runner group, or allow public repositories if that is intended.
-
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 onceFix
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
-
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 grewFix
Slim the build (multi-stage images, a .dockerignore), and remove images and build cache you no longer need between steps.
-
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 needFix
Remove unused preinstalled toolchains before the build (for example
sudo rm -rf /usr/local/lib/android /usr/share/dotnet /opt/ghcon a hosted runner), or use a larger runner. -
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 jobsFix
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
-
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 fetchedFix
Set
fetch-depth: 0on actions/checkout.git fetch --tagsalone is not enough in a shallow clone, because the history back to the tag is still missing; usegit fetch --unshallow --tags. -
The repository has only lightweight tags.
git describewithout--tagsuses annotated tags only, so it saysNo annotated tags can describe.Check
git describe --tags # works: the tags are lightweightFix
Call
git describe --tags, or create annotated tags (git tag -a v1.0.0 -m v1.0.0). -
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 remoteFix
Create the first tag, or use
git describe --tags --alwaysto 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
-
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
localhostwith the port mapped; in a job withcontainer:, use the service name. -
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 mappedFix
Add
ports: ["5432:5432"]to the service. -
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 yetFix
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
-
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 anonymousFix
Log in before pulling: docker/login-action with a Docker Hub username and access token stored as secrets.
services:andcontainer:images are pulled before any step, so give themcredentials:too. -
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 imagesFix
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
-
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 numbersFix
Mark the workspace safe before the git commands:
git config --global --add safe.directory "$GITHUB_WORKSPACE". -
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 gitFix
Add the same
safe.directoryline once in the job's first step after checkout. -
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 userFix
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
-
A value with line breaks (JSON, a key, command output) was written as
name=value. Every line must bename=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 valuesFix
Use the delimiter form:
{ echo 'CONFIG<<EOF'; cat config.json; echo 'EOF'; } >> "$GITHUB_ENV", or make JSON one line withjq -c. -
The delimiter form is used, but the closing delimiter is not alone on its own line: the value has no final line break, so
EOFlands 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 breakFix
Print a line break before the delimiter (
echobeforeecho '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
-
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.1Fix
Quote versions: "3.10".
-
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 messageFix
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
-
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 forFix
Add the one scope needed to that job: pull-requests: write to comment on a pull request, contents: write to create a release.
-
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 # readFix
Add a
permissions:block with the one scope needed, rather than changing the default to write. -
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]: DependabotFix
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. -
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/repoFix
Use a GitHub App token for that repository (actions/create-github-app-token) with only the permission needed.
-
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 tokenFix
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
-
The GITHUB_TOKEN has no
packages: writein 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: writeto the publishing job only. -
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 oneFix
Open the package's settings, Manage Actions access, and add this repository with the Write role.
-
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 caseFix
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
-
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 theseFix
Skip the step for forks (
if: github.event.pull_request.head.repo.full_name == github.repository); add what Dependabot needs to Dependabot secrets. -
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 jobFix
Add
environment: productionto the job. -
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 jobFix
Pass it (
secrets: DEPLOY_TOKEN: ${{ secrets.DEPLOY_TOKEN }}) or usesecrets: inherit(same organization or enterprise). -
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
-
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 tokenFix
Set
GH_TOKEN: ${{ github.token }}in the step'senv:. -
GH_TOKENcomes 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]: DependabotFix
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
-
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 defaultFix
Grant
contents: writeto that job only. If the push then fails with GH006 or GH013, the branch is protected: open a pull request instead. -
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]: DependabotFix
Do not push from fork pull requests; push from a trusted workflow after the merge.
-
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/repoFix
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
-
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, orgh workflow run, which the GITHUB_TOKEN may start), or push with a GitHub App token. -
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: missingFix
Add
tags:underon: 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
-
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=100and--paginate, or GraphQL (gh pr list --json), cache results, or use a GitHub App token with a higher limit. -
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-afterheader 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
-
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 requestFix
Let the workflow run for every pull request and skip work inside the job with
if:. -
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 rulesetsFix
Make the required name match the job's check name exactly, or rename the job back.
-
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
-
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 checkFix
Add
merge_group:to theon: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
-
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: bashon the step or indefaults: run: shell:; Git Bash is on Windows runners. Or write the step in PowerShell. -
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 runnerFix
Use
shell: bash(Git Bash has the common Unix tools), or install the tool. -
shell: cmdis 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/*.ymlFix
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
-
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 LFSFix
Set
lfs: trueon actions/checkout. -
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 quotaFix
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
-
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 secretFix
Pass only the non-secret part, and add the secret in the job that uses it.
-
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 secretsFix
Keep non-sensitive settings in
varsinstead 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
-
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 tokenFix
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. -
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 valueFix
Set the secret again with
gh secret set NAME, which prompts for the value, so no newline is added. -
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 updatedFix
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
-
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 differentlyFix
Pass the input under
with:with the exact name from the action's action.yml. -
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]: DependabotFix
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
-
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 repositoryFix
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), withsubmodules: recursivefor submodules; or an SSH deploy key withssh-key:. -
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 publicFix
Set GOPRIVATE, and give git credentials for github.com in that step, for example
gh auth setup-gitwith GH_TOKEN set to a read-only token. -
checkout ran with
persist-credentials: false, so later git commands in the job have no credentials.Check
grep -n 'persist-credentials' .github/workflows/*.yml # falseFix
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
-
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 permissionFix
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.
-
A GitHub App without Workflows: write, or a personal access token without the
workflowscope (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
-
The key changes every run, because it includes
github.shaorrun_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 keyFix
Key on the contents:
npm-${{ runner.os }}-${{ hashFiles('package-lock.json') }}, with restore-keys for a prefix match. -
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 refFix
Let the default branch save the cache (run the workflow on push to main), so every branch can read it.
-
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 leftFix
Keep caches small and few, and save from the default branch regularly.
-
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
-
An earlier run already saved an entry with this exact key.
Check
gh cache list --key npm- # an entry with this exact key already existsFix
Nothing is broken if the content is the same. To store new content, change the key: include
hashFiles(...)of the lockfile. -
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 jobFix
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
-
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 elsewhereFix
Add
cache-dependency-path: requirements-dev.txt(your file), or add a requirements file. -
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 itFix
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
-
No lockfile is committed. The cache has nothing to key on.
Check
git ls-files | grep -E 'package-lock|yarn.lock|pnpm-lock' # nothing printedFix
Commit the lockfile.
-
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.jsonFix
Set
cache-dependency-path: app/package-lock.json. -
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 itFix
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
-
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 existFix
Use the same name in both steps, best from one env variable.
-
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.
-
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 oneFix
Add
needs: <upload-job>to the downloading job. -
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 runFix
For another run, pass
run-idandgithub-tokento download-artifact. Raiseretention-daysif 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
-
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 jobFix
Put the matrix values in the name; merge later with
download-artifactandpattern:plusmerge-multiple: true. -
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 nameFix
Set
overwrite: trueon 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
-
The build wrote somewhere else, or did not run.
Check
ls -la "$GITHUB_WORKSPACE/dist" # in a step before the upload: missing or emptyFix
Fix the path, or the build step.
-
path:is relative to the workspace.working-directoryanddefaults.run.working-directoryapply torun:steps only, not touses:steps, so a build inapp/writes toapp/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. -
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 dotFix
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
-
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 SHAFix
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.
-
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 SHAFix
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
-
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 listedFix
Use majors that run on Node.js 24: checkout v5+, setup-node v5+, setup-python v6+, cache v5+, upload-artifact v6+, download-artifact v7+.
-
A third-party or your own action declares
using: node20in its action.yml.Check
grep -rn "using: *.node20" .github/actions action.yml 2>/dev/null # your own actionsFix
Change it to
node24and 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
-
Your own workflow or script still prints the old command.
Check
grep -rn 'set-output\|save-state' .github # your filesFix
Write
name=valueto$GITHUB_OUTPUT(state:$GITHUB_STATE). -
An action you use prints it, from an old version.
Check
gh run view <run-id> # ANNOTATIONS: the step that printed itFix
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
-
The job's permissions lack
id-token: write: apermissions: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: writeto the deploy job's permissions only. -
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 permissionsFix
Grant
id-token: writein the calling job too. -
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]: DependabotFix
Skip the deploy for fork pull requests.
-
role-to-assumeis 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 fromFix
Set role-to-assume from a value the run can read (a
varsvalue, 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
-
The
subclaim differs from the policy. A job withenvironment:sendsrepo:<owner>/<repo>:environment:<name>instead ofrepo:<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 formatFix
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. -
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 subFix
Write the exact sub under StringEquals, or use StringLike for a deliberate, narrow pattern.
-
audis 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
-
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 allowedFix
Deploy from an allowed branch, or add the branch to the policy if it really should deploy.
-
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 allowedFix
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
-
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: trueFix
That is the setting working. For deploys, use
cancel-in-progress: falseso a running deploy finishes. -
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 oneFix
Nothing to fix: the newest change is deployed; for deploys that is usually what you want.
-
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 groupFix
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
-
No required reviewer has approved yet.
Check
gh api repos/<owner>/<repo>/actions/runs/<run-id>/pending_deployments # the environment and who can approveFix
Approve from the run page (or the API) as a required reviewer; pending deployments expire after 30 days.
-
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 trueFix
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
-
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 itFix
Run actions/checkout first.
-
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 ./
-
checkout used
path:, so the repository is in a subfolder of the workspace.Check
grep -n -A3 'actions/checkout' .github/workflows/<file>.yml # path: setFix
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
-
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: bashto 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
-
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. -
The names differ in spelling between caller and called workflow.
Check
grep -n -A4 'secrets:' .github/workflows/*.yml # compare the names on both sidesFix
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
-
The
uses:path is wrong. It must beowner/repo/.github/workflows/file.yml@ref, or./.github/workflows/file.ymlin the same repository.Check
gh api 'repos/<owner>/<repo>/contents/.github/workflows/build.yml?ref=main' --jq .path # 404: no such file at that pathFix
Correct the path.
-
The ref after
@does not exist.Check
git ls-remote https://github.com/<owner>/<repo> '<ref>' # nothing printed: no such branch or tagFix
Use an existing tag or commit SHA.
-
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 > AccessFix
In the called repository, allow access under Settings > Actions > General > Access.
-
The file has no
on: workflow_call, so it is not reusable (the message then saysis not reusable as it is missing).Check
grep -n -A3 workflow_call .github/workflows/build.yml # nothing printedFix
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.