DEV Community

Cover image for Retry failed steps in GitHub Actions at the right level
Kay
Kay

Posted on Originally published at latchkey.dev

Retry failed steps in GitHub Actions at the right level

GitHub Actions has no built-in step retry, so to have GitHub Actions retry failed steps you choose one of four patterns: a retry action such as nick-fields/retry, a job re-run with gh run rerun --failed, the test runner's own retries, or continue-on-error with a second step. Each repeats something different, so retry at the level that gets a fresh copy of whatever failed, and never retry an assertion silently.

Four retry levels compared: what each repeats, whether it gets a new machine, and what it can hide
Retry at the level that replaces what failed. Only a job re-run gets a new machine; only a test retry knows which test flipped.

A step failed for a reason that had nothing to do with your change, and you want the workflow to try again by itself. The workflow syntax has continue-on-error and timeout-minutes but no retry key, so every option is a pattern on top of something else.

The patterns are not interchangeable. A step retry reruns a command on the same machine. A job re-run gets a new machine. A test retry reruns one test inside the same process tree. Choosing the wrong one either fails to fix the failure or hides the bug that caused it.

Four levels, four different things repeated

Pick by what failed. A network fetch inside a step wants the step retried. A machine that ran out of memory or lost its connection wants a new machine, which only a job re-run provides. A single test that flips wants the test runner, because the runner is the only level that knows which test it was.

Level Mechanism What runs again New machine What it can hide
Step nick-fields/retry, or a second step after continue-on-error One command No An assertion failure, if the command is the test suite
Job gh run rerun --failed, or Re-run failed jobs in the UI Failed jobs and their dependents Yes on GitHub-hosted Any intermittent bug, unless the attempt count is recorded
Test Playwright retries, jest.retryTimes, pytest --reruns, Surefire rerunFailingTestsCount The failed test No Races and shared state, unless flaky results are reported
None continue-on-error: true Nothing; the failure is ignored No Everything that step or job checks

Step level: retry the fetch, not the suite

nick-fields/retry v4.0.0 is the common step-level option. It needs command, max_attempts, and exactly one of timeout_minutes or timeout_seconds. Version 4 moved the action to the Node 24 runtime; the inputs did not change.

Read retry_on carefully. In the action's source, error means any nonzero exit, so wrapping npm test retries assertion failures exactly like network errors. Wrap the step that talks to the network, such as the install, and leave the test step alone. retry_on_exit_code narrows it to one exit code.

The action keeps its retries visible by default: warning_on_retry is true, so each failed attempt becomes a warning annotation reading Attempt 1 failed. Reason: followed by the error, and the total_attempts output can go into the step summary.

- id: install
  uses: nick-fields/retry@ad984534de44a9489a53aefd81eb77f87c70dc60 # v4.0.0
  with:
    command: npm ci
    max_attempts: 3
    timeout_minutes: 10
    retry_on: error
- if: always()
  run: echo "npm ci attempts=${{ steps.install.outputs.total_attempts }}" >> "$GITHUB_STEP_SUMMARY"
- run: npm test   # not wrapped: an assertion fails once and stays failed
Enter fullscreen mode Exit fullscreen mode

Inputs from action.yml and src/index.ts in nick-fields/retry at v4.0.0 (released 2026-03-20), read 2026-11-12.

Job level: a new machine, the same commit

A job re-run runs the same GITHUB_SHA on a new runner, which is the only retry that helps when the machine itself was the problem. From the UI it is Re-run jobs, then Re-run failed jobs; from the terminal it is gh run rerun <run-id> --failed, which also re-runs the jobs that depend on them. A run can be re-run at most 50 times, within 30 days.

To make it automatic without making it blind, trigger on workflow_run, re-run once, and only when the failed log carries a machine signal. The run_attempt == 1 guard caps it at one automatic re-run. Note that workflow_run triggers only if the workflow file exists on the default branch.

name: Re-run machine failures once
on:
  workflow_run:
    workflows: [CI]
    types: [completed]
permissions:
  actions: write
