running-github-actions-efficiently
Skill111 starsChanged 18 days agoMITView on GitHub
Cut GitHub Actions minutes and wall-clock time without losing coverage — the OS billing multiplier (macOS 10x / Windows 2x / Linux 1x), trigger hygiene that stops push+pull_request double-firing, concurrency that cancels superseded runs, dependency caching keyed on lockfiles, matrix discipline, and auditing scheduled crons that bill 24/7. Use when CI is burning included minutes, when runs feel slow, when setting up a new repo's workflows, or when reviewing an existing workflow for waste.
- Installs packages
1 other project uses a version of this, 1 of them edited.
Adopted by
Most recently taken up by
- kajisho5/ffmpeg-skilledited, first seen Oct 8, 2026
Other versions1 project uses a version of this.
Word for word · 0
None found.
Edited · 1
- kajisho5/ffmpeg-skill★ 4.8k91% same
Wrote their own version · 0
None found.
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 repository41 other files it gives its agents.
- building-accessible-interfacesSkill
- wiring-application-configSkill
- shipping-build-artifactsSkill
- permissioning-ci-workflowsSkill
- concurrent-branchesSkill
- shipping-across-surfacesSkill
- writing-defect-reportsSkill
- reporting-derived-metricsSkill
- guarding-destructive-operationsSkill
- keeping-git-repos-cleanSkill
- designing-interaction-statesSkill
- designing-marketing-pagesSkill
- rendering-untrusted-contentSkill
- reproducing-ci-locallySkill
- running-resumable-sync-jobsSkill
- normalizing-text-for-measurementSkill
- calibrating-thresholds-and-baselinesSkill
- designing-interfacesSkill
- verifying-external-behaviorSkill
- building-go-projectsSkill
- building-browser-extensionsSkill
- designing-python-apisSkill
- building-python-clisSkill
- improving-python-code-qualitySkill
- building-python-communitiesSkill
- documenting-python-librariesSkill
- deferring-heavy-dependenciesSkill
- reviewing-python-librariesSkill
- building-llm-backed-featuresSkill
- building-python-mcp-serversSkill
- packaging-python-librariesSkill
- optimizing-python-performanceSkill
- setting-up-python-librariesSkill
- managing-python-releasesSkill
- auditing-python-securitySkill
- testing-python-librariesSkill
- building-python-web-appsSkill
- building-rust-cratesSkill
- building-scala-projectsSkill
- building-swift-appsSkill
- shipping-swift-appsSkill
The file
We split this skill into the rules it gives an agent
12 rules. About 0 are common, 5 are distinctive, and 7 are very specific to this project.
≈0
Common · estimate
Many files like this have a rule of this kind.
≈5
Distinctive · estimate
This author's own choices. Worth a look.
7
Very specific
Tied to their project's names, files or tools. Probably skip.
- Common
- Distinctive
- Very specific
- Very specific: line 36
If a single macOS job does both (e.g.
- Very specific: line 61
Use SwiftLint's swiftlint-static (not the dynamically linked swiftlint, which needs a Swift runtime the bare runner lacks) and …
- Very specific: line 71
Don't gamble a repo's only build job on a Linux move — if swift build there depends on such a package, keep it on macOS.
- Very specific: line 85
When you push a feature branch that has an open PR, the push event and the pull_request event both fire a full run of the same commit.
- Distinctive: line 101
Add path filters so unrelated changes don't spin a runner at all:
- Very specific: line 211
Run the full matrix on main / nightly, and a minimal matrix (one OS, min+max version) on PRs.
- Distinctive: line 216
Test the versions you actually support.
- Distinctive: line 217
Prefer Linux cells; add macOS/Windows cells only for genuinely OS-specific code.
- Distinctive: line 261
Verify both a passing matrix and a deliberately failing cell on a disposable branch, then confirm the PR reports the original required …
- Very specific: line 277
Uptime / health checks: do not belong on Actions — a */5 or */15 cron is a runaway meter.
- Very specific: line 298
Add a timeout-minutes: to every job so a hung step can't burn the max 6-hour runner allotment.
- Distinctive: line 305
Don't guess which workflow is expensive — measure:
≈ “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
Running GitHub Actions Efficiently
Actions minutes on private repos bill against a monthly included quota; public repos are free. So cost concentrates in private repos, and a handful of workflows usually dominate the bill. This skill is the checklist for finding and fixing that waste. Every fix below preserves what CI actually verifies — none of them trade away coverage.
First, know the one number that dominates everything
Minutes are billed per job, rounded up to the minute, times a runner-OS multiplier:
| Runner | Multiplier |
Linux (ubuntu-*) | 1x |
Windows (windows-*) | 2x |
macOS (macos-*) | 10x |
A one-minute macOS job costs the same as ten Linux minutes. This single fact reorders every optimization: a repo with a few macOS jobs can outspend a repo with ten times as many Linux jobs. Before anything else, find your macOS jobs and ask of each: does this step genuinely need macOS?
- Xcode / Apple-platform builds and tests (
xcodebuild): yes, macOS is required. - Linters and formatters (even Swift ones like SwiftLint/SwiftFormat), pure
unit tests with no Apple frameworks, packaging, doc builds: usually no —
move them to
ubuntu-latestand cut that job's cost ~10x.
Split the must-be-macOS work into its own job and push everything else to Linux.
If a single macOS job does both (e.g. runs SwiftLint and an Xcode build), split
it: a cheap Linux lint job plus the macOS build job. The tools only parse
source, so the Linux job just checks out and runs them — no toolchain build.
SwiftLint and SwiftFormat both ship self-contained prebuilt Linux binaries, so "Swift means macOS" is a myth for the linting half of the pipeline:
lint:
runs-on: ubuntu-latest # 1x, not 10x
steps:
- uses: actions/checkout@v4
- name: Install SwiftLint & SwiftFormat (Linux)
run: |
set -euxo pipefail
curl -fsSL -o /tmp/sl.zip https://github.com/realm/SwiftLint/releases/download/0.65.0/swiftlint_linux_amd64.zip
unzip -oq /tmp/sl.zip -d /tmp/sl
sudo install -m0755 /tmp/sl/swiftlint-static /usr/local/bin/swiftlint
curl -fsSL -o /tmp/sf.zip https://github.com/nicklockwood/SwiftFormat/releases/download/0.62.1/swiftformat_linux.zip
unzip -oq /tmp/sf.zip -d /tmp/sf
sudo install -m0755 /tmp/sf/swiftformat_linux /usr/local/bin/swiftformat
- run: swiftlint lint
- run: swiftformat . --lintUse SwiftLint's swiftlint-static (not the dynamically linked swiftlint,
which needs a Swift runtime the bare runner lacks) and SwiftFormat's
swiftformat_linux — both are statically linked and run on plain ubuntu-latest.
Pin versions in the URL, curl -f to fail on a bad download, and reference the
exact binary names rather than a fragile find.
What genuinely can't move. A SwiftPM target only builds on Linux if its code
and its dependencies do. Two reliable signals it's macOS-pinned: the package
declares platforms: [.macOS(...)] only, or it depends on an Apple-focused
library (e.g. SQLite.swift, anything importing AppKit/SwiftUI/CloudKit).
Don't gamble a repo's only build job on a Linux move — if swift build there
depends on such a package, keep it on macOS. grep -rE 'import (AppKit|SwiftUI|
Cocoa|CloudKit|CoreData)' over the target's sources is a fast pre-check.
Trigger hygiene: stop paying for the same commit twice
The most common silent waste is a workflow that runs twice on every change:
on:
push: # ← no branch filter: fires on EVERY push to EVERY branch
pull_request: # ← also fires for the PR built from those same commitsWhen you push a feature branch that has an open PR, the push event and the
pull_request event both fire a full run of the same commit. On a 10x macOS
runner that doubles the most expensive thing you have.
Fix — scope push to the branches you actually gate on:
on:
push:
branches: [main] # only post-merge commits to main
pull_request: # all pre-merge validation happens hereNow branch work is validated once (by the PR) and main is validated once
(post-merge). No commit is ever built twice for the same reason.
Add path filters so unrelated changes don't spin a runner at all:
on:
pull_request:
paths-ignore: ['**.md', 'docs/**', '.github/ISSUE_TEMPLATE/**'](Note: a required status check gated on paths can block PRs that legitimately
change nothing in-scope — prefer paths-ignore for docs, or make the check
non-required, rather than paths on a required job.)
Concurrency: kill superseded runs automatically
Without a concurrency block, pushing three commits in quick succession starts
three full runs and lets all three finish. You only care about the last one.
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: trueThis cancels any in-progress run on the same ref when a newer one starts. It is the single highest-leverage line for anyone who pushes iteratively or force-pushes during review. Put it at the top level of every workflow.
One caveat: don't set cancel-in-progress: true on workflows that must run to
completion once started — deploys, releases, anything that writes external state.
Scope those with a distinct group and leave cancellation off (or queue them).
Cache dependencies — keyed on the lockfile
Re-downloading and re-resolving dependencies on every run is pure waste. Cache the
dependency directory, keyed on the lockfile so the cache invalidates exactly
when dependencies change, with a restore-keys fallback for partial hits.
Most setup-* actions have caching built in — prefer it over hand-rolled
actions/cache where it exists:
# Python (uv)
- uses: astral-sh/setup-uv@v6
with:
enable-cache: true
cache-dependency-glob: "uv.lock"
# Python (pip)
- uses: actions/setup-python@v5
with: { python-version: '3.12', cache: 'pip' }
# Node
- uses: actions/setup-node@v4
with: { node-version: '24', cache: 'npm' }
# Go
- uses: actions/setup-go@v5
with: { go-version: '1.22', cache: true } # caches modules + build cacheFor ecosystems without built-in caching, use actions/cache directly:
# Rust (cargo registry + build)
- uses: actions/cache@v4
with:
path: |
~/.cargo/registry
~/.cargo/git
target
key: ${{ runner.os }}-cargo-${{ hashFiles('Cargo.lock') }}
restore-keys: ${{ runner.os }}-cargo-
# SwiftPM (build + package repository cache)
- uses: actions/cache@v4
with:
path: |
.build
~/Library/Caches/org.swift.swiftpm
key: ${{ runner.os }}-spm-${{ hashFiles('Package.resolved') }}
restore-keys: ${{ runner.os }}-spm-
# Docker layers (buildx / build-push-action)
- uses: docker/build-push-action@v6
with:
cache-from: type=gha
cache-to: type=gha,mode=maxCache the dependency graph (downloaded/resolved packages), not fragile
machine-specific build state. Caching compiler-derived artifacts with absolute
paths (e.g. all of Xcode's DerivedData) can produce flaky or wrong builds — the
dependency cache is the safe, high-ROI target.
Two speed wins on macOS runners specifically:
# Skip the multi-minute `brew update` when you just need a couple of tools
- run: brew install swiftlint swiftformat
env:
HOMEBREW_NO_AUTO_UPDATE: "1"
HOMEBREW_NO_INSTALL_CLEANUP: "1"Matrix discipline
A matrix multiplies job count: os: [ubuntu, macos, windows] × python: [3.10, 3.11,
3.12, 3.13] is 12 jobs per run, and the macOS/Windows cells carry the 10x/2x
multipliers. Test broadly, but deliberately:
- Run the full matrix on
main/ nightly, and a minimal matrix (one OS, min+max version) on PRs.if: github.event_name == 'push'gates the expensive cells. fail-fast: true(the default) stops the whole matrix on the first failure — keep it unless you specifically need every cell's result.- Test the versions you actually support. Dropping an EOL runtime is free minutes.
- Prefer Linux cells; add macOS/Windows cells only for genuinely OS-specific code.
Preserve the required check when splitting jobs
Branch protection and rulesets require check names, not the work a job used to
do. Turning a required test job into matrix checks such as test (3.12) and
test (3.13) can leave the original test requirement waiting forever. Inspect
the configured requirements before renaming or splitting jobs; preserve the
exact required name with a small aggregate job:
jobs:
test-matrix:
runs-on: ubuntu-latest
strategy:
matrix:
python: ['3.12', '3.13']
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: ${{ matrix.python }}
- run: python -m pip install -e ".[dev]"
- run: python -m pytest
test:
name: test # exact existing required check name
needs: [test-matrix]
if: ${{ always() }}
runs-on: ubuntu-latest
steps:
- name: Require the matrix to succeed
env:
RESULT: ${{ needs.test-matrix.result }}
run: test "$RESULT" = successalways() makes the aggregate run after a failed dependency; the explicit result
check rejects failure, cancellation and skipping. A bare success step would
publish a green gate even when the matrix failed. Include every job the old gate
promised to verify in needs, and check each result. Do not put workflow-level
path filters on this required gate: a workflow that never starts cannot publish
its aggregate check.
Verify both a passing matrix and a deliberately failing cell on a disposable branch, then confirm the PR reports the original required check name. See GitHub's required-check troubleshooting guidance for dependency and workflow-skipping behavior.
Scheduled workflows bill around the clock
A schedule: cron runs whether or not anyone touched the repo — so it bills
continuously, forever, and is the easiest thing to forget. Audit every one:
on:
schedule:
- cron: '*/15 * * * *' # every 15 min = ~2,880 runs/month. Almost never worth it on Actions.- Uptime / health checks do not belong on Actions — a
*/5or*/15cron is a runaway meter. Use a purpose-built external monitor (many have free tiers). - Daily rebuilds of static content that hasn't changed (e.g. redeploying a
site on a timer) are wasted runs — deploy on push instead, and keep a
workflow_dispatch:for manual rebuilds. - DST/timezone hacks that register two crons and gate one out still pay the runner spin-up (checkout + toolchain setup) for the run that no-ops. Gate before the setup steps, or compute the schedule so only one fires.
- Genuinely periodic jobs (a real nightly build) are fine — just right-size the frequency to how often the input actually changes.
Find them all across a repo:
grep -rl "schedule:" .github/workflows/Failures and long runs still bill
- A job that fails at minute 9 of 10 bills all 9. Order steps cheap-to-expensive and lint/typecheck before the long build, so bad commits die fast.
- Add a
timeout-minutes:to every job so a hung step can't burn the max 6-hour runner allotment. - Flaky tests that auto-retry the whole workflow multiply cost — fix the flake rather than papering over it with reruns.
Measure before and after
Don't guess which workflow is expensive — measure:
- Repo/org billing: Settings → Billing → this month's Actions minutes, and the per-repo breakdown.
- Per-run breakdown (API):
/repos/{owner}/{repo}/actions/runs/{id}/timingreturns billable milliseconds split by OS multiplier. - What runs most (API):
/repos/{owner}/{repo}/actions/runs?created=>=YYYY-MM-DD— count runs per workflow and note the triggeringevent. A workflow with far more runs than you have merges is double-firing or over-scheduled.
Fix the top one or two offenders first; the distribution is almost always long-tailed.
Checklist
Audit:
- [ ] Identified every macOS/Windows job (10x/2x) — each justified, or moved to Linux
- [ ] Listed every schedule: cron and confirmed each is worth running 24/7
- [ ] Compared runs-per-workflow to merge frequency (excess = double-fire/over-schedule)
Triggers:
- [ ] push scoped to gated branches (e.g. [main]); pull_request handles pre-merge
- [ ] No workflow builds the same commit on both push and pull_request
- [ ] paths-ignore excludes docs/markdown-only changes
Concurrency:
- [ ] concurrency + cancel-in-progress on CI workflows
- [ ] Deploy/release workflows use a distinct group and do NOT cancel mid-run
Caching & speed:
- [ ] Dependencies cached, keyed on the lockfile, with restore-keys
- [ ] setup-* built-in caching used where available
- [ ] Cheap checks (lint/typecheck) run before the long build; jobs have timeout-minutes
- [ ] macOS brew steps skip auto-update (HOMEBREW_NO_AUTO_UPDATE)
Matrix:
- [ ] Full matrix on main/nightly; reduced matrix on PRs
- [ ] Only supported runtime versions; only necessary OSesFor the correctness side of CI (what a gate should actually check per language), see the language-specific project/setup skills. This skill is about making that gate cheap and fast without weakening it.
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.

