-
Notifications
You must be signed in to change notification settings - Fork 0
127 lines (114 loc) · 5.92 KB
/
Copy pathdeploy.yml
File metadata and controls
127 lines (114 loc) · 5.92 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
# CI-gated production deploy (DevOps remediation #2).
#
# Today the Hetzner deploy fires from the LOCAL pre-push hook, so a red CI still
# ships and `--no-verify` skips every gate. This workflow makes CI the gate:
# it runs ONLY after the CI workflow finishes successfully on main, then builds
# and ships. A failing CI → no deploy.
#
# ┌─ BEFORE YOU ENABLE THIS (it is inert until on `main` + secrets exist) ─────┐
# │ 1. Add repo secret HETZNER_SSH_KEY = a private key whose public half is in │
# │ the box's root authorized_keys (a deploy-only key, ideally). │
# │ 2. (optional) repo variable HETZNER_IP (defaults to 167.233.22.31). │
# │ 3. DISABLE the local hook or you'll deploy twice: comment out the │
# │ `>>> fleetcrown push-deploy >>>` block in .husky/pre-push. │
# │ 4. Merge this file to main. First run: watch the "Build" step — it builds │
# │ against a schema-only Postgres (no seed); if a page pre-renders off real │
# │ data and fails, add a minimal seed step here. │
# └────────────────────────────────────────────────────────────────────────────┘
name: Deploy
on:
workflow_run:
workflows: ["CI"] # the job in ci.yml
types: [completed]
branches:
[main] # a PR's CI is not a ship signal — filter it out
# here rather than creating a run that only skips
# Ships whatever is on main right now. Required, not a convenience: a CI run
# that GITHUB_TOKEN started (the auto-merge re-arm) emits NO workflow_run
# event, so the chain above never fires for an auto-merged PR — observed
# 2026-08-05 with three merges live on main and none of them deployed. The
# sweep dispatches this directly instead — it compares main's tip against the
# last successful run here and closes the gap. The sweep now lives in
# bitbaum/dotfiles (scripts/ci/auto-merge-sweep.sh); this repo enables the
# reconciler by passing `deploy_workflow: deploy.yml` in auto-merge.yml.
# Removing that input makes merges land and never ship.
workflow_dispatch: {}
# One deploy at a time; never cancel a ship mid-flight.
concurrency:
group: deploy-prod
cancel-in-progress: false
jobs:
deploy:
# Chained: only a GREEN CI run that was on main. Dispatched: the caller is
# responsible for the gate (the sweep checks main's CI before dispatching).
# Kill switch: dormant until you set repo variable DEPLOY_VIA_CI=true. This
# lets the workflow live on main (green PR, reviewed) WITHOUT deploying until
# you've added HETZNER_SSH_KEY and disabled the local push-deploy hook. Unset
# variable → '' → never runs.
if: >
vars.DEPLOY_VIA_CI == 'true' &&
(github.event_name == 'workflow_dispatch' ||
(github.event.workflow_run.conclusion == 'success' &&
github.event.workflow_run.head_branch == 'main'))
runs-on: ubuntu-latest
# Build does DB queries during static pre-render, so give it a schema-only
# Postgres (drizzle-kit push below). No real data — see the seed caveat above.
services:
postgres:
image: postgres:17
env:
POSTGRES_PASSWORD: ci
POSTGRES_DB: fleetcrown
ports: ["5432:5432"]
options: >-
--health-cmd pg_isready --health-interval 5s
--health-timeout 5s --health-retries 10
env:
DATABASE_URL: postgres://postgres:ci@localhost:5432/fleetcrown
HETZNER_IP: ${{ vars.HETZNER_IP || '167.233.22.31' }}
steps:
- uses: actions/checkout@v7
with:
# Chained: the exact CI-green commit. Dispatched: the tip of the ref
# it was dispatched on. Never an empty string — that silently checks
# out the default branch and would ship something nobody verified.
ref: ${{ github.event.workflow_run.head_sha || github.sha }}
- uses: actions/setup-node@v7
with:
node-version: 22
cache: npm
# Root deps AND the bridge subpackage — bridge/ is its own npm package with
# its own lockfile (pg, @types/pg). The build step below runs `tsc` in it,
# so without this the bridge build fails with "Cannot find module 'pg'".
- run: npm ci
- run: npm --prefix bridge ci
- name: Push schema to CI Postgres (build needs it)
run: npx drizzle-kit push
# Next's incremental compiler cache. Every deploy compiled the whole app
# from cold because the runner is ephemeral — the one part of the pipeline
# that is genuinely WORK rather than waiting, so it is worth making
# smaller. Keyed on the lockfile plus the sources, with a lockfile-only
# fallback so a normal source change still starts from a warm cache.
# `.next/cache` is compiler state, not output: what ships is
# `.next/standalone`, rebuilt every run either way.
- name: Cache Next.js build cache
uses: actions/cache@v6
with:
path: .next/cache
key: ${{ runner.os }}-next-${{ hashFiles('package-lock.json') }}-${{ hashFiles('src/**/*.ts', 'src/**/*.tsx') }}
restore-keys: |
${{ runner.os }}-next-${{ hashFiles('package-lock.json') }}-
${{ runner.os }}-next-
- name: Build (app + bridge)
run: |
npm run build
npm --prefix bridge run build
- name: SSH setup
run: |
mkdir -p ~/.ssh
printf '%s\n' "${{ secrets.HETZNER_SSH_KEY }}" > ~/.ssh/id_deploy
chmod 600 ~/.ssh/id_deploy
ssh-keyscan -H "$HETZNER_IP" >> ~/.ssh/known_hosts 2>/dev/null
printf 'Host %s\n IdentityFile ~/.ssh/id_deploy\n IdentitiesOnly yes\n' "$HETZNER_IP" >> ~/.ssh/config
- name: Ship to box (reuses deploy-hetzner.sh — flock, rollback, verify)
run: bash scripts/deploy-hetzner.sh --no-build