-
Notifications
You must be signed in to change notification settings - Fork 0
208 lines (188 loc) · 9.28 KB
/
Copy pathci.yml
File metadata and controls
208 lines (188 loc) · 9.28 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
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: pnpm/action-setup@v6
# setup-pnpm's default store nests inside its own bin dir, which breaks
# any prepare-building dep (portable-type TS2742) — pin it outside.
- run: pnpm config set store-dir ~/.pnpm-store --global
- uses: actions/setup-node@v7
with:
node-version: 22
cache: pnpm
- run: pnpm install --frozen-lockfile
# 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 `pnpm 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: pnpm 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}"