GitHub Actions #10 Operations Patterns: Path Filters, Concurrency, Debugging, and Cost Review

5 min read

The syntax and the pipelines are in place. This final part is about the operational habits you need when you run them every day. Each one is a small setting on its own, but without them the minor incidents keep coming: “I only touched the docs and the whole CI ran,” “two deploys overlapped and got tangled,” “a hung job billed six hours.” Let’s gather the pieces we brushed past throughout the series and organize them from an operations point of view.

Path filters: wake only what changed #

In a monorepo (or any ordinary project where docs and code share one repository), letting every push wake every workflow wastes time and money. Put path conditions on the trigger.

.github/workflows/backend.yml
on:
  push:
    branches: [main]
    paths:
      - "backend/**"
      - ".github/workflows/backend.yml"   # include changes to the workflow itself

paths-ignore handles the opposite direction (excluding docs changes, and so on). One convention worth adopting: include the workflow’s own path in paths. It prevents the misstep where a push that fixes the workflow does not trigger that workflow, and the fix gets merged “unverified.” What to watch out for is the combination with required checks. If a workflow that branch protection marks as “must pass” gets filtered out by paths and never runs at all, the PR waits forever. In that case you need an adjustment: either do not make that check conditional, or move the per-path condition down to a job-level if. Terraform Ops #1, where we ran plan only on infrastructure paths, is a real-world example of this filter.

concurrency: turn overlap into policy #

concurrency is your policy for when runs overlap on the same target. Runs with the same group key execute only one at a time, and among those waiting, only the newest one is kept.

Concurrency settings
# CI: cancel previous runs for the same PR (only the latest commit matters)
concurrency:
  group: ci-${{ github.ref }}
  cancel-in-progress: true

# Deploy: let an in-progress deploy finish (interrupting is riskier)
concurrency:
  group: deploy-prod
  cancel-in-progress: false

The criterion is whether the work is safe to interrupt. For PR CI, results for an old commit are meaningless, so cancelling is a win; deploys, migrations, and Terraform apply leave a half-finished state if cut off midway, so let them run to completion and make the next one wait. Group key design follows the same logic. Whether you split per PR (github.ref) or serialize on one global key (deploy-prod) should match your answer to “what must not overlap?”

timeout: don’t trust the 6-hour default #

The default job timeout is 6 hours (360 minutes). If a test hangs, or a step sits waiting on an unresponsive external API, it holds a runner — and bills — for six hours. Make it a habit to set an explicit limit on every job.

.github/workflows/test.yml
jobs:
  test:
    runs-on: ubuntu-latest
    timeout-minutes: 15

Setting the value to roughly two or three times a normal run is enough. A job that dies on timeout was abnormal anyway, and the earlier it dies, the earlier you find out.

Debugging: how to get more out of the logs #

Here are the tools to use when the failure logs are not enough, in order.

  1. Re-run: the Re-run menu on the run page offers an option to re-run only the failed jobs. It is the first tool for telling transient network failures apart.
  2. Debug logs: putting ACTIONS_STEP_DEBUG set to true into repository secrets (or variables) turns on detailed logs from inside each step, and ACTIONS_RUNNER_DEBUG enables runner-level logs. You can also enable it one-off with the “Enable debug logging” checkbox in the re-run dialog, which is the more convenient route.
  3. Context dumps: the toJSON() output from #2 is the fastest path for the “why did this if evaluate differently than I expected” class of debugging.
  4. Local runners: tools like act let you exercise a workflow locally without pushing. The environment is not fully identical to a real runner, so final confirmation still needs an actual run, but it dramatically shortens the round-trip time of YAML syntax trial and error.

Cost review: the screen that shows where it leaks #

Under your organization or account Settings, Billing shows Actions usage broken down by repository, workflow, and runner OS. The review procedure is simple. Open the top workflows by usage and check whether the tools from #4 are applied. Dependency installs without caching, unnecessary matrix axes (macOS in particular), full runs without paths filters, and long-running jobs without timeouts are the usual leak spots. If private-repository usage keeps growing, the self-hosted break-even analysis from #8 is the next step.

Wrapping up the series #

Here are the ten parts in summary. We laid out the structure and syntax (#1–#2), set up secrets and gates (#3), learned how to scale runs up and down (#4), componentized (#5), assembled image builds and an AWS deployment pipeline (#6–#7), established principles for runners and security (#8–#9), and closed today with operational habits. For cases where this series’ workflows meet real infrastructure, see the Terraform track, and if you want to consolidate GitHub itself into a certification, GitHub Foundations is a neighboring series.

What we covered in this post:

  • Use paths filters to wake only what changed, include the workflow’s own path, and watch the combination with required checks
  • The concurrency criterion is “is it safe to interrupt?” — cancel for CI, finish then serialize for deploys
  • Don’t trust the 6-hour default; set timeout-minutes explicitly on every job
  • For debugging, work through re-runs, the debug log toggles, context dumps, and local runners, in that order
  • Review costs on the Billing screen starting from the top workflows; missing caches, macOS matrices, and unfiltered runs are the usual leaks
X