Muse spark #435
Workflow file for this run
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] | |
| workflow_dispatch: | |
| inputs: | |
| runner: | |
| description: 'Runner to use (manual runs only)' | |
| type: choice | |
| options: | |
| - arc-runner | |
| - ubuntu-latest | |
| default: arc-runner | |
| concurrency: | |
| group: ci-${{ github.workflow }}-${{ github.ref }} | |
| # Cancel superseded runs on PR force-pushes; let main/push runs complete | |
| # so release artifacts and coverage uploads are never truncated. | |
| cancel-in-progress: ${{ github.event_name == 'pull_request' }} | |
| permissions: | |
| contents: read | |
| jobs: | |
| generated: | |
| name: Generated artifacts are current | |
| # CLEAN-05: proves every committed generated file is reproducible from its | |
| # source AND that verifying it does not touch the worktree. Before this | |
| # gate, `npm run prebuild` re-ran the changelog generator in WRITE mode, so | |
| # a build could silently rewrite a committed source file. | |
| # | |
| # Needs both toolchains: the registry spans Node generators (changelog, | |
| # route registry, i18n split) and Go generators (route templates, the AI | |
| # feature mirror, the SLO rules/dashboards). | |
| runs-on: ubuntu-latest | |
| steps: | |
| - uses: actions/checkout@v4 | |
| - uses: actions/setup-node@v4 | |
| with: | |
| node-version: "20" | |
| cache: npm | |
| cache-dependency-path: web/package-lock.json | |
| - uses: actions/setup-go@v5 | |
| with: | |
| go-version-file: go.mod | |
| cache-dependency-path: go.sum | |
| - run: npm ci --legacy-peer-deps | |
| working-directory: web | |
| - name: Registry coverage + freshness + non-mutation | |
| run: node scripts/check-generated-freshness.mjs | |
| - name: Worktree is byte-identical after the gate | |
| run: | | |
| if [ -n "$(git status --porcelain -uall)" ]; then | |
| echo "::error::the freshness gate mutated the worktree" | |
| git --no-pager status --porcelain -uall | |
| git --no-pager diff | |
| exit 1 | |
| fi | |
| echo "worktree clean" | |
| backend: | |
| name: Backend (lint + test + build) | |
| # Pinned to ubuntu-latest (not the arc-runner default) because `go test | |
| # -race` requires CGO, which requires a C compiler at runtime, and the | |
| # self-hosted arc-runner image does not currently ship one (`cgo: C | |
| # compiler "gcc" not found`). Frontend + Docker jobs can still ride | |
| # arc-runner β they do not need CGO. Re-pin to arc-runner once its | |
| # image includes build-essential / gcc. | |
| runs-on: ubuntu-latest | |
| services: | |
| postgres: | |
| # Use the TimescaleDB image because migration 000142_baseline_typed.up.sql | |
| # does `CREATE EXTENSION IF NOT EXISTS timescaledb;`. The plain | |
| # postgres:17-alpine image does not ship that extension. The image is | |
| # postgres-spec compliant (same ports, env vars, pg_isready), so the | |
| # only knock-on change is bumping --health-retries because the larger | |
| # image takes longer to warm up on a fresh runner. | |
| image: timescale/timescaledb-ha:pg17 | |
| env: | |
| POSTGRES_USER: test | |
| POSTGRES_PASSWORD: test | |
| POSTGRES_DB: teslasync_test | |
| ports: | |
| - 5432:5432 | |
| options: >- | |
| --health-cmd pg_isready | |
| --health-interval 10s | |
| --health-timeout 5s | |
| --health-retries 10 | |
| steps: | |
| - uses: actions/checkout@v4 | |
| - uses: actions/setup-go@v5 | |
| with: | |
| go-version: "1.25" | |
| - name: Lint | |
| run: | | |
| go install github.com/golangci/golangci-lint/cmd/golangci-lint@v1.64.8 | |
| golangci-lint run ./... | |
| - name: Test | |
| env: | |
| # ubuntu-latest's setup-go@v5 enables CGO by default, but be | |
| # explicit so the intent is documented and the step stays | |
| # bulletproof against future runner / setup-go changes: | |
| # `-race` strictly requires CGO. | |
| CGO_ENABLED: 1 | |
| DATABASE_HOST: localhost | |
| DATABASE_PORT: 5432 | |
| DATABASE_USER: test | |
| DATABASE_PASS: test | |
| DATABASE_NAME: teslasync_test | |
| run: go test -race -coverprofile=coverage.out -covermode=atomic ./... | |
| - name: Generate backend coverage reports | |
| if: always() && hashFiles('coverage.out') != '' | |
| run: | | |
| go tool cover -func=coverage.out > coverage.txt | |
| go tool cover -html=coverage.out -o coverage.html | |
| # Per-package roll-up (statement coverage averaged across the package's funcs) | |
| awk ' | |
| /^total:/ { next } | |
| { | |
| n = split($1, parts, "/") | |
| file = parts[n]; sub(/:.*$/, "", file) | |
| pkg = parts[1] | |
| for (i = 2; i < n; i++) pkg = pkg "/" parts[i] | |
| gsub("%", "", $3) | |
| sum[pkg] += $3 | |
| cnt[pkg]++ | |
| } | |
| END { | |
| for (p in sum) printf "%6.2f%% %s\n", sum[p]/cnt[p], p | |
| } | |
| ' coverage.txt | sort -nr > coverage-by-package.txt | |
| TOTAL_LINE=$(grep '^total:' coverage.txt || echo "total: (statements) 0.0%") | |
| { | |
| echo "## π Backend Coverage" | |
| echo "" | |
| echo "**$TOTAL_LINE**" | |
| echo "" | |
| echo "Full HTML + per-function reports are attached to this run as the **backend-coverage** artifact." | |
| echo "" | |
| echo "<details><summary>Per-package coverage (top 50)</summary>" | |
| echo "" | |
| echo '```' | |
| head -n 50 coverage-by-package.txt | |
| echo '```' | |
| echo "</details>" | |
| } >> "$GITHUB_STEP_SUMMARY" | |
| - name: Architecture test (no forbidden import edges) | |
| run: | | |
| go test -v ./internal/arch/... | |
| - name: Architecture metrics regression check | |
| run: | | |
| go run ./tools/archmetrics -compare tools/archmetrics/baseline.json | |
| - name: Integration test (telemetry replay) | |
| # KNOWN-FAILING (phase-50 in flight): TestTelemetryReplay asserts a | |
| # post-typed-migration invariant (signal_catalog/signal_observations | |
| # tables exist, 0 surviving raw_json jsonb columns, every writer fires | |
| # from the ingest path). Several phase-50 migrations / writer wirings | |
| # are still being authored by the Phase-50 AI Adoption prompt chain | |
| # ([47/67] at time of writing), so this test fails today on every | |
| # branch (including main β it has only been cascade-skipped because | |
| # the upstream Lint step has been red). | |
| # | |
| # We still want the test to RUN in CI for visibility (so the day it | |
| # turns green we notice), but we do not want it to gate merges while | |
| # the underlying refactor is mid-flight. Remove `continue-on-error` | |
| # once phase-50 lands. | |
| continue-on-error: true | |
| env: | |
| # ubuntu-latest's setup-go@v5 enables CGO by default; explicit | |
| # for the same reasons as the Test step above (`-race` needs CGO). | |
| CGO_ENABLED: 1 | |
| DATABASE_HOST: localhost | |
| DATABASE_PORT: 5432 | |
| DATABASE_USER: test | |
| DATABASE_PASS: test | |
| DATABASE_NAME: teslasync_test | |
| run: go test -tags integration -race -count=1 ./internal/api/... -run TestTelemetryReplay -v | |
| - name: Migration rollback test | |
| env: | |
| DATABASE_URL: postgres://test:test@localhost:5432/teslasync_test?sslmode=disable | |
| run: | | |
| go install -tags 'postgres' github.com/golang-migrate/migrate/v4/cmd/migrate@latest | |
| migrate -path migrations -database "$DATABASE_URL" up | |
| migrate -path migrations -database "$DATABASE_URL" version | |
| migrate -path migrations -database "$DATABASE_URL" down 1 | |
| migrate -path migrations -database "$DATABASE_URL" up | |
| - name: Build | |
| run: | | |
| CGO_ENABLED=0 go build -ldflags="-s -w" -o teslasync ./cmd/teslasync | |
| CGO_ENABLED=0 go build -ldflags="-s -w" -o notification-worker ./cmd/notification-worker | |
| CGO_ENABLED=0 go build -ldflags="-s -w" -o export-worker ./cmd/export-worker | |
| - name: Upload coverage | |
| if: always() | |
| uses: actions/upload-artifact@v4 | |
| with: | |
| name: backend-coverage | |
| path: | | |
| coverage.out | |
| coverage.html | |
| coverage.txt | |
| coverage-by-package.txt | |
| frontend: | |
| name: Frontend (lint + test + build) | |
| # Pinned to ubuntu-latest to match the Backend job: the arc-runner is | |
| # too resource-starved for vitest's default 5s testTimeout (45+ tests | |
| # routinely time out under load even though the same suite finishes | |
| # in ~80s locally and on ubuntu-latest). Re-pin to arc-runner once | |
| # the runner image has the bandwidth budget for jsdom + chart-heavy | |
| # component trees. | |
| runs-on: ubuntu-latest | |
| defaults: | |
| run: | |
| working-directory: web | |
| steps: | |
| - uses: actions/checkout@v4 | |
| - uses: actions/setup-node@v4 | |
| with: | |
| node-version: "20" | |
| cache: npm | |
| cache-dependency-path: web/package-lock.json | |
| - run: npm ci --legacy-peer-deps | |
| - name: Lint | |
| run: npm run lint | |
| - name: Transform pipeline uses no deprecated Vite options | |
| # CLEAN-02: Vitest 4 resolves its own nested Vite 8 (rolldown/OXC), | |
| # where the esbuild-era options @vitejs/plugin-react@4 sets are | |
| # deprecated. Static config assertions cannot see a *dependency* start | |
| # doing this after an upgrade; this boots the real runtime. | |
| run: npm run check:vite-deprecations | |
| - name: Test | |
| run: npx vitest run --coverage --reporter=verbose | |
| - name: Generate frontend coverage report | |
| if: always() && hashFiles('web/coverage/coverage-summary.json') != '' | |
| run: | | |
| node -e ' | |
| const fs = require("fs"); | |
| const path = "coverage/coverage-summary.json"; | |
| if (!fs.existsSync(path)) { process.exit(0); } | |
| const t = JSON.parse(fs.readFileSync(path, "utf8")).total; | |
| const out = []; | |
| out.push("## π Frontend Coverage"); | |
| out.push(""); | |
| out.push("| Metric | Covered | Total | % |"); | |
| out.push("|--------|--------:|------:|--:|"); | |
| for (const k of ["statements","branches","functions","lines"]) { | |
| const m = t[k]; | |
| out.push(`| ${k} | ${m.covered} | ${m.total} | ${m.pct}% |`); | |
| } | |
| out.push(""); | |
| out.push("Browsable HTML report attached as the **frontend-coverage** artifact (open `coverage/index.html`)."); | |
| fs.appendFileSync(process.env.GITHUB_STEP_SUMMARY, out.join("\n") + "\n"); | |
| ' | |
| - name: Upload coverage | |
| if: always() | |
| uses: actions/upload-artifact@v4 | |
| with: | |
| name: frontend-coverage | |
| path: web/coverage/ | |
| - name: Build | |
| run: npm run build | |
| - name: No source maps in the publishable build | |
| # CLEAN-04: `npm run build` sets no VITE_SOURCEMAP_MODE, so | |
| # `build.sourcemap` is false and dist/ must contain zero `.map` files | |
| # and zero sourceMappingURL references. postbuild already runs this; | |
| # repeated here as an explicit, greppable CI assertion. | |
| run: npm run check:source-maps | |
| docker: | |
| name: Docker Β· ${{ matrix.image }} | |
| # Pin to ubuntu-latest because self-hosted arc-runner pods have been | |
| # observed to stall the export-worker build indefinitely (>35 min vs | |
| # ~9 min for the sibling api/notification-worker builds on the same | |
| # run). GitHub-hosted ubuntu-latest finishes all three images | |
| # consistently. Override via the workflow_dispatch `runner` input. | |
| runs-on: ${{ inputs.runner || 'ubuntu-latest' }} | |
| needs: [backend, frontend] | |
| strategy: | |
| fail-fast: false | |
| matrix: | |
| include: | |
| - image: api | |
| file: Dockerfile | |
| - image: notification-worker | |
| file: Dockerfile.notification | |
| - image: export-worker | |
| file: Dockerfile.export-worker | |
| - image: fleet-telemetry | |
| file: Dockerfile.fleet-telemetry | |
| - image: web | |
| file: Dockerfile.web | |
| steps: | |
| - uses: actions/checkout@v4 | |
| - uses: docker/setup-buildx-action@v3 | |
| - name: Build ${{ matrix.image }} | |
| uses: docker/build-push-action@53b7df96c91f9c12dcc8a07bcb9ccacbed38856a # v7.3.0 | |
| with: | |
| context: . | |
| file: ${{ matrix.file }} | |
| push: false | |
| load: ${{ matrix.image == 'web' }} | |
| tags: teslasync-${{ matrix.image }}:ci | |
| cache-from: type=gha,scope=ci-${{ matrix.image }} | |
| cache-to: type=gha,mode=max,scope=ci-${{ matrix.image }} | |
| - name: Web image ships no source maps | |
| # CLEAN-04: the runtime stage copies dist/ straight into the nginx | |
| # document root, so this inspects the actual IMAGE, not just the build | |
| # directory. `vite.config.ts` (sourcemap false) and the Dockerfile | |
| # prune step are two layers; this is the assertion that they held. | |
| if: matrix.image == 'web' | |
| run: | | |
| set -euo pipefail | |
| maps=$(docker run --rm --entrypoint sh teslasync-web:ci -c \ | |
| "find /usr/share/nginx/html -name '*.map' -print | head -50") | |
| if [ -n "$maps" ]; then | |
| echo "::error::web image ships source maps in the nginx document root" | |
| echo "$maps" | |
| exit 1 | |
| fi | |
| # BusyBox grep has no --include, so filter with find. | |
| refs=$(docker run --rm --entrypoint sh teslasync-web:ci -c \ | |
| "find /usr/share/nginx/html \( -name '*.js' -o -name '*.css' \) -exec grep -lE '[#@][[:space:]]*sourceMappingURL[[:space:]]*=' {} + 2>/dev/null | head -50" || true) | |
| if [ -n "$refs" ]; then | |
| echo "::error::web image assets reference a source map" | |
| echo "$refs" | |
| exit 1 | |
| fi | |
| echo "web image: 0 source maps, 0 sourceMappingURL references" | |
| - name: Web image build identity is unversioned-and-harmless | |
| # CI builds are NOT releases: no VITE_APP_VERSION / VITE_GIT_SHA build | |
| # args are passed. The resulting SPA must therefore report an | |
| # UNPARSEABLE version. If it ever reports a bare `X.Y.Z` again, the | |
| # PWA handshake would read it as older than a newer API and pin a | |
| # non-dismissible "update required" prompt that reloading can never | |
| # clear β the exact defect release.yml's build args now prevent for | |
| # real releases, and this asserts for every other build. | |
| if: matrix.image == 'web' | |
| run: | | |
| set -euo pipefail | |
| PKG_VERSION=$(node -p "require('./web/package.json').version") | |
| SW=$(docker run --rm --entrypoint cat teslasync-web:ci /usr/share/nginx/html/sw.js) | |
| if ! printf '%s' "$SW" | grep -qF "dev-${PKG_VERSION}"; then | |
| echo "::error::unversioned web build must report dev-${PKG_VERSION}; a bare parseable version goes permanently stale against the API" | |
| exit 1 | |
| fi | |
| echo "unversioned web image reports dev-${PKG_VERSION} (unparseable β handshake verdict 'unknown')" |