Skip to content

Muse spark

Muse spark #435

Workflow file for this run

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')"