-
Notifications
You must be signed in to change notification settings - Fork 0
218 lines (199 loc) Β· 8.99 KB
/
Copy pathci.yml
File metadata and controls
218 lines (199 loc) Β· 8.99 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
209
210
211
212
213
214
215
216
217
218
name: CI
# OvenClear core is an offline, deterministic TypeScript library (Node 18+,
# vitest). This pipeline mirrors what a judge runs locally: typecheck + tests,
# then the offline PROOF stage (deterministic seed re-hash, end-to-end self-test,
# ledger tamper-rejection, benchmark), then a security gate. There is no UI
# framework, so the Lighthouse / bundle stages of the standard template are N/A.
#
# Stage 4 deploys the storefront to Vercel production β push to main only, and
# only once stages 1β3 are green. See that job for the staged-rollout mechanics
# that keep https://ovenclear.edycu.dev up even when a deploy is bad.
on:
push:
branches: [main]
pull_request:
branches: [main]
workflow_dispatch:
concurrency:
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
# Superseded PR runs are noise and get cancelled. A main-branch run is never
# cancelled: it carries the production deploy, and killing that halfway is how
# a live site ends up half-promoted.
cancel-in-progress: ${{ github.event_name == 'pull_request' }}
permissions:
contents: read
jobs:
# ββ Stage 1: Quality (typecheck + 156 vitest tests) ββββββββββββββββββββββ
quality:
name: "Stage 1 Β· Quality"
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
node-version: ${{ github.event_name == 'push' && fromJSON('[20, 22]') || fromJSON('[20]') }}
steps:
- name: Checkout
uses: actions/checkout@v7
- name: Setup Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v7
with:
node-version: ${{ matrix.node-version }}
cache: "npm"
- name: Install dependencies
run: npm ci
- name: Type check (tsc --noEmit, strict)
run: npm run typecheck
- name: Unit + golden tests (vitest run)
run: npm test
# ββ Stage 2: Proof (offline determinism + provenance integrity) ββββββββββ
proof:
name: "Stage 2 Β· Offline Proof"
needs: [quality]
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v7
- name: Setup Node.js
uses: actions/setup-node@v7
with:
node-version: 20
cache: "npm"
- name: Install dependencies
run: npm ci
- name: Deterministic fixture re-hash (seed --check)
run: npm run seed:check
- name: End-to-end self-test (interview β verdict β label + QA β re-issue)
run: npm run self-test
- name: Ledger tamper-rejection (verify_ledger)
run: npm run verify-ledger
- name: Verdict benchmark + zero golden-flip gate
run: npm run bench
# ββ Stage 3: Security (secret scan + dependency audit) βββββββββββββββββββ
security:
name: "Stage 3 Β· Security"
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v7
with:
fetch-depth: 0 # full history for the secret scanner
- name: TruffleHog secret scan
uses: trufflesecurity/trufflehog@main
with:
extra_args: --only-verified
- name: Setup Node.js
uses: actions/setup-node@v7
with:
node-version: 20
cache: "npm"
- name: Install dependencies
run: npm ci
- name: npm audit (high + critical)
run: npm audit --audit-level=high
continue-on-error: true # advisory: never block on a transitive upstream advisory
# ββ Stage 4: Deploy (Vercel production, staged rollout) ββββββββββββββββββ
#
# https://ovenclear.edycu.dev is a judged artifact. The ordering below exists
# so that a broken build can never take it down:
#
# 1. Build api/index.js locally. Vercel has no build command configured for
# this project, so the bundle esbuild produces from api/_app.ts is what
# actually gets uploaded and run. Skip this step and the function is
# simply absent (api/index.js is gitignored β it is an artifact, not
# source). .vercelignore deliberately does NOT exclude it.
# 2. Deploy to the production target with --skip-domain. That creates a real
# production deployment but leaves every domain, including the custom one,
# pointing at the deployment that is already known to work.
# 3. Smoke the new deployment on its own URL. If it fails, the job stops here
# and the live site never saw it.
# 4. Only then promote, force the custom-domain alias (promote does not
# reliably move an aliased custom domain), and smoke the live domain.
#
# Runtime configuration (NODE_ENV, DATA_DIR, PRICE_USD, LEDGER_KEY_NAMESPACE,
# STRIPE_*, BASE_URL) lives in the Vercel project's Production environment, not
# here β server/config.ts refuses to boot in production without it, so a
# missing variable fails step 3 rather than the live site.
deploy:
name: "Stage 4 Β· Deploy (Vercel production)"
needs: [quality, proof]
if: github.ref == 'refs/heads/main' && (github.event_name == 'push' || github.event_name == 'workflow_dispatch')
runs-on: ubuntu-latest
environment:
name: Production
url: https://ovenclear.edycu.dev
env:
VERCEL_TOKEN: ${{ secrets.VERCEL_TOKEN }}
VERCEL_ORG_ID: ${{ vars.VERCEL_ORG_ID }}
VERCEL_PROJECT_ID: ${{ vars.VERCEL_PROJECT_ID }}
PROD_DOMAIN: ovenclear.edycu.dev
steps:
- name: Checkout
uses: actions/checkout@v7
- name: Setup Node.js
uses: actions/setup-node@v7
with:
node-version: 20
cache: "npm"
- name: Install dependencies
run: npm ci
- name: Build the serverless bundle (api/_app.ts β api/index.js)
run: npm run build:vercel
- name: Refuse to deploy without the function bundle
run: |
test -s api/index.js || { echo "::error::api/index.js is missing or empty β the deployed function would 404."; exit 1; }
echo "api/index.js: $(wc -c < api/index.js) bytes"
- name: Install Vercel CLI
run: npm install --global vercel@53
- name: Link and pull the production environment
run: vercel pull --yes --environment=production --token="$VERCEL_TOKEN"
- name: Deploy to production (custom domain NOT moved yet)
id: deploy
run: |
set -euo pipefail
# `--skip-domain` makes this the current production deployment while
# leaving ovenclear.edycu.dev on the last known-good one, which is what
# buys us the smoke gate below. The CLI prints a JSON envelope on newer
# versions and a bare URL on older ones; accept either.
vercel deploy --prod --skip-domain --archive=tgz --token="$VERCEL_TOKEN" | tee deploy.out
url=$(node -e '
const fs = require("fs");
const t = fs.readFileSync("deploy.out", "utf8").trim();
try { const j = JSON.parse(t); if (j?.deployment?.url) { console.log(j.deployment.url); process.exit(0); } } catch {}
const m = t.match(/https:\/\/[A-Za-z0-9._-]+\.vercel\.app/);
if (!m) { console.error("could not parse a deployment URL out of the CLI output"); process.exit(1); }
console.log(m[0]);
')
rm -f deploy.out
echo "Deployed: $url"
echo "url=$url" >> "$GITHUB_OUTPUT"
echo "host=${url#https://}" >> "$GITHUB_OUTPUT"
- name: Smoke the new deployment BEFORE it takes traffic
run: node scripts/smoke.mjs "${{ steps.deploy.outputs.url }}"
- name: Move the custom domain onto the verified deployment
run: |
set -uo pipefail
new_host="${{ steps.deploy.outputs.host }}"
served=$(vercel inspect "https://${PROD_DOMAIN}" --token="$VERCEL_TOKEN" 2>&1 || true)
if printf '%s' "$served" | grep -qF "$new_host"; then
echo "${PROD_DOMAIN} already resolves to ${new_host}."
else
# Known Vercel behaviour, and the reason this step exists at all: a
# production deployment does not reliably pull an aliased custom
# domain across with it, so the alias is set explicitly.
echo "${PROD_DOMAIN} is not on ${new_host} yet; setting the alias explicitly."
vercel alias set "${{ steps.deploy.outputs.url }}" "$PROD_DOMAIN" --token="$VERCEL_TOKEN"
served=$(vercel inspect "https://${PROD_DOMAIN}" --token="$VERCEL_TOKEN" 2>&1 || true)
printf '%s' "$served" | grep -qF "$new_host" \
|| { echo "::error::${PROD_DOMAIN} still does not point at ${new_host}."; exit 1; }
fi
- name: Smoke the LIVE site (hard gate)
run: node scripts/smoke.mjs "https://${PROD_DOMAIN}"
- name: Summarise
if: always()
run: |
{
echo "### Production deploy"
echo ""
echo "- deployment: ${{ steps.deploy.outputs.url }}"
echo "- live: https://${PROD_DOMAIN}"
} >> "$GITHUB_STEP_SUMMARY"