Why GitHub Actions Workflows Need Optimization Rules
GitHub Actions is the most popular CI/CD platform — and AI-generated workflows are consistently slow and wasteful. The AI generates: sequential jobs when they could run in parallel, no dependency caching (npm install on every run), no matrix strategies (duplicate jobs for each OS/Node version), inline scripts instead of reusable actions, and secrets referenced incorrectly. The result is a 15-minute workflow that could run in 3 minutes.
GitHub Actions charges by the minute for private repos. A workflow that wastes 12 minutes per run, triggered 50 times per day, costs your organization real money — and slows down every developer waiting for CI to pass before merging.
These rules cover: job structure, caching, matrix strategies, reusable workflows, concurrency, secrets, and common workflow patterns (CI, CD, scheduled tasks).
Rule 1: Parallel Jobs with Dependencies
The rule: 'Split workflows into parallel jobs: lint, typecheck, test, and build run simultaneously — not sequentially. Use needs: for jobs that depend on others: deploy needs: [build, test]. Jobs without needs: run in parallel by default. Each job gets a fresh runner — use caching to share node_modules across jobs.'
For the CI pattern: 'jobs: { lint: { runs-on: ubuntu-latest, steps: [...] }, typecheck: { runs-on: ubuntu-latest, steps: [...] }, test: { runs-on: ubuntu-latest, steps: [...] }, build: { runs-on: ubuntu-latest, needs: [lint, typecheck, test], steps: [...] } }. lint, typecheck, and test run in parallel. build runs only after all three pass.'
AI generates one job with sequential steps: checkout → install → lint → typecheck → test → build. If lint takes 2 minutes and test takes 5 minutes, sequential = 7 minutes. Parallel = 5 minutes (the longest job). For 4 jobs, parallel is typically 2-3x faster.
- Parallel by default: lint, typecheck, test — no needs: = runs immediately
- needs: for dependent jobs: build needs: [lint, typecheck, test]
- Each job = fresh runner — use caching to avoid re-installing deps
- Sequential steps within a job — parallel jobs across the workflow
- 4 parallel jobs: 2-3x faster than sequential — real minutes saved per run
lint, typecheck, and test with no needs: run simultaneously. 3 jobs at 2min + 3min + 5min = 5min parallel (longest job), not 10min sequential. One structural change halves CI time.
Rule 2: Dependency Caching
The rule: 'Cache node_modules (or pnpm store) across workflow runs: actions/setup-node with cache: "pnpm" handles this automatically. For pnpm: - uses: pnpm/action-setup@v4 with: { version: 9 } then - uses: actions/setup-node@v4 with: { node-version: 22, cache: "pnpm" }. This caches the pnpm store — subsequent runs skip downloading packages entirely.'
For Turborepo/Nx cache: 'Cache the build cache directory: - uses: actions/cache@v4 with: { path: .turbo, key: turbo-${{ hashFiles("**/pnpm-lock.yaml") }}-${{ github.sha }}, restore-keys: turbo-${{ hashFiles("**/pnpm-lock.yaml") }}- }. This restores Turborepo/Nx cached outputs — builds that have not changed are skipped.'
AI runs pnpm install from scratch on every workflow run — downloading and extracting hundreds of packages every time. With caching, subsequent runs restore from cache in seconds. The cache key should be based on the lockfile hash — when deps change, the cache invalidates.
Without caching, pnpm install downloads and extracts hundreds of packages on every run. With actions/setup-node cache: 'pnpm', subsequent runs restore from cache in seconds. One line saves 1-3 minutes per job.
Rule 3: Matrix Strategies for Multi-Version Testing
The rule: 'Use matrix strategy to test across Node versions and operating systems: strategy: { matrix: { node-version: [18, 20, 22], os: [ubuntu-latest, macos-latest, windows-latest] } }. This creates 9 jobs (3 versions x 3 OS) from one job definition. Use fail-fast: false to let all matrix combinations complete even if one fails — see the full picture.'
For smart matrices: 'Test all versions on Ubuntu (cheap, fast), only the primary version on macOS/Windows (expensive, slow): strategy: { matrix: { include: [{ os: ubuntu-latest, node: 18 }, { os: ubuntu-latest, node: 20 }, { os: ubuntu-latest, node: 22 }, { os: macos-latest, node: 22 }, { os: windows-latest, node: 22 }] } }. This tests version compatibility (3 jobs) + OS compatibility (2 jobs) = 5 jobs instead of 9.'
AI duplicates entire job definitions for each version — 3 identical jobs with different node-version settings. Matrix replaces duplication with one parameterized definition. Changes to the job template update all matrix combinations at once.
Rule 4: Reusable Workflows and Composite Actions
The rule: 'Extract common workflow patterns into reusable workflows (.github/workflows/reusable-ci.yml with workflow_call trigger) or composite actions (.github/actions/setup/action.yml). Use for: setup steps (checkout + install + cache), deployment steps, and notification steps. Call reusable workflows: uses: ./.github/workflows/reusable-ci.yml with: { node-version: 22 }.'
For composite actions: 'Create setup actions for consistent environment setup: .github/actions/setup/action.yml: { runs: { using: composite, steps: [checkout, pnpm setup, node setup with cache, pnpm install --frozen-lockfile] } }. Use in every job: - uses: ./.github/actions/setup. One place for setup logic — 5 steps become 1.'
AI copies the same 5 setup steps into every job: checkout, install pnpm, setup node, restore cache, install deps. With a composite action, each job uses one line. When you add a step (like Turbo cache), update one file — all jobs get it automatically.
Rule 5: Concurrency, Secrets, and Permissions
The rule: 'Set concurrency groups to cancel redundant runs: concurrency: { group: ${{ github.workflow }}-${{ github.ref }}, cancel-in-progress: true }. When you push again to a branch, the previous workflow run is cancelled — no wasted minutes on outdated code. Use for CI workflows. Do NOT cancel deploy workflows (cancel-in-progress: false) — partial deploys are dangerous.'
For secrets: 'Reference secrets with ${{ secrets.SECRET_NAME }}. Never echo secrets in logs: run: echo ${{ secrets.TOKEN }} exposes the value (GitHub masks it, but not always reliably). Use environment secrets for deployment: environment: production (requires approval). Set least-privilege permissions: permissions: { contents: read, pull-requests: write } — never use the default broad permissions.'
For permissions: 'Set minimal permissions per workflow: permissions: { contents: read } is sufficient for CI. Add pull-requests: write only for workflows that comment on PRs. Add packages: write only for publishing. Never leave permissions unset — the default (write-all) is too broad for most workflows.'
- Concurrency: cancel-in-progress: true for CI — false for deploys
- Group: workflow + ref — cancels previous run on same branch
- Never echo secrets — use environment secrets for deploy approval
- Minimal permissions per workflow: contents: read for CI — add only what needed
- Default permissions (write-all) are too broad — always set explicitly
Push twice to a branch = two workflow runs on outdated code. concurrency: { cancel-in-progress: true } cancels the first run when the second starts. No wasted minutes on code you already replaced.
Complete GitHub Actions Rules Template
Consolidated rules for GitHub Actions workflows.
- Parallel jobs: lint + typecheck + test simultaneously — needs: for dependent jobs
- Dependency caching: actions/setup-node cache: pnpm — .turbo cache for build artifacts
- Matrix strategy: multi-version + multi-OS — fail-fast: false for full picture
- Reusable workflows: workflow_call — composite actions: .github/actions/setup/
- Concurrency: cancel-in-progress for CI — never for deploys
- Secrets: never echo — environment secrets for deploy approval
- Minimal permissions: contents: read default — add only what each workflow needs
- actions/checkout@v4, actions/setup-node@v4, actions/cache@v4 — always pin versions