Test, build, and share data

Design the job graph

Split work by independent outcome and connect jobs with explicit needs only where ordering matters.

One giant job with twenty steps is easy to write and hard to debug. Split work by outcome, then connect jobs only where data or gates require it.

Independent checks should run in parallel. Build should wait for tests. Deploy should wait for build and approval.

When everything lives in one job, you lose per-gate timing in the Actions UI. You also re-run lint when only tests failed.

Jobs also let you scope permissions differently. Deploy can write while test stays read-only.

A typical graph

jobs:
  lint:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - run: npm ci && npm run lint

  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - run: npm ci && npm test

  package:
    needs: [lint, test]
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - run: npm ci && npm run build
      - uses: actions/upload-artifact@v4
        with:
          name: site
          path: dist/

  deploy:
    needs: package
    environment: production
    runs-on: ubuntu-latest
    steps:
      - uses: actions/download-artifact@v4
        with:
          name: site
          path: dist/
      - run: ./scripts/deploy.sh dist/

lint and test start together. package waits for both. deploy waits for package.

Each job name tells you what failed. “test” failing is clearer than step 14 of “ci”.

Avoid fake dependencies

Do not add needs: [lint, test, typecheck, audit, ...] to every job just because those jobs exist.

Ask: does this job consume output from that job? If not, parallelize it or merge it.

Also avoid one job that runs lint, test, build, and deploy sequentially when lint and test could overlap. You pay wall-clock time on every push.

Measure before and after

Time a serial workflow, then reshape it into a graph. Compare total minutes and how fast you find the failing gate.

A realistic win: three parallel five-minute jobs finish in about five minutes. One fifteen-minute job always takes fifteen.

If two jobs need the same setup, duplicate checkout and install rather than serializing unrelated work through fake needs: edges.

Try this on your own project: list required outputs and gates on paper, then remove one unnecessary needs: edge and watch what still runs.

Lesson completed