ci: cache Docker layers, add concurrency, parallelize smoke, pin bun … #1062
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| name: CI | |
| on: | |
| push: | |
| branches: [main] | |
| pull_request: | |
| branches: [main] | |
| # Cancel superseded runs on a PR branch (new push kills the old run), but never | |
| # cancel a push to main: each main commit must complete so its SHA-tagged image | |
| # lands in ECR (see build-and-push). On push events head_ref is empty, so the | |
| # group falls back to the unique run_id — making every main run its own group. | |
| concurrency: | |
| group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }} | |
| cancel-in-progress: true | |
| # Single source of truth for the bun toolchain version across every job. | |
| # Pinned (not `latest`) for deterministic CI; bump here to upgrade. | |
| env: | |
| BUN_VERSION: "1.3.14" | |
| # Each job runs a single `bun run verify:*` (or underlying) script. Do not | |
| # inline individual check steps here — package.json is the source of truth | |
| # for what "verify" means. If a job needs a new check, add it to the matching | |
| # verify subscript in package.json so local `bun run verify` stays in parity. | |
| jobs: | |
| lint-and-check: | |
| name: Lint & Typecheck | |
| runs-on: ubuntu-latest | |
| steps: | |
| - uses: actions/checkout@v6 | |
| - uses: oven-sh/setup-bun@v2 | |
| with: | |
| bun-version: ${{ env.BUN_VERSION }} | |
| - name: Install dependencies | |
| run: bun install | |
| - name: Install web dependencies | |
| run: cd web && bun install | |
| - name: Verify (static) | |
| run: bun run verify:static | |
| test-unit: | |
| name: Unit Tests | |
| runs-on: ubuntu-latest | |
| steps: | |
| - uses: actions/checkout@v6 | |
| - uses: oven-sh/setup-bun@v2 | |
| with: | |
| bun-version: ${{ env.BUN_VERSION }} | |
| - name: Install dependencies | |
| run: bun install | |
| - name: Install web dependencies | |
| run: cd web && bun install | |
| # Bundle UI deps must be installed so each bundle's co-located tests | |
| # (e.g. the automations markdown sanitizer contract) can resolve their | |
| # own npm dependencies (marked, dompurify, happy-dom). `test:bundles` | |
| # runs `bun test` inside each src/bundles/*/ui package. Mirrors the | |
| # install loop inside build:bundles; CI doesn't build bundles for the | |
| # unit job, so the install must be explicit. | |
| - name: Install bundle UI dependencies | |
| run: bun run install:bundles | |
| - name: Verify (unit + web tests) | |
| run: bun run verify:test-unit | |
| test-integration: | |
| name: Integration Tests | |
| runs-on: ubuntu-latest | |
| steps: | |
| - uses: actions/checkout@v6 | |
| - uses: oven-sh/setup-bun@v2 | |
| with: | |
| bun-version: ${{ env.BUN_VERSION }} | |
| - name: Install dependencies | |
| run: bun install | |
| - name: Integration tests | |
| run: bun run test:integration | |
| smoke: | |
| name: Smoke Tests | |
| # Runs on PRs and on main, in parallel with the other test jobs — NOT chained | |
| # behind unit/integration. build-and-push gates on all four test jobs (incl. | |
| # smoke), so nothing builds until every test is green; chaining smoke only | |
| # delayed that gate without adding safety. Smoke hits the live mpak registry + | |
| # downloads bundles (~15s, network). Not gated to main-only: that previously | |
| # let a stale smoke assertion merge green and turn main red post-merge. | |
| runs-on: ubuntu-latest | |
| steps: | |
| - uses: actions/checkout@v6 | |
| - uses: actions/setup-python@v6 | |
| with: | |
| python-version: "3.13" | |
| - uses: oven-sh/setup-bun@v2 | |
| with: | |
| bun-version: ${{ env.BUN_VERSION }} | |
| - name: Install dependencies | |
| run: bun install | |
| - name: Install mpak | |
| # Intentionally @latest: smoke validates the live registry against the | |
| # current mpak. Do NOT pin — a fixed version blinds the check it exists for. | |
| run: npm install -g @nimblebrain/mpak@latest | |
| - name: Smoke tests | |
| run: bun run smoke | |
| # Build + push a SHA-tagged image for every commit that lands on main, so | |
| # `make deploy` can roll out any main commit without a manual local push. | |
| # This is the everyday counterpart to release.yml (which builds on `v*` | |
| # tags): same ECR repos, same `:<short-sha>` tag — a `v*` release just adds | |
| # the version tag and GHCR/`:latest` on top. Gating on every check above | |
| # means an image only exists in ECR if it built green, which closes the | |
| # "deployed a tag that was never pushed" hole. | |
| # | |
| # `if` restricts this to direct pushes to main (post-merge), never PRs — so | |
| # the OIDC/ECR credentials are never exposed to fork PRs, and only verified | |
| # main commits get published. | |
| build-and-push: | |
| name: Build & Push Images | |
| needs: [lint-and-check, test-unit, test-integration, smoke] | |
| if: github.event_name == 'push' && github.ref == 'refs/heads/main' | |
| runs-on: ubuntu-latest | |
| permissions: | |
| id-token: write | |
| contents: read | |
| steps: | |
| - uses: actions/checkout@v6 | |
| # buildx (docker-container driver) is required for the GitHub Actions | |
| # layer cache below. The cache restores the base layers (apt deps, node, | |
| # bun, mpak) that don't change between commits, so a warm build skips them. | |
| - uses: docker/setup-buildx-action@v3 | |
| - uses: aws-actions/configure-aws-credentials@v6 | |
| with: | |
| role-to-assume: ${{ vars.AWS_ROLE_ARN }} | |
| aws-region: us-east-1 | |
| - uses: aws-actions/amazon-ecr-login@v2 | |
| id: ecr | |
| - name: Set image tag | |
| id: tag | |
| run: echo "sha=$(git rev-parse --short HEAD)" >> "$GITHUB_OUTPUT" | |
| - name: Build and push platform image | |
| uses: docker/build-push-action@v6 | |
| with: | |
| context: . | |
| file: Dockerfile | |
| platforms: linux/amd64 | |
| push: true | |
| build-args: | | |
| BUILD_SHA=${{ steps.tag.outputs.sha }} | |
| tags: ${{ steps.ecr.outputs.registry }}/nimblebrain/agent-platform:${{ steps.tag.outputs.sha }} | |
| # Per-image cache scope so the platform and web caches never overwrite | |
| # each other. mode=max caches intermediate (multi-stage) layers too. | |
| cache-from: type=gha,scope=platform | |
| cache-to: type=gha,mode=max,scope=platform | |
| # NOTE: web is built without VITE_TURNSTILE_SITE_KEY (a build-time Vite | |
| # var), matching release.yml. The Makefile `push` target injects a | |
| # per-env key from 1Password; if a build-time Turnstile key is required | |
| # in prod, this image won't carry it. See the deployments Makefile. | |
| - name: Build and push web image | |
| uses: docker/build-push-action@v6 | |
| with: | |
| context: web | |
| file: web/Dockerfile | |
| platforms: linux/amd64 | |
| push: true | |
| tags: ${{ steps.ecr.outputs.registry }}/nimblebrain/agent-web:${{ steps.tag.outputs.sha }} | |
| cache-from: type=gha,scope=web | |
| cache-to: type=gha,mode=max,scope=web |