Skip to content

CI

CI #1183

Workflow file for this run

name: CI
on:
workflow_dispatch:
schedule:
- cron: "17 3 * * 2"
push:
branches: ["master"]
pull_request:
branches: ["master"]
permissions: read-all
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
jobs:
backend-sqlite:
name: Backend (SQLite)
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
- name: Set up Bun
uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2
with:
bun-version: 1.4.2
- name: Install dependencies
run: bun install --frozen-lockfile
- name: Build frontend (security-regression assertions need dist, issue #290)
run: cd frontend && bun run build
- name: Build terraform-config-inspect (registry install e2e needs the same binary prod bakes in, issue #720)
run: CGO_ENABLED=0 GOBIN=$GITHUB_WORKSPACE/backend/bin go install github.com/hashicorp/terraform-config-inspect@v0.0.0-20260709150029-2fb54c236733
- name: Allow binding loopback 443 for the genuine provider-mirror install (best effort; the suite skips that leg otherwise)
run: sudo sysctl -w net.ipv4.ip_unprivileged_port_start=443 || true
- name: Run backend tests (SQLite)
env:
# Raises the tofu release-lookup ceiling from 60/hr (per shared
# runner IP) to 5000/hr when both matrix jobs run e2e downloads.
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
# Keep pull requests fast and deterministic; the scheduled run
# expands the independent lifecycle model's seed corpus.
TERRENCE_PROPERTY_CASES: ${{ github.event_name == 'schedule' && '2000' || '256' }}
# The backend package script serializes Bun workers because the test
# suite shares its Elysia app and SQLite connection across files.
run: cd backend && bun run test
- name: Prove critical guard tests detect mutations
run: bun backend/scripts/check-guard-mutations.ts
backend-postgres:
name: Backend (Postgres)
runs-on: ubuntu-latest
services:
postgres:
image: postgres:18-alpine@sha256:d3e1620b530c944afa6e887d22eb899824da68e19c52024bf98f5220c88a65b2
env:
POSTGRES_USER: terrence
POSTGRES_PASSWORD: terrence
POSTGRES_DB: terrence_test
ports:
- 5432:5432
options: >-
--health-cmd pg_isready
--health-interval 10s
--health-timeout 5s
--health-retries 5
steps:
- name: Checkout code
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
- name: Set up Bun
uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2
with:
bun-version: 1.4.2
- name: Install dependencies
run: bun install --frozen-lockfile
- name: Build frontend (security-regression assertions need dist, issue #290)
run: cd frontend && bun run build
- name: Build terraform-config-inspect (registry install e2e needs the same binary prod bakes in, issue #720)
run: CGO_ENABLED=0 GOBIN=$GITHUB_WORKSPACE/backend/bin go install github.com/hashicorp/terraform-config-inspect@v0.0.0-20260709150029-2fb54c236733
- name: Allow binding loopback 443 for the genuine provider-mirror install (best effort; the suite skips that leg otherwise)
run: sudo sysctl -w net.ipv4.ip_unprivileged_port_start=443 || true
- name: Run backend tests (Postgres)
env:
DATABASE_URL: postgres://terrence:terrence@localhost:5432/terrence_test
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
# The backend package script serializes Bun workers because the test
# suite shares its Elysia app and SQLite connection across files.
run: cd backend && bun run test
- name: Run SQLite to PostgreSQL migration and export contracts
env:
PG_TEST_ADMIN_URL: postgres://terrence:terrence@localhost:5432/terrence_test
# Keep DATABASE_URL absent: the application must start on SQLite so
# the actual migration path runs, not the already-PostgreSQL guard.
run: cd backend && bun test tests/api/db-migration.test.ts tests/api/db-export.test.ts --max-concurrency=1 --no-orphans
ha-mixed-version:
name: HA mixed-version smoke
if: github.event_name == 'pull_request'
runs-on: ubuntu-latest
services:
postgres:
image: postgres:18-alpine@sha256:d3e1620b530c944afa6e887d22eb899824da68e19c52024bf98f5220c88a65b2
env:
POSTGRES_USER: terrence
POSTGRES_PASSWORD: terrence
POSTGRES_DB: terrence_mixed
ports:
- 5432:5432
options: >-
--health-cmd pg_isready
--health-interval 10s
--health-timeout 5s
--health-retries 5
steps:
- name: Checkout code
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
fetch-depth: 0
- name: Set up Bun
uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2
with:
bun-version: 1.4.2
- name: Install dependencies
run: bun install --frozen-lockfile
- name: Run protocol-1/protocol-2 HA compatibility smoke
env:
DATABASE_URL: postgres://terrence:terrence@localhost:5432/terrence_mixed
run: bash scripts/ha-mixed-version-smoke.sh
frontend:
name: Frontend
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
- name: Set up Bun
uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2
with:
bun-version: 1.4.2
- name: Install dependencies
run: bun install --frozen-lockfile
- name: Run frontend tests
env:
NO_COLOR: "1"
run: cd frontend && bun run test
- name: Frontend build
run: cd frontend && bun run build
checks:
name: Checks
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
fetch-depth: 0
- name: Set up Bun
uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2
with:
bun-version: 1.4.2
- name: Verify Bun runtime version
run: bun run check:bun-version
- name: Install dependencies
run: bun install --frozen-lockfile
- name: Check formatting
run: bun run format:check
- name: Check lockfile deduplication
run: bun run deps:dedupe:check
- name: Dependency vulnerability audit (issue #285)
run: bun run deps:audit
- name: Check dependency release-age policy
run: |
bun - <<'NODE'
const fs = require("fs");
const renovate = JSON.parse(fs.readFileSync("renovate.json", "utf8"));
const lines = fs.readFileSync("bunfig.toml", "utf8").split(/\r?\n/);
const installStart = lines.findIndex((line) => line.trim() === "[install]");
const installEnd = lines.findIndex((line, index) => index > installStart && /^\s*\[/.test(line));
const installLines = lines.slice(installStart + 1, installEnd === -1 ? lines.length : installEnd);
const ageAssignments = installLines.filter((line) => /^\s*minimumReleaseAge\s*=/.test(line));
const activeAge = ageAssignments[0]?.match(/^\s*minimumReleaseAge\s*=\s*(\d+)\s*(?:#.*)?$/)?.[1];
if (renovate.minimumReleaseAge !== "3 days" || renovate.lockFileMaintenance?.enabled !== false || ageAssignments.length !== 1 || activeAge !== "259200") process.exit(1);
console.log("Dependency release-age policy is enforced; Renovate lock maintenance is disabled because Bun enforces the same guard.");
NODE
- name: Verify controlled supply-chain policy (issue #787)
run: bun run check:supply-chain
- name: Verify rolling-upgrade schema compatibility
run: bun run check:schema-compat
- name: Generate dependency manifest, SBOM, and change summary
env:
BASE_REF: ${{ github.event_name == 'pull_request' && github.event.pull_request.base.sha || github.event.before }}
run: |
set -euo pipefail
args=()
if [[ -n "${BASE_REF:-}" ]] && git cat-file -e "${BASE_REF}^{commit}" 2>/dev/null; then
args+=(--base-ref "$BASE_REF")
fi
bun scripts/supply-chain-manifest.ts \
--output dependency-manifest.json \
--sbom-output dependency-sbom.spdx.json \
--summary-output dependency-change-summary.md \
"${args[@]}"
- name: Upload dependency supply-chain evidence
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: dependency-supply-chain
path: |
dependency-manifest.json
dependency-sbom.spdx.json
dependency-change-summary.md
supply-chain/dependency-exceptions.json
retention-days: 90
- name: Check canonical brand assets
run: bun run --cwd frontend scripts/brand-assets.tsx --check
- name: Typecheck
run: bun run typecheck
- name: Lint (baseline report)
run: bun run lint
continue-on-error: true
- name: Lint debt budget
run: bun run lint:budget
- name: Knip
run: bun run knip
- name: Security endpoint coverage (todo 143)
run: bun test backend/tests/unit/endpoint-policy-compliance.test.ts
- name: Secret fingerprint check (todo 116)
run: bun test backend/tests/worker/env-isolation.test.ts
- name: Verify operational runbook in disposable storage (issue #757)
run: bun backend/scripts/verify-operations-docs.ts
- name: Run documented upgrade fixture (issue #754)
run: bun test backend/tests/db/upgrade-invariants.test.ts --max-concurrency=1 --no-orphans
# Coverage gate (issue #380): runs the backend suite under coverage and
# uploads the report. NOT yet enforcing (--fail) while legacy exposure
# exists; ratchet the threshold in backend/scripts/coverage-report.ts as
# exposure is closed out.
coverage:
name: Backend coverage report
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
- name: Set up Bun
uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2
with:
bun-version: 1.4.2
- name: Install dependencies
run: bun install --frozen-lockfile
- name: Build frontend (security-regression assertions need dist, issue #290)
run: cd frontend && bun run build
- name: Backend coverage report
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: cd backend && bun run coverage --json > /tmp/coverage.json
- name: Upload coverage report
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: backend-coverage
path: /tmp/coverage.json
retention-days: 30
# Supply-chain / artifact verification (todos 828, 889, 800).
build-verification:
name: Build verification
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
- name: Set up Bun
uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2
with:
bun-version: 1.4.2
- name: Install dependencies
run: bun install --frozen-lockfile
- name: Frontend build (reproducible)
run: cd frontend && bun run build
- name: Docker full build and runtime smoke check (todo 828)
run: |
docker build -f Dockerfile -t terrence:ci . > /tmp/docker-build.log 2>&1 || (cat /tmp/docker-build.log; exit 1)
docker run --rm terrence:ci /usr/bin/bun --version
# Verify non-root guarantee (issue #379)
user=$(docker inspect terrence:ci --format '{{.Config.User}}' 2>/dev/null || true); echo "image USER=$user"; case "$user" in nonroot*|65532*) ;; *) echo "FAIL: image USER must be nonroot/65532" >&2; exit 1;; esac
# Deep-verify with the tracked script: image USER, live process
# uid/gid, storage ownership and writability. The probe container is
# always removed afterwards, even when verification fails.
docker run -d --name terrence-ci-verify terrence:ci > /dev/null
cleanup_verify() { docker rm -f terrence-ci-verify > /dev/null 2>&1 || true; }
trap cleanup_verify EXIT
for attempt in $(seq 1 30); do
health=$(docker inspect terrence-ci-verify --format '{{.State.Health.Status}}' 2>/dev/null || true)
if [ "$health" = "healthy" ]; then break; fi
if [ "$attempt" -lt 30 ]; then sleep 5; fi
done
# Re-inspect after the polling loop: only a confirmed healthy
# container may be passed to the runtime-user verification script.
health=$(docker inspect terrence-ci-verify --format '{{.State.Health.Status}}' 2>/dev/null || true)
if [ "$health" != "healthy" ]; then
echo "FAIL: verification container is not healthy (status=${health:-unknown})" >&2
exit 1
fi
bash backend/scripts/verify-container-user.sh terrence-ci-verify
trap - EXIT
cleanup_verify
echo "Docker image build and runtime smoke check OK"
- name: Bundle size budget (todo 800)
run: bun run check:bundle-size
browser-tests:
name: Browser & E2E Tests (Bun.WebView)
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
- name: Set up Bun
uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2
with:
bun-version: 1.4.2
- name: Install dependencies
run: bun install --frozen-lockfile
- name: Build static frontend
run: cd frontend && bun run build
- name: Run browser E2E & accessibility suite (Bun.WebView)
run: bun run test:profile -- browser
provider-compatibility:
name: Provider and CLI (${{ matrix.engine }}, ${{ matrix.tier }})
strategy:
fail-fast: false
matrix:
engine: [terraform, tofu]
tier: ${{ github.event_name == 'schedule' && fromJSON('["canary"]') || fromJSON('["floor", "current"]') }}
env:
TERRENCE_E2E_CLI: ${{ matrix.engine }}
TERRENCE_E2E_TIER: ${{ matrix.tier }}
TERRENCE_E2E_SEED: eng21-${{ matrix.engine }}-${{ matrix.tier }}
TERRENCE_E2E_RESULTS_DIR: ${{ github.workspace }}/provider-lifecycle-results
runs-on: ubuntu-latest
permissions:
contents: read
steps:
- name: Checkout code
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
- name: Set up Bun
uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2
with:
bun-version: 1.4.2
- name: Set up Terraform for provider schema verification
uses: hashicorp/setup-terraform@dfe3c3f87815947d99a8997f908cb6525fc44e9e # v4.0.1
with:
terraform_version: v1.16.3
- name: Install dependencies
run: bun install --frozen-lockfile
- name: Verify the tracked hashicorp/tfe provider catalog
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
set -euo pipefail
provider_version="$(bun -e 'const fs=require("fs"); const s=JSON.parse(fs.readFileSync("backend/src/data/provider_surface.json", "utf8")); const m=/v([0-9]+[.][0-9]+[.][0-9]+)/.exec(s.provider); if (!m) process.exit(1); process.stdout.write(m[1]);')"
TFE_PROVIDER_VERSION="$provider_version" TERRAFORM_BIN=terraform bun backend/scripts/refresh-provider-surface.ts
git diff --exit-code -- backend/src/data/provider_surface.json backend/tests/e2e/provider_surface.json
- name: Build frontend for API contract fixtures
run: cd frontend && bun run build
- name: Run focused CLI and API compatibility contracts
run: |
set -euo pipefail
cd backend
bun test --bail=5 --max-concurrency=1 --no-orphans \
tests/api/e2e_full_flow.test.ts \
tests/api/oauth-login-flow.test.ts
cd ../frontend
bun test --bail=5 --max-concurrency=1 --no-orphans \
tests/terraform-cli-route-compat.test.tsx
- name: Cache verified CLI binaries and provider packages
uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6
with:
path: |
backend/storage/binaries
backend/storage/e2e-plugin-cache
key: ${{ runner.os }}-${{ runner.arch }}-cli-${{ matrix.engine }}-${{ matrix.tier }}-${{ hashFiles('backend/tests/e2e/cli_matrix.json', 'backend/tests/e2e/provider_surface.json') }}
- name: Run pinned provider and remote CLI journeys
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: bun run test:profile -- sqlite-cli
- name: Build the production Landlock helper
if: matrix.engine == 'terraform' && matrix.tier == 'current'
run: backend/bin/build-landlock-runner.sh
- name: Run provider E2E with required production sandboxing
if: matrix.engine == 'terraform' && matrix.tier == 'current'
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
TERRENCE_E2E_CLI: terraform
TERRENCE_E2E_SECURITY_PROFILE: required
run: bun run test:profile -- sandbox
- name: Upload successful provider lifecycle evidence
if: always()
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: provider-lifecycle-results-${{ matrix.engine }}-${{ matrix.tier }}
path: provider-lifecycle-results/*
if-no-files-found: ignore
retention-days: 30
provider-canary-review:
name: Open provider canary review item
needs: provider-compatibility
if: always() && github.event_name == 'schedule' && needs.provider-compatibility.result == 'failure'
runs-on: ubuntu-latest
permissions:
issues: write
steps:
# Deliberately no checkout: this write-capable job must not execute repository code.
- name: Open a review item for a failed canary
env:
GH_TOKEN: ${{ github.token }}
run: |
title="[CLI canary] provider compatibility failure"
existing="$(gh issue list --repo "$GITHUB_REPOSITORY" --state open --search "$title in:title" --json number --jq '.[0].number // empty')"
if [ -z "$existing" ]; then
cat > /tmp/canary-issue.md <<EOF
The scheduled provider/CLI canary matrix failed. Review the [CI run](${GITHUB_SERVER_URL}/${GITHUB_REPOSITORY}/actions/runs/${GITHUB_RUN_ID}) and its redacted CLI diagnostic artifacts to identify the failing engine.
The supported floor and current pins have not changed. Promote a new version only after the complete compatibility matrix passes.
EOF
gh issue create --repo "$GITHUB_REPOSITORY" --title "$title" --body-file /tmp/canary-issue.md
fi
cli-compatibility-report:
name: Publish tested CLI matrix artifact
needs: provider-compatibility
if: github.event_name != 'schedule'
runs-on: ubuntu-latest
permissions:
contents: read
steps:
- name: Checkout code
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
- name: Set up Bun
uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2
with:
bun-version: 1.4.2
- name: Download successful matrix evidence
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8
with:
pattern: provider-lifecycle-results-*
merge-multiple: true
path: provider-lifecycle-results
- name: Generate the tested compatibility page
run: bun backend/scripts/report-cli-compatibility.ts provider-lifecycle-results backend/docs/cli-compatibility-results.md
- name: Upload the tested compatibility page and evidence
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: cli-compatibility-report
path: |
backend/docs/cli-compatibility-results.md
provider-lifecycle-results/*-lifecycle.json
retention-days: 90
publish-nightly:
name: Publish nightly image
needs:
- backend-sqlite
- backend-postgres
- frontend
- checks
- coverage
- build-verification
- browser-tests
- provider-compatibility
- cli-compatibility-report
if: github.event_name == 'push' && github.ref == 'refs/heads/master'
runs-on: ubuntu-latest
permissions:
contents: read
actions: read
packages: write
steps:
- name: Checkout repository
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
fetch-depth: 2
- name: Set up Bun
uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2
with:
bun-version: 1.4.2
no-cache: true
- name: Determine build version
id: version
run: |
pkg_version=$(bun -e "console.log(String(require('./package.json').version ?? '0.0.0'))" 2>/dev/null || echo "0.0.0")
echo "version=${pkg_version}" >> "$GITHUB_OUTPUT"
echo "sha_short=$(git rev-parse --short HEAD)" >> "$GITHUB_OUTPUT"
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@f87e5991a6d7451dcb8d9637bfbc97413f497069 # v4.4.1
- name: Log in to GitHub Container Registry
uses: docker/login-action@dbcb813823bdd20940b903addbd779551569679f # v4.6.0
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Extract Docker metadata
id: meta
uses: docker/metadata-action@dc802804100637a589fabce1cb79ff13a1411302 # v6.2.0
with:
images: ghcr.io/${{ github.repository }}
tags: |
type=raw,value=nightly
type=sha,format=short
- name: Download tested CLI/provider evidence
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8
with:
name: cli-compatibility-report
path: release-evidence
- name: Build and push Docker image
id: build
uses: docker/build-push-action@c3c9e263c25d99ce0380d002d59b67737d91b0dc # v7.4.0
with:
context: .
file: ./Dockerfile
push: true
build-args: |
BUILD_VERSION=${{ steps.version.outputs.version }}
BUILD_SHA=${{ steps.version.outputs.sha_short }}
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
cache-from: type=gha
cache-to: type=gha,mode=max
provenance: mode=max
sbom: true
outputs: type=image,name=ghcr.io/${{ github.repository }},push=true
- name: Write redacted build manifest
run: |
set -euo pipefail
args=()
if git rev-parse HEAD^ >/dev/null 2>&1; then args+=(--base-ref HEAD^); fi
bun scripts/supply-chain-manifest.ts \
--output dependency-manifest.json \
--sbom-output dependency-sbom.spdx.json \
--summary-output dependency-change-summary.md \
"${args[@]}"
bun run check:supply-chain
bun backend/scripts/build-manifest.ts \
--output build-manifest.json \
--version "${{ steps.version.outputs.version }}" \
--commit "$(git rev-parse HEAD)" \
--image "ghcr.io/${{ github.repository }}:nightly" \
--digest "${{ steps.build.outputs.digest }}" \
--dependency-manifest dependency-manifest.json \
--dependency-sbom dependency-sbom.spdx.json \
--dependency-summary dependency-change-summary.md \
--dependency-exceptions supply-chain/dependency-exceptions.json \
--evidence-dir release-evidence
- name: Upload redacted build manifest
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: build-manifest-nightly
path: |
build-manifest.json
dependency-manifest.json
dependency-sbom.spdx.json
dependency-change-summary.md
supply-chain/dependency-exceptions.json
retention-days: 365