- Measure performance - Establish a baseline first. Capture job duration, queue time, cache hit rate, and any user-visible latency.
- Wait less
- Minimize runner queuing - Ensure sufficient runner capacity to avoid wasted time.
- Fail fast - Group quick pre-checks (lint, type-check) early so broken PRs are rejected ASAP.
- Do less
- Conditional filters - Use
if:expressions and path filters so only jobs affected by a change run. - Caching - Re-use dependency installs (
actions/cache) and compiled layers (docker/build-push-action) to avoid redundant work.
- Conditional filters - Use
- Improve resource usage
- Parallelize (cores + jobs) - Split test matrices by OS/language version; leverage job-level
strategy.matrixand ensure your tests are utilizing all available CPU cores. - Avoid emulation - Native ARM and x86 runners beat emulators (e.g., skip QEMU where possible)
- Parallelize (cores + jobs) - Split test matrices by OS/language version; leverage job-level
- Prevent slowdowns - Track long-tail job times, set alerts when they regress, and prune unused caches/artifacts to keep runtimes predictable.
- Mono- vs. multi-repo considerations - Think through how you will share logic across projects to avoid copy-pasting and maintining many copies of similar logic.
- Define a “CI API” for each service
- Single task runner/build tool - Tools like Task, Make, or Bazel hide implementation details so workflows simply call
task test. - Standard commands - Adopt a common contract (
install,test,build,dev) so every repo behaves the same on local machines and in CI.
- Single task runner/build tool - Tools like Task, Make, or Bazel hide implementation details so workflows simply call
- Avoid duplication - Extract shared steps into Composite Actions or Reusable Workflows (
.github/workflows/reusable.yml) and version them just like libraries. - Iterate locally - Use
actand local scripts to dry-run jobs before pushing, shortening the feedback loop.
- Grant the minimum necessary permissions - Limit workflow tokens with
permissions:and job-levelenv:; preferGITHUB_TOKENover personal tokens. - Avoid long-lived credentials - Use OpenID Connect (OIDC) to request short-lived cloud creds at job runtime instead of storing secrets.
- Pin action versions with the SHA - Reference actions using the commit SHA (e.g.,
@<sha>), to prevent unexpected updates and potential security issues. - Don’t allow self-hosted runners on fork PRs - Restrict forked PRs to GitHub-hosted runners to mitigate supply-chain attacks.
- Require approval for protected environments - Tie production deploys to an Environment with
required_reviewers; manual approval gates stop compromised branches from auto-shipping.