Skip to content
Français
CI/CD & Automatisation medium

Debugging GitHub Actions workflows

40 min de lecture

Read this page in French

Your CI workflow fails. You look at the logs, and you understand nothing. Or worse: the workflow does not even trigger, with no error message at all. How do you find out what is wrong?

This guide gives you the detective techniques to understand what happens inside your GitHub Actions workflows and to solve problems efficiently.

What you will learn

  • Diagnose a workflow that does not trigger
  • Read the logs and enable the detailed debug mode
  • Inspect the contexts and check that the secrets are present
  • Spot the bottlenecks of a slow workflow
  • Test locally with act to avoid the back and forth

Why is debugging hard?

Unlike local code that you can step through with a debugger, GitHub Actions workflows run on remote machines you have no direct access to.

The specific challenges:

ChallengeConsequence
Remote executionNo real-time console.log
Ephemeral environmentThe VM disappears after the run
Limited logsBy default, only the standard messages are visible
No breakpointsImpossible to pause the workflow

The three kinds of problems

Before debugging, identify the kind of problem:

The workflow does not trigger

The workflow exists but nothing happens when you push code.

The workflow fails

The workflow starts but one or several jobs fail.

The workflow is too slow

The workflow works but takes far too long.

Each kind of problem calls for a different approach.


Problem 1: the workflow does not trigger

This one is often the most frustrating: you push code, but nothing shows up in the Actions tab.

Step 1: check that the file is in the right place

The workflow must sit in .github/workflows/ and carry the .yml or .yaml extension.

Fenêtre de terminal
# Check the structure
ls -la .github/workflows/
# You should see your workflow files
# ci.yml deploy.yml etc.

Common mistake: the file is in .github/workflow/ (no "s") or in another folder.

Step 2: check the YAML syntax

A syntax error prevents GitHub from loading the workflow. Use actionlint to validate it:

Fenêtre de terminal
# Install actionlint
brew install actionlint
# Validate every workflow
actionlint
# Validate one specific file
actionlint .github/workflows/ci.yml

Common errors:

# ❌ Wrong indentation (2 spaces expected)
jobs:
build: # 1 space instead of 2
runs-on: ubuntu-24.04
# ❌ Missing quotes around special characters
on:
push:
branches:
- feature/* # The * must be quoted: "feature/*"
# ❌ Missing colon
jobs:
build
runs-on: ubuntu-24.04 # Error: "build" without ":"

Step 3: check the trigger filters

Your workflow may be configured to react only to certain events or certain branches.

# This workflow only triggers on main
on:
push:
branches:
- main # If you are on develop, nothing happens!
# This workflow only triggers if .py files change
on:
push:
paths:
- '**/*.py' # A change in package.json is ignored

How do you check?

  1. Look at the on: section of your workflow
  2. Compare it with the branch and the event you are testing
  3. Check the paths: and paths-ignore: filters

Step 4: check the Actions tab

Sometimes the workflow is disabled or waiting for approval:

  1. Go to the Actions tab of your repository
  2. Look at the list of workflows on the left
  3. If the workflow does not appear, it is a syntax error
  4. If the workflow appears greyed out, it is disabled (click to enable it)
  5. If you see "Workflows require approval", you have to approve it

Step 5: check the special events

Some events behave in a particular way:

EventCommon trap
pull_requestDoes not trigger on fork PRs (because of the secrets)
pull_request_targetTriggers on the target repository, not on the fork
scheduleCan be delayed when GitHub is under load
workflow_dispatchHas to be triggered manually from the interface

Problem 2: the workflow fails

The workflow starts but a step fails. Here is how to find the cause.

Understanding the structure of the logs

When you click on a failed run, you see a hierarchy:

Run (the whole workflow)
└── Job (for instance build, test, deploy)
└── Step 1: Checkout code (passed)
└── Step 2: Setup Node.js (passed)
└── Step 3: Install dependencies (passed)
└── Step 4: Run tests (FAILED) <- The problem is here
└── Step 5: Upload coverage (skipped)

Click on the failed step to see the details.

Enabling the debug logs

By default, GitHub only displays the standard messages. To see more details, enable the debug mode.

Method 1: re-run with debug

  1. Go to the failed run
  2. Click Re-run all jobs (menu at the top right)
  3. Tick Enable debug logging
  4. Run it again

Method 2: environment variables

Add these variables to your workflow:

name: CI
on: push
permissions: {}
env:
# Enables the detailed logs of the runner (the machine doing the work)
ACTIONS_RUNNER_DEBUG: true
# Enables the detailed logs of every step
ACTIONS_STEP_DEBUG: true
jobs:
build:
runs-on: ubuntu-24.04
permissions:
contents: read
steps:
# ...

Performance

Debug logs are very verbose and slow the run down. Turn them off once the problem is solved.

Using the workflow commands

GitHub Actions recognises special commands in the logs. Use them to create visible messages:

