-
Notifications
You must be signed in to change notification settings - Fork 0
207 lines (182 loc) · 7.68 KB
/
Copy pathcloud.yml
File metadata and controls
207 lines (182 loc) · 7.68 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
name: cloud
# CI for the Ghost Cloud SaaS (cloud/). Scoped to cloud/** so it never runs on
# Rust-only changes and never collides with rust.yml.
#
# Postgres and Redis are real services rather than placeholders. Roughly 90 of
# the suite's tests are gated on `Boolean(process.env.DATABASE_URL)`, so a
# DATABASE_URL pointing at nothing is the worst of both worlds: the guard reads
# "a database is available", the tests run, and they fail on connection refused.
# Without the variable at all they silently skip, which is how a fully green
# check can cover none of the engine.
#
# Path scoping lives in the `changes` job, not in `on:` — see the long comment
# in rust.yml. Short version: a workflow skipped by a path filter never creates
# its check runs, so requiring `build` in branch protection would leave every
# Rust-only PR blocked on a check that can never report. A job skipped by an
# `if:` condition does create its check run, concludes "skipped", and satisfies
# branch protection.
on:
push:
branches: [master, develop]
pull_request:
concurrency:
group: cloud-ci-${{ github.ref }}
cancel-in-progress: true
permissions:
contents: read
pull-requests: read
defaults:
run:
working-directory: cloud
jobs:
changes:
name: Detect changed paths
runs-on: ubuntu-latest
timeout-minutes: 5
# This job never checks out, so the workflow-level `working-directory:
# cloud` would point bash at a directory that does not exist yet.
defaults:
run:
working-directory: .
outputs:
cloud: ${{ steps.filter.outputs.cloud }}
steps:
- id: filter
env:
GH_TOKEN: ${{ github.token }}
run: |
set -euo pipefail
if [ "${{ github.event_name }}" != "pull_request" ]; then
echo "cloud=true" >> "$GITHUB_OUTPUT"
exit 0
fi
files=$(gh api \
"repos/${{ github.repository }}/pulls/${{ github.event.number }}/files" \
--paginate --jq '.[].filename')
if printf '%s\n' "$files" \
| grep -qE '^cloud/|^\.github/workflows/cloud\.yml$'; then
echo "cloud=true" >> "$GITHUB_OUTPUT"
else
echo "cloud=false" >> "$GITHUB_OUTPUT"
fi
build:
needs: changes
# Fail closed. If detection itself breaks, `needs.changes.outputs.cloud` is
# empty and a bare equality test would skip this job — which branch
# protection then accepts as satisfied, merging cloud changes with no CI at
# all. Run on failure instead. `!cancelled()` is required for the job to be
# considered at all once a dependency has failed.
if: ${{ !cancelled() && (needs.changes.result == 'failure' || needs.changes.outputs.cloud == 'true') }}
runs-on: ubuntu-latest
timeout-minutes: 20
services:
postgres:
image: postgres:16-alpine
env:
POSTGRES_USER: ghost
POSTGRES_PASSWORD: ghost
POSTGRES_DB: ghost
ports:
- 5432:5432
options: >-
--health-cmd "pg_isready -U ghost"
--health-interval 10s
--health-timeout 5s
--health-retries 10
redis:
image: redis:7-alpine
ports:
- 6379:6379
options: >-
--health-cmd "redis-cli ping"
--health-interval 10s
--health-timeout 5s
--health-retries 10
env:
# The user must be `ghost`, not `postgres`: the wrong one fails with
# "User was denied access on the database", which reads like a missing
# migration and sends you looking in the wrong place.
DATABASE_URL: postgresql://ghost:ghost@localhost:5432/ghost?schema=public
REDIS_URL: redis://localhost:6379
AUTH_SECRET: ci-placeholder-secret-value-0000000000000000
APP_URL: http://localhost:3000
GHOST_ARTIFACT_DIR: ${{ github.workspace }}/cloud/.artifacts
# Required, not optional. Without it the worker skips session capture at
# an approval gate, so `sessionUrl` stays null and every gated run refuses
# to resume — `planRestore` returns `unsafe` and the run ends INCIDENT.
# That is correct fail-safe behaviour in production and a broken suite
# here: 16 tests in compensateRun/runWorkflow/slots assert a run reaches
# SUCCEEDED or COMPENSATED, and they fail on the status rather than on
# anything naming the key. A CI-only throwaway value; never a real secret.
GHOST_SESSION_KEY: Z2hvc3QtY2ktdGhyb3dhd2F5LXNlc3Npb24ta2V5LTA=
steps:
- uses: actions/checkout@v4
- uses: pnpm/action-setup@v4
with:
version: 10.33.0
- uses: actions/setup-node@v4
with:
node-version: 22
cache: pnpm
cache-dependency-path: cloud/pnpm-lock.yaml
- name: Install dependencies
run: pnpm install --frozen-lockfile
- name: Install Playwright Chromium
# The driver and engine tests drive real Chromium. Runners have none
# preinstalled, so install it here. PLAYWRIGHT_BROWSERS_PATH is left
# unset, which makes `discoverChromium` no-op and lets Playwright use
# its own cache — the revision then matches by construction.
run: pnpm --filter @ghost/worker exec playwright install --with-deps chromium
- name: Prisma validate
run: pnpm db:validate
- name: Apply migrations
# `migrate deploy`, not `migrate dev` — the latter is interactive and
# will offer to reset the database.
run: pnpm db:migrate:deploy
- name: Typecheck
run: pnpm typecheck
- name: Tests
run: pnpm test
- name: Build
run: pnpm build
- name: Build worker container image
# `pnpm build` above only bundles the worker with tsup — it never
# exercises the Dockerfile's own multi-stage COPY list, and it never
# runs the resulting dist/index.js in an isolated, workspace-filtered
# node_modules the way the image's `pnpm install --filter
# @ghost/worker...` does. Both gaps let a genuinely broken image ship
# silently before: a missing `packages/core/prisma` COPY failed
# `pnpm install` inside the image, and bundling @prisma/client's CJS
# runtime into the ESM output crashed the container on boot with
# "Dynamic require of 'fs' is not supported". Build it here so a
# regression in either fails CI instead of surfacing at deploy time.
run: docker build -f apps/worker/Dockerfile -t ghost-worker-ci .
- name: Smoke-test worker container boots
# A build succeeding is not the same as the entrypoint running. Boot
# it against the same Postgres/Redis service containers this job
# already has and require the worker's own startup log line — not just
# "container still alive", which a crash-loop with a restart policy
# would also satisfy.
run: |
set -euo pipefail
container_id=$(docker run -d --network host \
-e DATABASE_URL="$DATABASE_URL" \
-e REDIS_URL="$REDIS_URL" \
-e GHOST_SESSION_KEY="$GHOST_SESSION_KEY" \
ghost-worker-ci)
trap 'docker logs "$container_id" || true; docker rm -f "$container_id" >/dev/null 2>&1 || true' EXIT
ok=""
for _ in $(seq 1 15); do
if docker logs "$container_id" 2>&1 | grep -q "Ghost worker started"; then
ok=1
break
fi
if [ "$(docker inspect -f '{{.State.Running}}' "$container_id")" != "true" ]; then
break
fi
sleep 1
done
if [ -z "$ok" ]; then
echo "worker container did not report startup within 15s" >&2
exit 1
fi