Repository navigation
193 lines (182 loc) · 8.73 KB
/
Copy pathdeploy.yml
File metadata and controls
193 lines (182 loc) · 8.73 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
name: deploy
# Push to master → self-hosted dashboard runner rebuilds the image and
# restarts the stack. `workflow_dispatch` lets you re-deploy without a
# code change (e.g. to pick up an .env edit).
on:
push:
branches: [master]
# Don't redeploy prod for changes that can't affect the app image:
# CI workflow edits (incl. the dev deploy/ingest workflows) and docs.
# A commit that also touches app code (packages/**, Dockerfile, lockfile,
# …) still triggers — paths-ignore only skips when ALL changed files match.
# To force a redeploy after a workflow edit, use the workflow_dispatch below.
paths-ignore:
- '.github/**'
- 'docs/**'
- '**/*.md'
workflow_dispatch:
# Only one deploy at a time — newer pushes supersede in-progress runs so
# we don't queue up redundant rebuilds.
concurrency:
group: deploy
cancel-in-progress: true
jobs:
deploy:
runs-on: [self-hosted, dashboard, inferencex-app-deploy]
env:
COMPOSE_FILE: /opt/docker-compose/docker-compose.yml
IMAGE_TAG: inferencex-app:latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Start postgres + wait for healthy
# Runs before the image build: some pages' generateStaticParams
# (e.g. compare-precision/compare-spec-decode OG images) query
# Postgres during `next build`, so the build needs a real,
# reachable database — not just the runtime container.
run: |
docker compose -f "$COMPOSE_FILE" up -d postgres
# Compose names containers `<project>-<service>-N`; project is
# `inferencex` (set via `name:` in compose).
for i in {1..30}; do
status=$(docker inspect inferencex-postgres-1 --format '{{.State.Health.Status}}' 2>/dev/null || echo unknown)
echo "postgres health: $status"
[ "$status" = "healthy" ] && exit 0
sleep 2
done
echo "postgres failed to become healthy" >&2
docker compose -f "$COMPOSE_FILE" logs postgres | tail -50
exit 1
- name: Run DB migrations (pre-build)
# Some pages' generateStaticParams query columns/tables that only
# exist after pending migrations run, so the schema has to be
# caught up *before* the image build. There's no post-build migrate
# step anymore — deploy.yml doesn't own migrations upstream doesn't
# have either; the ingest workflows (ingest-results.yml,
# ingest-agentic-results.yml), byte-identical to upstream, already
# run `pnpm admin:db:migrate --yes` before every ingest. This step
# uses a throwaway bun container instead of the app image (which
# doesn't exist yet pre-build): mounts the already checked-out
# source and calls the exact same real migrate script
# (packages/db/src/migrate.ts, via the same `db:migrate` bun
# script `admin:db:migrate` runs) — not a reimplementation.
# Image tag matches package.json's `packageManager` (bun@1.3.14).
# --network host + 127.0.0.1:5432: see Build image's comment below.
run: |
set -a
# shellcheck disable=SC1091
source "$(dirname "$COMPOSE_FILE")/.env"
set +a
db_url="postgresql://${POSTGRES_USER}:${POSTGRES_PASSWORD}@127.0.0.1:5432/${POSTGRES_DB}"
echo "::add-mask::$db_url"
echo "::add-mask::$POSTGRES_PASSWORD"
docker run --rm --network host \
-v "$GITHUB_WORKSPACE:/app" -w /app \
-e CI=true \
-e DATABASE_READONLY_URL="$db_url" \
-e DATABASE_WRITE_URL="$db_url" \
-e DATABASE_DRIVER=postgres \
-e DATABASE_SSL=false \
oven/bun:1.3.14-slim \
sh -c 'bun install --frozen-lockfile && bun run admin:db:migrate --yes --no-ssl'
- name: Fix workspace ownership
# The migration container above runs as root and bind-mounts
# $GITHUB_WORKSPACE, so `bun install` leaves root-owned files
# (node_modules/) in the checked-out tree. The next run's
# actions/checkout (running as the unprivileged runner user) can't
# remove them and fails outright. Reclaim ownership every time,
# regardless of whether the step above succeeded.
if: always()
run: |
docker run --rm -v "$GITHUB_WORKSPACE:/app" oven/bun:1.3.14-slim \
chown -R "$(id -u):$(id -g)" /app
- name: Build image
# Tag both :latest (consumed by compose) and :<sha> so we can
# roll back by `docker tag inferencex-app:<sha> inferencex-app:latest`
# then `docker compose up -d` without re-pulling the source.
#
# --network host + 127.0.0.1:5432: postgres is published to
# loopback only (see docker-compose.yml) specifically so the build
# can reach it — some pages' generateStaticParams (e.g. the
# compare-precision/compare-spec-decode OG images) query Postgres
# during `next build`, and a dummy/unreachable DB host fails the
# build outright.
# --secret passes the DB URL as a mounted file (see Dockerfile),
# not as --build-arg, so it never lands in image layers/history.
run: |
set -a
# shellcheck disable=SC1091
source "$(dirname "$COMPOSE_FILE")/.env"
set +a
db_url="postgresql://${POSTGRES_USER}:${POSTGRES_PASSWORD}@127.0.0.1:5432/${POSTGRES_DB}"
echo "::add-mask::$db_url"
echo "::add-mask::$POSTGRES_PASSWORD"
export DATABASE_READONLY_URL="$db_url"
export DATABASE_WRITE_URL="$db_url"
docker build \
--network host \
--secret id=database_readonly_url,env=DATABASE_READONLY_URL \
--secret id=database_write_url,env=DATABASE_WRITE_URL \
-t "$IMAGE_TAG" \
-t "inferencex-app:${GITHUB_SHA::7}" \
.
- name: Sync GITHUB_TOKEN into compose .env
# Writes the INGEST_GITHUB_TOKEN repo secret into
# /opt/docker-compose/.env as GITHUB_TOKEN so the app container can
# authenticate `gh api` during `pnpm admin:db:ingest:run`.
# Rotation = update the secret + redeploy.
#
# .env is owned by the runner user and already 0600 — write directly
# (no sudo, no chown). `docker compose` runs as the runner user, so
# rewriting it as root would break compose's ability to read .env on
# the next step.
env:
INGEST_GITHUB_TOKEN: ${{ secrets.INGEST_GITHUB_TOKEN }}
run: |
if [ -z "$INGEST_GITHUB_TOKEN" ]; then
echo "INGEST_GITHUB_TOKEN secret is unset; leaving .env untouched. Ingest will fail to auth until this is set." >&2
exit 0
fi
# Atomic rewrite: write to .env.new with 0600, then mv into place.
# Same filesystem → mv is atomic, so .env is either fully-old or
# fully-new even on crash.
install -m 0600 /dev/null /opt/docker-compose/.env.new
{
grep -v '^GITHUB_TOKEN=' /opt/docker-compose/.env || true
printf 'GITHUB_TOKEN=%s\n' "$INGEST_GITHUB_TOKEN"
} >> /opt/docker-compose/.env.new
mv -f /opt/docker-compose/.env.new /opt/docker-compose/.env
- name: Recreate app + nginx
# Compose detects the new image and recreates the app container.
# nginx restarts as a dependency so it picks up the fresh upstream.
run: docker compose -f "$COMPOSE_FILE" up -d --remove-orphans
- name: Smoke-test
run: |
for i in {1..30}; do
if curl -fsS -o /dev/null http://127.0.0.1/; then
echo "app responding on :80"
exit 0
fi
sleep 2
done
echo "app did not respond" >&2
docker compose -f "$COMPOSE_FILE" logs app | tail -50
exit 1
- name: Prune images + build cache
# Bound disk growth. `docker image prune -f` only removes *dangling*
# layers — the per-sha rollback tags built above are NOT dangling, so
# they used to accumulate indefinitely until the disk filled. Keep the
# newest $KEEP_TAGS per-sha tags for rollback and drop older ones;
# `:latest` and the image the running container uses are never removed
# (rmi of an in-use image fails, which we ignore). Also cap the
# buildkit cache by age.
if: always()
env:
KEEP_TAGS: '5'
run: |
docker image prune -f
docker images inferencex-app --format '{{.Tag}}' \
| grep -vx latest \
| tail -n +$((KEEP_TAGS + 1)) \
| while read -r t; do [ -n "$t" ] && docker rmi "inferencex-app:$t" || true; done
docker builder prune -f --filter until=72h