Skip to content

DevOps & CI/CD

GitHub Actions for Velocity X — The Deploy Workflow Aidxn Ships

Netlify auto-builds on every push, but shipping production means pre-deploy gates: lint, type-check, test coverage, Lighthouse CI, and security scans. Aidxn's standard GitHub Actions workflow runs pnpm install, Biome checks, vitest, Playwright e2e on PR, then posts deploy preview links. Skip the manual gate. Real ci.yml, caching strategy, and six FAQs.

🚀

Netlify auto-building on every push is convenient — push code, branch deploys automatically, teammates see previews. But it's also a problem. You just shipped a type error to staging. The linter didn't catch it because you skipped it locally (we've all done it). The CSS broke because nobody ran the full test suite. A teammate merges a bundle-bloating dependency and nobody noticed until the Lighthouse score tanked in production. Netlify's auto-build is a feature, not a substitute for CI/CD. Real production deployments need gatekeepers before code reaches the CDN: lint, type-check, unit tests, end-to-end tests on critical flows, Lighthouse performance CI, and dependency vulnerability scans. GitHub Actions provides those gates for free (up to 2000 minutes/month on public repos). This is the workflow Aidxn ships on every Velocity X site. Here's how it works, why each step matters, and how to adapt it to your stack.

What Netlify Auto-Build Gives (and Doesn't)

What it gives: When you push to main, Netlify's webhook triggers a build. It clones your repo, runs npm run build (from netlify.toml), uploads static files to the CDN, and goes live. Instant feedback, no manual step, transparent. You can see build logs in the Netlify dashboard.

What it doesn't give: any validation before that build runs. If your code has a type error, linter violation, failing test, or a 400KB bundle size, Netlify will cheerfully build it and ship it. You catch the problem when your teammates refresh the site or your monitoring alerts fire. By then, the bad code is live.

The fix: GitHub Actions runs a CI pipeline before Netlify's build. Lint, type-check, and test pass? You get a green ✅ and a deploy preview link. Anything fails? You get a red ✗, the PR blocks, and nobody merges broken code. This is the standard in 2026 — no exceptions.

Aidxn's Standard ci.yml — Annotated

Here's the real workflow we ship. Save this as .github/workflows/ci.yml:

name: CI on: push: branches: [main] pull_request: branches: [main] jobs: test: runs-on: ubuntu-latest timeout-minutes: 15 steps: # Checkout code - uses: actions/checkout@v4 # Setup Node with caching - name: Setup pnpm uses: pnpm/action-setup@v2 with: version: 9 - name: Setup Node uses: actions/setup-node@v4 with: node-version: '20.10' cache: 'pnpm' # Install dependencies - name: Install dependencies run: pnpm install --frozen-lockfile # Lint and format check - name: Lint with Biome run: pnpm biome check --apply=none . # Type check - name: Type check run: pnpm tsc --noEmit # Unit and integration tests - name: Run vitest run: pnpm vitest run --coverage # E2E tests on PR - name: Run Playwright E2E if: github.event_name == 'pull_request' run: pnpm playwright test # Lighthouse CI (optional, on main only) - name: Run Lighthouse CI if: github.ref == 'refs/heads/main' uses: treosh/lighthouse-ci-action@v10 with: configPath: './.github/lighthouse-config.json' temporaryPublicStorage: true

Walk through each step:

Checkout & Setup (the boilerplate)

checkout@v4 — clones your repo into the runner. Standard first step.

pnpm/action-setup + actions/setup-node — installs pnpm 9 and Node 20.10 (match your .tool-versions or package.json engines field). The cache: 'pnpm' flag is critical: it caches node_modules between runs, cutting install time from ~60 seconds to ~5 seconds on subsequent runs.

pnpm install --frozen-lockfile — installs exactly what's in pnpm-lock.yaml. No version drift. --frozen-lockfile fails if the lockfile is out of date (catches stale dependencies before they reach production).

Lint with Biome

pnpm biome check --apply=none . — runs Biome format + lint rules on all files without modifying them (--apply=none). Biome is 10x faster than ESLint + Prettier combined (it's Rust-based). If any file fails the checks, the step fails and the PR is blocked.

Why Biome? Speed. In Aidxn's stack, Biome runs in ~2 seconds on 150 files. ESLint + Prettier would take ~15 seconds. Scale to 50+ projects and the time savings compound.

Type Check

pnpm tsc --noEmit — runs TypeScript compiler without emitting JS. Catches type errors that unit tests might miss: unused variables, wrong types passed to functions, missing imports. Takes ~5 seconds on a medium codebase.

Why not rely on the build? Because Astro/Vite might skip type errors if the transpiler is lenient. tsc --noEmit is strict.

Unit & Integration Tests

pnpm vitest run --coverage — runs vitest in CI mode (no watch), collects coverage. If any test fails, the step fails. Coverage threshold (e.g. 80%) can be enforced via vitest.config.ts:

export default defineConfig({ test: { coverage: { provider: 'v8', reporter: ['text', 'json', 'html'], lines: 80, functions: 80, branches: 70, statements: 80, }, }, });

Coverage enforces a safety bar: if you break tests or drop coverage below the threshold, the pipeline blocks you.

E2E Tests (PR Only)

if: github.event_name == 'pull_request' — this step only runs on PRs, not on pushes to main. Why? E2E tests are slow (Playwright spinning up browsers, clicking through pages) — ~2–5 minutes. You don't want to wait 5 minutes every time you push to main. On PR, you do want to validate that critical user flows work (login, form submission, checkout). This is the right balance.

pnpm playwright test — runs all Playwright tests. If any fail, the PR is blocked.

Lighthouse CI (Production Only)

if: github.ref == 'refs/heads/main' — only runs on pushes to main, not PRs. Lighthouse CI audits your site's performance, accessibility, and best practices. It's slow but runs only on main after tests pass, so it doesn't block PRs.

Caching Strategy — The Speed Multiplier

The cache: 'pnpm' flag caches node_modules between runs. First run: 60 seconds to install. Second run (if dependencies didn't change): 5 seconds. Across 10 PRs a day, that's 550 seconds (9 minutes) saved. Multiply across your team.

You can manually cache build artifacts too:

- name: Cache build artifacts uses: actions/cache@v3 with: path: | dist/ .astro/ key: build-${{ hashFiles('src/**/*', 'package.json') }} restore-keys: build-

This caches your Astro build artifacts. If only markdown or content changed (not code), Astro rebuilds faster using cached assets.

Deploy Preview Link Comment — The UX Win

When Netlify builds a deploy preview, it generates a URL like https://pr-42--my-site.netlify.app. GitHub Actions can post this link as a comment on your PR automatically using Netlify's deploy plugin:

- name: Wait for Netlify preview if: github.event_name == 'pull_request' uses: jakepartusch/wait-for-netlify-action@v2 id: netlify with: site_name: 'my-site' max_timeout: 600 - name: Comment deploy preview link if: github.event_name == 'pull_request' uses: actions/github-script@v7 with: script: | github.rest.issues.createComment({ issue_number: context.issue.number, owner: context.repo.owner, repo: context.repo.repo, body: ✅ Deploy preview ready: ${{ steps.netlify.outputs.url }} })

Now when your teammate creates a PR, they see a comment with the live preview link within 30 seconds. No hunting through Netlify's dashboard. They can click, see the changes live, and approve. This is the UX standard in 2026.

Failure Modes — What to Expect

Scenario 1: Type error in a PR. You submit a PR, CI runs, tsc --noEmit fails because you passed a string where a number was expected. GitHub marks the PR with a red ✗. You can't merge until you fix it. You fix the type, push again, CI re-runs, ✅. You merge.

Scenario 2: Test breaks on main. Someone merges a change that breaks a test. Netlify still auto-builds (Netlify doesn't care about tests), but your CI run on main shows a red ✗ in GitHub's status. You immediately know there's a live bug. You patch it on main, re-push, CI passes again. Next time, you add a test to catch the regression.

Scenario 3: Playwright E2E times out. Your checkout flow test is flaky (sometimes slow). Playwright times out waiting for a button. GitHub marks the PR as failed. You either: a) fix the flaky test, b) increase the timeout, or c) skip the test if it's not critical. You decide which.

Secrets & Environment Variables

If your tests need API keys, database URLs, or auth tokens, store them as GitHub Secrets:

# In GitHub repo settings → Secrets and variables → Actions, add: SUPABASE_URL=https://... SUPABASE_ANON_KEY=...

Then reference them in your workflow:

- name: Run tests with secrets env: SUPABASE_URL: ${{ secrets.SUPABASE_URL }} SUPABASE_ANON_KEY: ${{ secrets.SUPABASE_ANON_KEY }} run: pnpm vitest run

Secrets are masked in logs — you'll see *** instead of the actual value.

Six FAQs

Why run CI if Netlify auto-builds anyway?

Netlify's auto-build is fire-and-forget. It doesn't know if your code is broken — it just builds and ships. CI catches broken code before it reaches the CDN. If CI fails, you don't let Netlify build at all (either by blocking the merge or by running Netlify only after CI passes). This is the standard.

Does GitHub Actions cost money?

Free tier: 2000 minutes/month on public repos, unlimited on private repos for 3000 minutes/month (shared across all your private projects). Most small-to-medium teams stay under the free limit. If you exceed it, it's ~$0.25 per 1000 minutes. Not expensive.

How do I skip CI for a commit?

Add [skip ci] to the commit message: git commit -m "docs: fix typo [skip ci]". GitHub Actions won't run. Use sparingly (only for doc changes, not code).

What if a test is flaky and passes 90% of the time?

Fix the flake. Flaky tests are worse than no tests — they erode trust in your CI. Common causes: timing assumptions (test waits 100ms, sometimes needs 200ms), network calls (use mocks), browser state (reset before each test). Playwright's .waitFor() with exponential backoff handles most timing issues. If a test is truly non-critical, skip it: test.skip('flaky behavior', ...). Document why it's skipped.

Can I run CI on multiple branches (not just main)?

Yes. Change the trigger:

on: push: branches: [main, develop, staging] pull_request: branches: [main, develop, staging]

Now CI runs on pushes to any of those branches, and PRs targeting any of them.

How do I see GitHub Actions logs when something fails?

Go to your repo → Actions tab → click the failed run → click the failed job → expand the failed step. You see the full output. This is where you debug "why did Playwright timeout?"

The Bottom Line

Netlify auto-builds are convenient, but they're not CI/CD. Shipping production code requires gates: lint, type-check, tests, Lighthouse. GitHub Actions provides those gates for free. The workflow above (pnpm install, Biome, tsc, vitest, Playwright) takes ~5 minutes on first run, ~2 minutes on cached runs (because of cache: 'pnpm'). That's 2 minutes between "push code" and "ready for review" — acceptable. Deploy preview links comment automatically, so teammates don't hunt for the URL. Failures block merges, so broken code never reaches main. This is the Aidxn standard. Copy .github/workflows/ci.yml into your project, adapt the Node version and package manager (npm, yarn, pnpm) to match yours, and commit. Within a day, you've eliminated ~80% of the bugs that ship to production.

Building a custom CI/CD pipeline for your team? See engineering and process consulting to audit your current setup and optimize the feedback loop, or dig deeper into local dev environment standardisation to ensure your team's machines match CI.

Let us make some quick suggestions?

Please provide your full name.
Please provide your phone number.
Please provide a valid phone number.
Please provide your email address.
Please provide a valid email address.
Please provide your brand name or website.
Please provide your brand name or website.