- name: Diagnostics
run: |
# Debug message (only visible in debug mode)
echo "::debug::PATH variable = $PATH"
# Notice (information visible in the logs)
echo "::notice::Compilation succeeded in 45 seconds"
# Warning (yellow triangle in the interface)
echo "::warning::The lodash dependency is outdated"
# Error (red cross, makes the step fail)
echo "::error::Configuration file missing"

What you get in the interface:

  • ::debug:: is only visible with ACTIONS_STEP_DEBUG: true
  • ::notice:: produces a blue annotation in the run summary
  • ::warning:: produces a yellow annotation, and does not fail the workflow
  • ::error:: produces a red annotation, and fails the step

Inspecting the contexts

The contexts (github, env, secrets and so on) carry the information available during the run. Print them to understand the state:

- name: Print the GitHub context
env:
# Context values go through env: (never interpolated inside run:)
REPO: ${{ github.repository }}
BRANCH: ${{ github.ref }}
COMMIT: ${{ github.sha }}
EVENT: ${{ github.event_name }}
ACTOR: ${{ github.actor }}
run: |
echo "Repository: $REPO"
echo "Branch: $BRANCH"
echo "Commit: $COMMIT"
echo "Event: $EVENT"
echo "Actor: $ACTOR"

To see everything at once (useful while debugging):

- name: Dump every context
env:
# A variable is used to avoid escaping problems
ALL_CONTEXTS: |
github: ${{ toJSON(github) }}
env: ${{ toJSON(env) }}
job: ${{ toJSON(job) }}
steps: ${{ toJSON(steps) }}
run: echo "$ALL_CONTEXTS"

Checking whether a secret exists

Secrets are masked in the logs (replaced by ***). To check that a secret is properly configured without exposing it:

- name: Check the secrets
env:
# The secret goes through env:, never interpolated directly inside run:
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
run: |
if [ -n "$NPM_TOKEN" ]; then
echo "NPM_TOKEN is configured"
else
echo "NPM_TOKEN is empty or not configured"
exit 1
fi

Keeping evidence when something fails

When a test fails, the log files disappear with the VM. Set up an automatic upload:

- name: Run the tests
run: npm test
- name: Save the logs on failure
if: failure() # Only runs if the previous step failed
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: debug-logs-${{ github.run_id }}
path: |
logs/
coverage/
npm-debug.log
test-results/
retention-days: 5 # Keeps the logs for 5 days

The artifacts can be downloaded from the page of the run.

Carrying on despite an error

Sometimes you want to see what happens after an error:

- name: A step that may fail
id: risky-step
run: ./unstable-script.sh
continue-on-error: true # The workflow carries on even if this step fails
- name: Analyse the result
run: |
echo "Result of the previous step: ${{ steps.risky-step.outcome }}"
# outcome is either 'success' or 'failure'
if [ "${{ steps.risky-step.outcome }}" == "failure" ]; then
echo "The script failed, but we carry on to collect information"
fi

Problem 3: the workflow is too slow

Your workflow works, but it takes 15 minutes instead of 3. Here is how to find the bottlenecks.

Analysing the duration of the steps

GitHub displays the duration of every step in the interface. Click on a job to see the detail:

Checkout (2s)
Setup Node.js (5s)
Install dependencies (4m 30s) <- Suspicious!
Run tests (1m 15s)
Build (45s)

In that example, installing the dependencies takes 4 minutes 30. It is most likely a cache problem.

Checking that the cache is used

Look for these messages in the logs:

# Cache used
Cache restored from key: npm-linux-abc123def456
# Cache not found
Cache not found for input keys: npm-linux-xyz789

If the cache is not found, check:

  1. Does the cache key match your configuration?
  2. Has the cache expired (7 days without being used)?
  3. Was the cache invalidated by a change of lockfile?

See the Artifacts vs Cache guide to optimise it.

Measuring precisely with timestamps

For long operations, add measurements:

- name: Start the timer
run: echo "START_TIME=$(date +%s)" >> "$GITHUB_ENV"
- name: Long operation
run: npm ci
- name: Stop the timer
run: |
END_TIME=$(date +%s)
DURATION=$((END_TIME - START_TIME))
echo "::notice::Dependency install: ${DURATION} seconds"

Identifying network problems

Slow downloads are a frequent cause of slowness:

- name: Network diagnostics
run: |
echo "=== DNS test ==="
time nslookup registry.npmjs.org
echo "=== Download test ==="
time curl -s -o /dev/null -w "Time: %{time_total}s\n" https://registry.npmjs.org

Bounding the run time

To avoid workflows running forever:

jobs:
build:
runs-on: ubuntu-24.04
timeout-minutes: 30 # The whole job cannot exceed 30 min
steps:
- name: Risky operation
timeout-minutes: 5 # This specific step cannot exceed 5 min
run: ./potentially-long-script.sh

The common error messages

Four messages keep coming back in workflows. Recognising them saves precious time: behind every cryptic label sits a precise cause and a known fix.