jobs:
  rerun:
    if: github.event.workflow_run.conclusion == 'failure' && github.event.workflow_run.run_attempt == 1
    runs-on: ubuntu-latest
    env:
      GH_REPO: ${{ github.repository }}
      GH_TOKEN: ${{ github.token }}
      RUN_ID: ${{ github.event.workflow_run.id }}
    steps:
      - run: |
          if gh run view "$RUN_ID" --log-failed |
             grep -qE 'exit code 137|lost communication with the server|No space left on device|ECONNRESET|EAI_AGAIN'; then
            echo "Re-ran run $RUN_ID once: machine signal in the failed log" >> "$GITHUB_STEP_SUMMARY"
            gh run rerun "$RUN_ID" --failed
          else
            echo "Left run $RUN_ID red: no machine signal" >> "$GITHUB_STEP_SUMMARY"
          fi
Enter fullscreen mode Exit fullscreen mode

Re-run limits from the re-running workflows and jobs page, and the workflow_run default-branch rule from the events reference, both on docs.github.com, read 2026-11-12. canonical/snapd runs the same shape (workflow_run, actions: write, gh run rerun --failed with github.token, a run_attempt cap) in .github/workflows/rerun.yaml.

Test level: the runner's own retries

Test runners retry one test, in the same job, and the good ones record that they did. Playwright marks a test that failed then passed as flaky. Surefire counts it under Flakes in its summary. pytest-rerunfailures prints rerun counts, and its --only-rerun takes a regular expression, so you can rerun a connection error and never an AssertionError. Jest's retryTimes works only with the default jest-circus runner and must be called at the top level of a file or in a describe block.

This is the right level for evidence, the wrong level for machine faults: a retried test still runs on the machine that just ran out of memory.

```Test runner retry settings

Playwright, playwright.config.ts

retries: process.env.CI ? 2 : 0,

Jest (jest-circus), top of a test file

jest.retryTimes(2, { logErrorsBeforeRetry: true });

pytest-rerunfailures: rerun connection errors, never assertions

pytest --reruns 2 --only-rerun ConnectionError

Maven Surefire

mvn -Dsurefire.rerunFailingTestsCount=2 test




## continue-on-error is not a retry

`continue-on-error: true` on a step lets the job pass when the step fails; on a job it lets the workflow run pass when the job fails. Nothing runs again. Its legitimate uses are experimental matrix legs and the native retry below, where `steps.<id>.outcome` still reads `failure` while `conclusion` reads `success`, so a second step can try once more and say so with a warning annotation.



```.github/workflows/ci.yml
- id: first
  continue-on-error: true
  run: docker pull ghcr.io/acme/test-db:16
- if: steps.first.outcome == 'failure'
  run: |
    echo "::warning title=Retried::docker pull failed once and was retried"
    docker pull ghcr.io/acme/test-db:16
Enter fullscreen mode Exit fullscreen mode

continue-on-error, outcome and conclusion as defined in the workflow syntax and contexts references on docs.github.com, read 2026-11-12.

FAQ

Does GitHub Actions have a built-in retry for steps?

No. The workflow syntax has continue-on-error and timeout-minutes but no retry key. Use a retry action such as nick-fields/retry for a step, gh run rerun --failed for jobs, or your test runner's retries for individual tests.

How do I re-run only the failed jobs in GitHub Actions?

In the run summary, open the Re-run jobs dropdown and choose Re-run failed jobs, or run gh run rerun <run-id> --failed. Jobs that depend on the failed ones run again too. The re-run uses the same commit SHA and the privileges of the actor who triggered the original run.

How do I retry a GitHub Actions step on failure?

Wrap the command in nick-fields/retry with max_attempts, a timeout and command, or run the step with continue-on-error: true and add a second step that runs when steps.<id>.outcome is failure. Retry the network call, not the test suite.

Do retries hide real bugs?

They can. A retry that turns a race into a green build removes the evidence while the bug stays. Retries are safe when they are scoped to machine and network failures and when every retry leaves a record: a warning annotation, a flaky count or an attempt number in the summary.

References

Top comments (0)