GitHub·7 min read·

Debugging GitHub Actions: Re-runs, tmate, Logs and Inputs

Reproduce failing GitHub Actions runs with step debug logs, failed-job re-runs, reproducible local builds, and interactive tmate SSH sessions on the runner.

NB

Netbay Engineering

Netbay Engineering

On this page

A red check next to a workflow stops your team, but the failure message is usually terse. GitHub Actions gives you a ladder of debugging tools: richer logs, seeded secrets, re-runs, and even an interactive SSH session into a live runner with tmate. This post walks the practical techniques in rough order of escalation, from turning on verbose logs to dropping into a shell on the build machine.

Turn on the debug logs

The fastest win is debug logging. GitHub Actions reveals way more when you set two runner-level flags as repository or organization variables. Set ACTIONS_RUNNER_DEBUG=true to get detailed runner logs (step timing, tool cache lookups, setup), and ACTIONS_STEP_DEBUG=true to capture the secret-masked expanded values of each step, including the exact command that ran. The runner also writes a diagnostic log file honoring the variable at the end of each step.

The first thing to do on a confusing failure is re-run with these two variables temporarily set. They add noise, so enable them only while you investigate, then remove them.

yaml
# set once in repo settings as a variable, then delete after debugging
env:
  ACTIONS_RUNNER_DEBUG: "true"
  ACTIONS_STEP_DEBUG: "true"

jobs:
  build:
    runs-on: ubuntu-22.04
    steps:
      - run: ./build.sh

Re-run with the same inputs vs re-run failed jobs

The Run workflow button offers two choices. Re-run all jobs starts the entire workflow from scratch. Re-run failed jobs only re-executes the jobs that failed, using the artifacts and state the earlier successful jobs already produced. Choose the failed-jobs re-run when an earlier build step is expensive and definitely succeeded; choose a full re-run when you changed secrets, actions, or dependency versions and need a clean slate.

If your failure depends on a schedule or a manual input, note that a re-run does not re-collect workflow_dispatch inputs — it replays the original inputs. For a truly fresh run with new inputs, trigger workflow_dispatch again.

Inspect the step logs

Each step's log is expandable in the Actions UI. Look for the exact command executed and any error output. Common patterns: a step silently ran against a wrong Node version, a secret was empty because it was scoped to an environment the job did not target, or a path was relative to the wrong working directory. The step log shows environment variables but masks secret values by design, so a masked (value hidden) entry means the variable existed; empty output usually means the secret was absent.

For secret-scope issues, confirm the job references the right environment — a common cause of an empty secret is referencing an environment secret from a job that does not target that environment.

Simulating the failing step locally

When the failure is about the tooling rather than the environment, reproduce it locally before touching the runner. Build the same install and command sequence in a local shell mirroring the runner's shell (bash by default on Linux runners). You can often shrink the search space by running just the failing script against your local checkout. This is faster than a debug round trip and gives you a real shell with your own tooling.

Keep local debugging honest: use the same user, the same environment variables, and the same working directory layout. A script that works in your shell but fails in the runner often comes down to an environment difference you can now isolate.

Drop into the runner with tmate

When logs are not enough, you can SSH into the live runner. The trick is a tmate step: tmate creates a remote terminal session and prints its host string, which you use to connect. Enable it conditionally so it only runs when you want it — typically under an if on failure, so the session is available exactly when the job fails, or driven by an input.

yaml
- name: Debug with tmate
  if: failure()
  uses: mxschmitt/action-tmate@v3
  with:
    limit-access-to-actor: true

The limit-access-to-actor option restricts who can join the session to the actor who triggered the run, a sensible guard. Connect with the printed host, inspect the filesystem, environment, and leftover workspaces, then log out. Be careful: a tmate step lets someone drive the runner interactively, so gate it and remove it from production workflows before merge.

A practical debugging workflow

Putting it together, a resilient job might catch a failing step and, on failure, print diagnostics and open a session, while keeping normal runs lean:

yaml
jobs:
  build:
    runs-on: ubuntu-22.04
    steps:
      - uses: actions/checkout@v4
      - run: ./build.sh
      - name: Collect diagnostics
        if: failure()
        run: |
          echo "--- pwd ---"
          pwd
          echo "--- node ---"
          node --version || true
          echo "--- step status ---"
          echo "exit code captured in logs"
      - name: Drop to shell
        if: failure() && github.event_name == 'workflow_dispatch'
        uses: mxschmitt/action-tmate@v3
        with:
          limit-access-to-actor: true
red check on job ACTIONS_STEP_DEBUG logs re-run failed jobs local reproduce / tmate SSH escalate: logs -> re-run -> local -> live shell if: failure() + workflow_dispatch keeps it opt-in

Takeaway

Start with debug logs and a re-run of just the failed jobs, reproduce locally when you can, and only escalate to an interactive tmate session for environment-only mysteries. Gate the live-session step so it never runs in normal CI. When you need a real machine to poke at, an Ubuntu VPS on Netbay arrives in under a minute — netbayhosts.in.

Keep reading

Follow along on a real VPS

Deploy Linux in under 60 seconds

These guides are written against Ubuntu, Debian, and RHEL-family images — the same ones on NetBay.

Deploy an instance