Skip to main content

setup-github-actions

Write or optimize GitHub Actions workflows for private repos so they use as little of the plan's included Actions usage as possible - runner choice (ubuntu-slim, arm64), triggers, concurrency, timeouts, job layout, caching, and Claude Code review jobs. Use when creating or editing any workflow under .github/workflows, adding CI or deploy jobs, or when Actions minutes or billing run high.

Source facts

Repository
dvcrn/skills
Last source activity
October 30, 2026 at 08:32
Detected SKILL.md language
English
Stars
0
Forks
0

Install options

The review-first prompt is selected by default. You can switch to a direct command or download a local copy.

Review the source files

Read SKILL.md and any companion files shown by SkillsMP before deciding whether to install.

Showing SKILL.md

SKILL.md
Source instructions · Read-only preview
name
setup-github-actions
description
Write or optimize GitHub Actions workflows for private repos so they use as little of the plan's included Actions usage as possible - runner choice (ubuntu-slim, arm64), triggers, concurrency, timeouts, job layout, caching, and Claude Code review jobs. Use when creating or editing any workflow under .github/workflows, adding CI or deploy jobs, or when Actions minutes or billing run high.
# Cost-efficient GitHub Actions Private repos draw from the account's included Actions usage, which is a dollar amount (Pro: about $18/month), not a minute count. Every job is billed separately and rounded up to the whole minute. Public repos are free on standard runners, so this matters for private repos. ## Runner choice | Label | Spec | $/min | Use for | |---|---|---|---| | `ubuntu-24.04-arm` | 2 vCPU | $0.005 | Default for any job that runs code: tests, compiles, typechecks, bundling, linters that compile, Dialyzer, e2e, `services:`, Docker, agents that run commands | | `ubuntu-slim` | 1 vCPU, 5 GB, container | $0.002 | Jobs that mostly wait or do almost nothing: remote builds/deploys, API calls, `paths-filter`, label/issue automation, notifications, release notes | | `ubuntu-latest` | 2 vCPU x64 | $0.006 | Only when part of the toolchain has no linux/arm64 build | CPU-bound work on `ubuntu-slim` runs about 2.5 to 3x slower than on a 2-vCPU runner (measured: a Claude review that runs the test suite took 9 min on slim vs 3 min on `ubuntu-latest`). At 3x slower slim costs the same and feedback is 3x later, so slim only pays off for jobs that spend their time waiting. ### Picking `ubuntu-slim` Walk these in order; the first "yes" picks the runner. 1. Does the job need `services:`, `container:`, Docker, or a Docker-based action (`runs.using: docker` in its `action.yml`, e.g. `getsentry/action-release@v3`)? **Yes → `ubuntu-24.04-arm`.** (Or replace the Docker action with its CLI at a pinned version, e.g. `npx @sentry/cli@<version>`, and continue.) 2. Does it do CPU-bound work: compile, run tests, typecheck, bundle, Dialyzer, a linter that compiles the project or installs deps, or an agent (Claude Code, Codex) allowed to run builds or tests? **Yes → `ubuntu-24.04-arm`.** 3. Does it need more than 5 GB RAM or 14 GB disk, or could it ever run longer than 10 minutes (the hard job limit on slim is 15)? **Yes → `ubuntu-24.04-arm`.** 4. Otherwise it waits on something remote or barely runs: **`ubuntu-slim`.** Examples: `flyctl deploy --remote-only`, Cloudflare/Vercel deploys that build remotely, `gh` CLI calls, Slack/webhook notifications, `paths-filter`, labelers, release drafting, a Claude review restricted to reading the diff (no shell tools). Then measure: a slim job must stay under 3x its 2-vCPU duration and well under 15 minutes. If it doesn't, move it to arm64. Slim has minimal preinstalled tools; install what the job needs with `setup-*` actions or `jdx/mise-action`. On arm64, check that steps don't download amd64-only binaries. ## Triggers - Run CI on `pull_request`. Don't rerun the full suite on `push` to the default branch when changes land through PRs: the PR run already tested `refs/pull/N/merge`. If commits are also pushed directly, keep `push` with only the fast jobs. - Caches saved on a PR ref are visible only to that PR; other PRs restore from the default branch. When `push` no longer runs the suite, keep a cache-warming job on `push` to the default branch, filtered with `paths:` to the lockfiles (`mix.lock`, `go.sum`, `package-lock.json`, `bun.lock`), that only installs and compiles dependencies. Otherwise every new PR starts cold after a dependency change. - Deploy workflows run on `push` to the default branch and need no test rerun alongside them. - Gate jobs by area with workflow-level `paths:` when there are no required status checks. With required checks, use one workflow with a `dorny/paths-filter` job on `ubuntu-slim` and per-job `if:`, because a workflow skipped by `paths:` leaves its check pending. The filter job bills at least 1 minute per run, so use it only when the jobs it skips cost noticeably more. ## Every workflow ```yaml permissions: contents: read concurrency: group: ${{ github.workflow }}-${{ github.ref }} cancel-in-progress: ${{ github.event_name == 'pull_request' }} ``` Grant extra `permissions` per job only where needed. `cancel-in-progress` stays false outside PRs, so a second merge never aborts a running deploy; deploy workflows use a fixed group (`concurrency: deploy-<app>`) with `cancel-in-progress: false`. Set `timeout-minutes` on every job. The default is 360, so one hung deploy can cost 6 hours. Use about 3x the normal duration: 10 for deploys, 15 to 20 for test suites. ## Job layout - Each job repeats checkout, toolchain setup, cache restore and minute rounding. Merge small jobs that share a toolchain (lint + test on the same stack, a boot check that needs the test build) into one job. - Use `parallel:` steps inside the merged job for independent work. Steps that wait on I/O overlap well; CPU-heavy steps (compile, Dialyzer, `go test -race`) on a 2-vCPU runner mostly compete. Environment changes from a `setup-*` step inside a `parallel:` group apply only after the group ends. - Keep jobs separate when `paths:` or `if:` gating will skip most of them on a typical PR. ## Caching - Cache build output that takes longer to rebuild than to restore: Mix `deps` + `_build`, Dialyzer PLTs, Go modules/build cache (`setup-go` does this), package manager stores (`setup-node` `cache:`, `jdx/mise-action` `cache: true`), Playwright browsers. - Include `runner.os`, `runner.arch` and the lockfile hash in every key that holds native build output. Without `runner.arch`, an x64 `_build` restores onto arm64 and NIFs or cgo binaries break. - `restore-keys` must be a prefix of the save `key`. A mismatched prefix never matches, so a lockfile change rebuilds from scratch: ```yaml key: ${{ runner.os }}-${{ runner.arch }}-mix-${{ hashFiles('**/mix.lock') }} restore-keys: | ${{ runner.os }}-${{ runner.arch }}-mix- ``` - Don't `go install` tools from source on every run. Install a pinned version through `jdx/mise-action`, or download a pinned release binary and verify its SHA-256 checksum. ## Claude Code review job - `runs-on: ubuntu-24.04-arm` with `timeout-minutes: 20` for reviews that may run the build or tests, and for `@claude` implementation jobs. - `ubuntu-slim` only fits a review whose allowed tools exclude shell commands, so it only reads the diff and posts comments. Set `timeout-minutes: 15`. - Trigger reviews on `pull_request: types: [opened, ready_for_review]` with `if: ${{ !github.event.pull_request.draft }}`, so a PR opened as a draft is reviewed once when it becomes ready. Don't trigger on `synchronize`, or every push to a PR starts a new review. For label-triggered reviews, use `types: [labeled]` with `if: github.event.label.name == '<label>'`. - Group by PR so a new run replaces a pending one. Comment-triggered runs (`issue_comment`) carry the number in `issue`, not `pull_request`: `concurrency: { group: claude-${{ github.event.pull_request.number || github.event.issue.number }}, cancel-in-progress: true }`. - Skip bot-authored PRs by author, not actor (on `ready_for_review` and `labeled` the actor is whoever changed the PR): `if: ${{ !endsWith(github.event.pull_request.user.login, '[bot]') }}`. - Turn off Copilot code review in the same repo: it also bills Actions minutes. ## Dependabot Use `groups:` in `.github/dependabot.yml` so updates land as one PR per ecosystem, not one per package. Each PR triggers a full CI run. ## Measuring usage Billing needs the `user` scope (`gh auth refresh -h github.com -s user`). Organization repos use `/organizations/<org>/settings/billing/usage` instead. Compare runners by `grossAmount`, since minutes on different runners cost different amounts: ```sh gh api "/users/<user>/settings/billing/usage?year=2026&month=10" \ -q '[.usageItems[] | select(.product=="actions" and .unitType=="Minutes")] | group_by(.repositoryName) | map({repo: .[0].repositoryName, min: (map(.quantity) | add), usd: (map(.grossAmount) | add)}) | sort_by(-.usd)' ``` The report breaks usage down only to the repo. For per-workflow numbers, sum job durations from `repos/<owner>/<repo>/actions/runs/<id>/jobs`, rounding each job up to the minute.
View on GitHub