"Resource not accessible by integration"

Error: Resource not accessible by integration

What it means: the workflow does not have the permission to do what it is trying to do (comment on a PR, create a label and so on).

Fix: add the required permissions:

permissions:
contents: read # Read the code
pull-requests: write # Comment on the PRs
issues: write # Create and edit issues

"Workflow does not have permission"

What it means: similar to the previous one, but at repository level.

Fix:

  1. Go to Settings, then Actions, then General
  2. Check "Workflow permissions"
  3. Select "Read and write permissions" if needed

"The workflow is not valid"

What it means: a YAML syntax error.

Fix: use actionlint to find the exact error.

"Context access might be invalid"

What it means: you are trying to reach a property that may not exist.

# ❌ Risky if the step has no outputs
- run: echo ${{ steps.build.outputs.version }}
# ✅ Safer with a default value
- run: echo ${{ steps.build.outputs.version || 'unknown' }}

A diagnostic workflow

Here is a complete workflow to trigger manually and diagnose your environment:

name: Diagnostics
on:
workflow_dispatch: # Manual trigger only
# No right is needed: the job only does introspection
permissions: {}
jobs:
diagnostics:
runs-on: ubuntu-24.04
timeout-minutes: 10
steps:
- name: System information
run: |
echo "=== Operating system ==="
uname -a
cat /etc/os-release
echo "=== Resources ==="
echo "CPU: $(nproc) cores"
free -h
df -h
echo "=== Installed tools ==="
echo "Node: $(node --version 2>/dev/null || echo 'not installed')"
echo "Python: $(python3 --version 2>/dev/null || echo 'not installed')"
echo "Docker: $(docker --version 2>/dev/null || echo 'not installed')"
echo "Git: $(git --version)"
- name: GitHub Actions context
env:
# Context values go through env:, never in plain text inside run:
REPO: ${{ github.repository }}
REF: ${{ github.ref }}
COMMIT: ${{ github.sha }}
ACTOR: ${{ github.actor }}
EVENT: ${{ github.event_name }}
RUN_ID: ${{ github.run_id }}
RUN_NUMBER: ${{ github.run_number }}
run: |
echo "Repository: $REPO"
echo "Branch/Tag: $REF"
echo "Commit SHA: $COMMIT"
echo "Actor: $ACTOR"
echo "Event: $EVENT"
echo "Run ID: $RUN_ID"
echo "Run Number: $RUN_NUMBER"
- name: Network test
run: |
echo "=== DNS ==="
nslookup github.com
echo "=== Connectivity ==="
curl -s -o /dev/null -w "GitHub API: %{http_code}\n" https://api.github.com
curl -s -o /dev/null -w "npm Registry: %{http_code}\n" https://registry.npmjs.org
curl -s -o /dev/null -w "PyPI: %{http_code}\n" https://pypi.org
- name: Environment variables
run: |
echo "=== GitHub variables ==="
env | grep GITHUB_ | sort
echo "=== PATH ==="
echo "$PATH" | tr ':' '\n'

To run it: Actions, then Diagnostics, then Run workflow.


Testing locally with act

Rather than pushing and waiting on every change, use act to run your workflows on your own machine.

Fenêtre de terminal
# Install act
brew install act
# Run the default workflow
act
# Run one specific job
act -j build
# Verbose mode for debugging
act -v

See the complete act guide.


Key points

Debugging a workflow rests on four reflexes: validate early, make the logs talkative, keep traces, and shorten the test loop.

Validate the syntax

Use actionlint before pushing to avoid the obvious errors.

Enable the debug mode

The ACTIONS_RUNNER_DEBUG and ACTIONS_STEP_DEBUG variables reveal the hidden details.

Save the artifacts

Use if: failure() to upload the logs when things go wrong.

Test locally

act saves you time by avoiding the back and forth with GitHub.

Debugging checklist:

  1. Check that the workflow sits in .github/workflows/
  2. Validate the syntax with actionlint
  3. Check the trigger filters (on:, branches:, paths:)
  4. Look at the Actions tab (is the workflow disabled?)
  5. Enable the debug logs if the problem persists
  6. Inspect the contexts to understand the state
  7. Save the artifacts on failure

Next steps

  • Speeding things up with the cache: treating the most frequent cause of a slow workflow, once the diagnosis is made.
  • Artifacts vs Cache: picking the right mechanism when the slowdown comes from moving files around.
  • GitHub CLI (gh): reading the logs and restarting a failed run without leaving the terminal.
  • act: replaying the workflow locally to shorten the fix loop, without consuming GitHub minutes.

Is this site useful to you?

Fewer than 1% of readers support this site.

I maintain more than 700 free guides, with no ads and no tracking. Any support, even a symbolic one, helps cover hosting and keeps these resources free. Thank you for the help.

The form does not show? Open Ko-fi in a new tab.

Subscribe and follow my DevSecOps work on LinkedIn