fix(control): the second wording of "no zellij", and a remedy nothing rendered #1737
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: | |
| # 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}" |