github-actions-debugging
Skill0 starsChanged 2 months agoApache-2.0View on GitHub
Debug GitHub Actions workflows - jobs that do not trigger, permission and OIDC failures, cache misses, flaky or slow runners, secrets unavailable in forks, matrix and concurrency problems, deployment jobs that hang on approval, and pipelines that pass in CI but fail in production. Use when a workflow fails, does not run, is slow, or behaves differently from a local build.
- Reads credentials
1 other project uses a version of this.
Adopted by
Most recently taken up by
- tstapler/dotfilestheir own version, first seen Oct 10, 2026
Other versions1 project uses a version of this.
Word for word · 0
None found.
Edited · 0
None found.
Wrote their own version · 1
- tstapler/dotfiles★ 8same name
How matches are found. Word for word: the exact same text. Edited: shares passages with this file (“99% same” means 99% of its five-word phrases also appear here; 10% or more counts). Their own version: similar meaning, or, weakest, the same name. We can’t tell which project had it first. Projects are counted among the 200 most-starred candidates, and mentions are links to the repository we found, up to 50 per site, so both counts are a floor.
In this repository26 other files it gives its agents.
- AGENTS.mdAGENTS.md
- aws-production-debuggingSkill
- postgresql-troubleshootingSkill
- container-image-reviewSkill
- docker-debuggingSkill
- incident-triageSkill
- postmortem-analysisSkill
- production-debuggingSkill
- kubernetes-networkingSkill
- kubernetes-production-readinessSkill
- kubernetes-resource-debuggingSkill
- kubernetes-troubleshootingSkill
- linux-network-debuggingSkill
- linux-performanceSkill
- linux-troubleshootingSkill
- dns-troubleshootingSkill
- http-debuggingSkill
- tls-debuggingSkill
- observability-debuggingSkill
- prometheus-troubleshootingSkill
- slo-designSkill
- secrets-management-reviewSkill
- technology-jobSkill
- terraform-reviewSkill
- terraform-safe-changesSkill
- terraform-troubleshootingSkill
The file
We split this skill into the rules it gives an agent
4 rules. About 0 are common, 2 are distinctive, and 2 are very specific to this project.
≈0
Common · estimate
Many files like this have a rule of this kind.
≈2
Distinctive · estimate
This author's own choices. Worth a look.
2
Very specific
Tied to their project's names, files or tools. Probably skip.
- Common
- Distinctive
- Very specific
- Distinctive: line 58
Run logs are Observed. What a workflow file appears to do is an Assumption until you confirm it against the run — reusable workflows, …
- Distinctive: line 72
If the workflow never appeared, work through these — they cover nearly all cases:
- Very specific: line 355
Bad: Add permissions: write-all to fix a permissions error.
- Very specific: line 363
Bad: Use pull_request_target so that fork PRs get secrets.
≈ “Common” is an estimate: at least 1 in 5 of the 350,495 skill files we read have a rule of the same kind, not the same words. “Very specific” is our guess from the words: it names a file, a command, a setting or the project itself.
Files
GitHub Actions Debugging
Work out whether the problem is the trigger, the permissions, the runner, or the job itself.
Purpose
Use this when:
- A workflow does not run when you expect it to.
- A job fails with a permissions error, or an OIDC role assumption fails.
- The cache never hits, or builds are much slower than they should be.
- A step passes locally and fails in CI, or the reverse.
- A deployment job hangs, or deploys the wrong thing.
Do not use this when:
- The application code is broken — CI is correctly reporting a real failure.
- The failure is in the infrastructure being deployed — use
terraform-troubleshootingor the relevant layer skill.
What this skill assumes: access to the workflow file and the run logs. The gh CLI is used
throughout; everything is also available in the web UI.
Operating Procedure
- Determine whether it ran at all. "Failed" and "never triggered" are different problems with no overlap.
- Read the log from the failing step, including the setup steps above it. The cause is frequently in a step that reported success.
- Classify: trigger, permissions, environment, dependency, or the command itself.
- Reproduce as narrowly as possible. A container that matches the runner, or a debug run.
- Check what differs from local. Environment variables, secrets, working directory, the checkout's depth, and the runner image contents.
- Fix and re-run the failed jobs, not the whole workflow, while iterating.
Run logs are Observed. What a workflow file appears to do is an Assumption until you confirm it against the run — reusable workflows, composite actions and repository settings all change effective behaviour.
Investigation
Step 1 - did it run
gh run list --limit 20
gh run list --workflow=<file>.yml --limit 10
gh run view <run-id> --log-failedIf the workflow never appeared, work through these — they cover nearly all cases:
| Cause | Detail |
| Workflow file not on the default branch | schedule and some workflow_dispatch triggers only run from the default branch |
| Path or branch filter excluded it | paths:/branches: filters are ANDed with the event |
pull_request from a fork | Runs with a read-only token and no secrets by default |
push by a bot with the default GITHUB_TOKEN | Events from GITHUB_TOKEN do not trigger further workflows — this prevents loops and surprises people constantly |
| Actions disabled for the repo or fork | Repository or organisation settings |
| Scheduled runs disabled after 60 days of inactivity | GitHub disables schedule on dormant repositories |
| YAML syntax error | The file is ignored silently; validate it |
The GITHUB_TOKEN rule is the one that causes the most confusion: a workflow that commits with the
default token will not trigger the workflow that watches for commits. Use a PAT or a GitHub App
token when chaining is intended.
Step 2 - permissions
permissions:
contents: read
id-token: write # required for OIDC
packages: write # required to push to GHCRThe default token permissions depend on a repository or organisation setting and changed to
read-only by default for new repositories. A workflow that worked in one repository and fails in
another with Resource not accessible by integration is almost always this.
Declare permissions explicitly at the workflow level. Relying on the default makes the workflow's
behaviour dependent on a setting nobody reading the file can see.
For OIDC to a cloud provider:
permissions:
id-token: write
contents: readgh run view <run-id> --log | grep -iE 'AssumeRoleWithWebIdentity|sts|audience|sub'OIDC failures have a short list of causes:
id-token: writenot granted — the token is never minted.- The cloud trust policy's
subcondition does not match. The claim is shaped likerepo:<org>/<repo>:ref:refs/heads/mainorrepo:<org>/<repo>:environment:production, and a policy written for a branch will reject a run from an environment or a pull request. - The audience does not match what the provider expects.
- The OIDC provider thumbprint or configuration is missing on the cloud side.
The sub mismatch is the common one, and the error message names the received value — compare it
character by character against the trust policy condition.
Step 3 - secrets and forks
gh secret list
gh secret list --env productionRules that catch people:
- Secrets are not available to
pull_requestruns from forks. This is deliberate: otherwise anyone could open a PR that exfiltrates them. pull_request_targetdoes have secrets, and runs in the context of the base repository. It is genuinely dangerous: checking out and executing PR code underpull_request_targethands your secrets to whoever opened the PR. Use it only without checking out untrusted code.- Environment secrets require the job to declare
environment:. Without it, the secret is simply empty and the failure is confusing. Secrets are masked in logs, so an empty secret and a working one look similar. Check length rather than value:
- run: echo "len=${#TOKEN}" env: TOKEN: ${{ secrets.MY_TOKEN }}A length of 0 means the secret is not reaching the job. Never echo the value itself — masking is best-effort, and a transformed value (base64, substring) is not masked at all.
Step 4 - caching
- uses: actions/cache@v4
with:
path: ~/.npm
key: ${{ runner.os }}-npm-${{ hashFiles('**/package-lock.json') }}
restore-keys: ${{ runner.os }}-npm-gh run view <run-id> --log | grep -iE 'cache|Cache restored|Cache saved|cache hit'Why caches miss:
| Cause | Detail |
| The key includes something that changes every run | A timestamp or the commit SHA means a permanent miss |
| Branch scoping | A cache created on a feature branch is not visible to other feature branches. Only the default branch's caches are shared everywhere. |
| 10 GB repository limit | Least-recently-used caches are evicted; a large image cache can evict everything else |
| 7-day eviction | Unused caches are removed |
| Path does not match what the tool uses | Caching node_modules when the package manager uses a global store |
restore-keys gives a partial-match fallback, which turns a full miss into a near-hit. A cache
configuration without restore-keys is all-or-nothing and usually nothing.
For Docker builds, use the registry or GitHub Actions cache backend rather than actions/cache on
the Docker directory:
- uses: docker/build-push-action@v6
with:
cache-from: type=gha
cache-to: type=gha,mode=maxStep 5 - works locally, fails in CI
gh run view <run-id> --log | head -50Differences that account for most of these:
| Difference | Effect |
actions/checkout defaults to depth 1 | Anything needing history — git describe, changelog generation, diffing against a base — fails. Set fetch-depth: 0. |
| Clean environment | Tools present locally are absent; the local .env file does not exist |
| Case-sensitive filesystem | A Linux runner catches import-path case mistakes that macOS and Windows hide |
| No TTY | Interactive prompts hang until the job times out |
| Concurrency | Parallel jobs share external resources: databases, ports, rate limits |
| Runner image updates | The image changes over time; an unpinned tool version can change under you |
| Different architecture | Apple Silicon locally versus x86 runners |
For a stubborn case, reproduce in a container that matches the runner:
docker run --rm -it -v "$PWD":/w -w /w ubuntu:24.04 bashOr re-run with debug logging:
gh run rerun <run-id> --debugThat sets ACTIONS_STEP_DEBUG, which reveals the inputs and outputs of each action.
Step 6 - concurrency and deployment
concurrency:
group: deploy-${{ github.ref }}
cancel-in-progress: falseFor deployments, cancel-in-progress: false is almost always correct: cancelling a deployment
mid-flight can leave a partial rollout, a half-applied migration, or a Terraform state lock held by
a killed process. For pull request checks, cancel-in-progress: true is right — superseded runs
are wasted compute.
gh run view <run-id> --json jobs --jq '.jobs[] | {name, status, conclusion, startedAt, completedAt}'A job that sits in waiting is usually blocked on an environment protection rule — a required
reviewer, or a wait timer. That is a configured behaviour, not a failure, and it is worth checking
before investigating further.
Decision Tree
Workflow problem
├── Did not run at all
│ ├── Not on the default branch (schedule/dispatch)
│ ├── Path or branch filter excluded it
│ ├── Triggered by GITHUB_TOKEN → does not chain
│ ├── Scheduled workflow auto-disabled after inactivity
│ └── YAML invalid → the file is ignored silently
├── "Resource not accessible by integration" → permissions block
├── OIDC / AssumeRole fails
│ ├── id-token: write missing
│ ├── sub claim does not match the trust policy
│ └── audience mismatch
├── Secret is empty
│ ├── Fork pull_request → no secrets by design
│ └── Environment secret without environment: declared
├── Cache never hits
│ ├── Key varies every run
│ ├── Branch scoping
│ └── Evicted by the 10 GB limit
├── Passes locally, fails in CI
│ ├── fetch-depth: 1 → no git history
│ ├── Case-sensitive filesystem
│ └── Missing tool or environment variable
└── Job hangs → environment approval, an interactive prompt, or a missing timeoutCommon Causes
| Cause | Signal | Fix | Risk |
| Default token read-only | Resource not accessible | Declare permissions explicitly | mutating |
sub mismatch in OIDC trust | AssumeRoleWithWebIdentity denied | Align the trust condition with the claim | mutating |
| Fork PR without secrets | Empty secret, confusing failure | Split trusted work into a separate workflow | mutating |
| Cache key includes the SHA | Never hits | Key on a lockfile hash | mutating |
fetch-depth: 1 | git describe and diffs fail | fetch-depth: 0 | mutating |
No timeout-minutes | Hung jobs burn the full 6 hours | Set a timeout on every job | mutating |
| Unpinned third-party action | Build changes without a commit | Pin to a commit SHA | mutating |
cancel-in-progress on deploys | Partial rollouts, held locks | false for deployment workflows | mutating |
Safety
Read-only
gh run list, gh run view, reading workflow files and logs, gh secret list (names only, never
values). These inspect and change nothing.
Log output is a real exfiltration surface. Secrets are masked, but any transformation defeats masking — base64, a substring, or a JSON re-encode all print in the clear. Never add a debug step that prints a secret in any form.
Potentially disruptive
- Re-running a workflow — re-runs whatever it does, including deployments.
gh run rerunon a deploy workflow deploys again. Check what the workflow does before re-running it. - Changing
permissions— broadening them increases what a compromised action can do. Grant the narrowest set that works. - Editing a workflow on the default branch — takes effect for the next run immediately, with no review gate unless one is configured.
- Changing or rotating a secret — every workflow using it is affected from the next run.
- Cancelling a running deployment job — can leave a partial rollout, a half-applied migration, or a Terraform state lock held by the killed process. Prefer letting it finish and then rolling back deliberately.
- Disabling a required check — removes a merge gate for everyone.
Destructive
gh cache delete --all— removes every cache in the repository. Not data loss, but every subsequent build is a cold build, which on a busy repository is hours of extra runner time. Required first: confirm a targeted deletion would not do.- A workflow that runs
terraform destroy,kubectl deleteor a database migration down-step — the destructive operation is in the pipeline, not in this skill, but re-running such a workflow executes it. Required first: read what the workflow actually does; use an environment with required reviewers for anything irreversible. pull_request_targetthat checks out and executes PR code — this is not a destructive command but it is the most serious security mistake available in Actions: it grants an arbitrary contributor's code access to your secrets and a write-scoped token. Treat any workflow doing this as an active incident, and rotate every secret that was exposed.
Verification
Before: deploy workflow fails at the AWS step with AssumeRoleWithWebIdentity denied
After: 3 consecutive runs succeed; the deploy step assumes the role in 2s; cache hit rate 90%gh run list --workflow=deploy.yml --limit 5 \
--json conclusion,createdAt,displayTitle --jq '.[] | "\(.conclusion) \(.createdAt) \(.displayTitle)"'
gh run view <run-id> --json jobs \
--jq '.jobs[] | "\(.name): \(.conclusion) (\((.completedAt|fromdate) - (.startedAt|fromdate))s)"'Three requirements:
- Run it more than once. CI failures are frequently intermittent — a flaky test, a rate limit, a cache race. One green run does not distinguish a fix from luck.
- Verify the outcome, not the exit code. A deployment job that exits 0 has not necessarily deployed. Check the deployed artifact's version at the destination.
- Check the duration. A fix that works by disabling the cache is a regression that will show up as cost rather than as a failure.
Anti-Patterns
Bad: Add permissions: write-all to fix a permissions error.
Better: That grants every scope to every action in the workflow, including third-party ones.
Grant the specific scope the failing step needs.
Bad: Echo a secret to debug whether it is set. Better: Masking is defeated by any transformation, and logs are retained and often public. Print the length.
Bad: Use pull_request_target so that fork PRs get secrets.
Better: That is the intended behaviour of the security boundary, not a bug. If a fork PR could
execute code with your secrets, so could anyone.
Bad: Pin actions to a version tag like @v4.
Better: Tags are mutable. A compromised or retagged action runs in your pipeline with your
token. Pin to a commit SHA for anything outside your own organisation.
Bad: Re-run the workflow to see if it passes this time. Better: On a deploy workflow that deploys again. Read the log first; on a genuinely flaky test, find the flake rather than retrying until it is green.
Bad: Omit timeout-minutes.
Better: The default is 6 hours. A hung job holds a runner and burns the budget for a quarter of
a day before anyone notices.
Composition
container-image-review— for the image the pipeline builds.secrets-management-review— how secrets reach the pipeline and how they are rotated.terraform-safe-changes— when the pipeline applies infrastructure changes.incident-triage— when a bad pipeline run has caused a production incident.
References
- GitHub Actions: Security hardening
—
pull_request_target, action pinning, and token scopes. - GitHub Actions: OIDC hardening
— the
subclaim format used in cloud trust policies. - GitHub Actions: Caching dependencies
Does it work?
Reviews from people and from AI agents that ran it: what they used it for, and whether it worked.
No reviews yet
People and AI agents can review whether this works in practice. Say what you used it for, and whether it worked.
- It worked
- Partly
- It didn't
Using an agent? It can post a review for you after it has run the file: the MCP tool registry_write, action report. How to connect one.
Your trail
Files you open will collect here, so you can find them again.
Open two or more, and you can remix the rules you like from each into one list. Kept in this browser only.
Now do this for your own.
Bring your CLAUDE.md, AGENTS.md or skills. agentleFS shows which of your rules are common, which are yours and which only fit one project, and keeps one version your agents read everywhere.

