Skip to content

fix(control): the second wording of "no zellij", and a remedy nothing rendered #1737

fix(control): the second wording of "no zellij", and a remedy nothing rendered

fix(control): the second wording of "no zellij", and a remedy nothing rendered #1737

Workflow file for this run

name: CI
on:
push:
branches: [main]
pull_request:
# Load-bearing for CD: a merge made by auto-merge.yml uses the default
# GITHUB_TOKEN, and a push made with that token does NOT trigger workflows.
# Deploy chains off CI via workflow_run, so without a way to dispatch CI on
# main by hand, every automated merge would land and silently never ship.
workflow_dispatch: {}
# Cancel in-progress runs of the same workflow on the same branch — saves
# minutes when commits land in quick succession.
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
jobs:
check:
runs-on: ubuntu-latest
# A migrated Postgres, so tests that need one stop being excluded forever.
# Six suites were on the permanent skip list for lack of a database, and
# the entitlement e2e — signature verification, actor lookup, grant write,
# dedupe, expiry sweep — is the BILLING boundary. It had never run in CI.
#
# pgvector, not plain postgres: src/db/schema/knowledge-embeddings.ts
# declares a `vector` column, and drizzle-kit push dies with
# `type "vector" does not exist` on a stock image.
services:
postgres:
image: pgvector/pgvector:pg16
env:
POSTGRES_PASSWORD: postgres
ports: ["5432:5432"]
options: >-
--health-cmd pg_isready
--health-interval 5s
--health-timeout 5s
--health-retries 10
env:
DATABASE_URL: postgresql://postgres:postgres@127.0.0.1:5432/postgres
steps:
# fetch-depth: 0, because one check compares against a release TAG.
# The default shallow checkout fetches no tags at all, and a shallow one
# with `fetch-tags` still lacks the tagged commit's trees — so
# `git diff fleet-runner-v* HEAD` could not run, and the desktop-release
# drift gate would have passed vacuously in the one place it most needs
# to run. It reports that absence as a failure rather than as clean, so
# this line is what keeps it able to go green honestly.
- uses: actions/checkout@v7
with:
fetch-depth: 0
- uses: actions/setup-node@v7
with:
node-version: 22
cache: npm
- run: npm ci
# check:design scans with ripgrep, which is NOT preinstalled on the
# runner. Without this the script exited 127 on every pattern, read that
# as "no matches", and reported ok while scanning nothing — the gate was
# decorative for its entire life. The script now hard-fails when rg is
# absent, so this step is what keeps the gate able to run at all.
- name: Install ripgrep (required by check:design)
run: sudo apt-get update -qq && sudo apt-get install -y -qq ripgrep
# ONE canonical bundle — the exact `npm run verify` you run locally
# (tsc + lint + check:design + check:desktop + test:unit + test:home +
# test:ops). CI calls it VERBATIM, so a green local verify can't diverge
# from a green CI. The unit suite is glob-discovered
# (scripts/test-unit.ts), so a new scripts/test/*.ts is picked up for
# free — no more born-dead tests.
#
# check:desktop is deliberately IN the bundle rather than a parallel job:
# a separate job would be faster in wall-clock but would break "green
# local verify => green CI", and desktop/ is precisely the area where
# nobody was running anything locally. See scripts/check-desktop.sh.
# `drizzle-kit push`, NOT the drizzle/*.sql files. Those do not replay
# from empty — 0001_canonical_project_identity.sql references
# `user_projects`, which 0000 never creates, and 51 of 56 migrations fail
# against a fresh database. The journal is history, not a rebuild recipe.
#
# push is safe to run non-interactively HERE specifically because the
# database is empty: its truncate prompts only fire for tables holding
# rows, so there is nothing for it to ask about. Never point this at a
# database with data in it.
- name: Migrate the CI database
run: |
psql "$DATABASE_URL" -c 'CREATE EXTENSION IF NOT EXISTS vector;'
npx drizzle-kit push --force
- name: Verify
run: npm run verify
# `next build` is intentionally NOT here — the App-Router pages do
# DB queries during static pre-render, so a real Postgres would
# need to be set up + migrated + seeded before the build can run.
# The Hetzner deploy step (scripts/deploy-hetzner.sh) builds against
# the live box DB, so this CI focuses on type/lint and leaves build
# verification to that deploy.
# ── Close the CD gap where it opens, instead of polling for it ──────────────
#
# Deploy chains off CI via workflow_run. That event never fires for a CI run
# that GITHUB_TOKEN started — which is exactly how every auto-merged PR gets
# its main-CI (auto-merge.yml dispatches it, because the merge push itself
# triggers nothing either). So the chain is broken for the ONLY path that
# actually merges code here.
#
# The sweep covered that by reconciling on its next tick: correct, self-
# healing, and up to ten minutes late. Measured on #284: 18.6 minutes of the
# 26.8-minute merge→live was this wait, with nothing running.
#
# A dispatch is not a push, so it is not subject to the no-cascade rule: this
# job can start Deploy directly the moment CI proves main is green. The sweep
# keeps its reconciler as the safety net for a deploy that never fired or
# failed — a poll is the right shape for "did something get missed?", and the
# wrong shape for "did something just happen?".
#
# Deliberately only for workflow_dispatch: a real push to main still chains
# through workflow_run on its own, and dispatching here too would ship twice.
ship:
needs: check
if: github.ref == 'refs/heads/main' && github.event_name == 'workflow_dispatch'
runs-on: ubuntu-latest
permissions:
actions: write
steps:
- uses: actions/checkout@v7
- name: Dispatch Deploy now that main is green
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
echo "main is green — dispatching Deploy without waiting for a sweep tick"
gh workflow run deploy.yml --ref main
# ---------------------------------------------------------------------------
# Mint the Fleet Runner release tag once main is green, so desktop code ships
# itself the way server code already does.
#
# The gap this closes: desktop/ only reaches a machine when a
# `fleet-runner-v*` tag exists, and until now a human had to mint it. Nobody
# did. Six merged commits sat unreleased after v0.8.12 — a power-source
# router, a usage double-billing fix, capture-hook changes — all live on the
# server, none on any machine, with nothing anywhere reporting a problem.
#
# The split of responsibility is deliberate:
# the BUMP is human — it says "this change is meant to reach machines",
# and scripts/test/desktop-release-drift.ts fails the
# build when desktop/ changed without one.
# the TAG is automatic — a purely mechanical step, and therefore exactly the
# kind of step a person forgets.
#
# Idempotent by construction: it exits when the tag already exists, so it is a
# no-op on every push between version bumps.
#
# WHY IT DISPATCHES INSTEAD OF RELYING ON THE TAG PUSH: a push made with the
# default GITHUB_TOKEN triggers no workflows, and desktop-release.yml fires on
# `push: tags`. Pushing the tag from here would therefore create the tag and
# build nothing — the same silent no-cascade trap that once left merges
# undeployed. So the tag push is treated as bookkeeping and the release is
# started explicitly, with `--ref` on the tag so the `mirror` job's
# `startsWith(github.ref, 'refs/tags/fleet-runner-v')` guard still holds.
# ---------------------------------------------------------------------------
release-desktop:
needs: check
if: github.ref == 'refs/heads/main'
runs-on: ubuntu-latest
permissions:
contents: write
actions: write
steps:
- uses: actions/checkout@v7
with:
fetch-depth: 0
- name: Tag and release Fleet Runner if the version moved
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
set -euo pipefail
version="$(node -p "require('./desktop/package.json').version")"
tag="fleet-runner-v${version}"
if git rev-parse -q --verify "refs/tags/${tag}" >/dev/null; then
echo "${tag} already exists — nothing to release."
exit 0
fi
echo "desktop/package.json is at ${version} with no ${tag} — releasing."
git tag "$tag"
git push origin "$tag"
# The tag now exists but nothing is watching it (see the header).
gh workflow run desktop-release.yml --ref "$tag"
echo "dispatched desktop-release.yml on ${tag}"