-
Notifications
You must be signed in to change notification settings - Fork 1.1k
236 lines (212 loc) · 8.07 KB
/
Copy pathvalidate.yml
File metadata and controls
236 lines (212 loc) · 8.07 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
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
name: deploy
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
on:
workflow_dispatch:
push:
branches:
- 'main'
pull_request: {}
jobs:
setup:
name: 🔧 Setup
timeout-minutes: 15
strategy:
fail-fast: false
matrix:
os: [ubuntu-latest, windows-latest, macos-latest]
runs-on: ${{ matrix.os }}
steps:
- name: ⎔ Setup node
uses: actions/setup-node@v4
with:
node-version: 26
# SKIP_PLAYGROUND keeps typecheck/lint meaningful: problem playground apps
# intentionally fail solution tests/types, which would create false CI red.
# Use bash explicitly: Windows runners default to PowerShell, which cannot
# parse this retry loop.
- name: ▶️ Add repo
shell: bash
env:
# Kept getting npm ECOMPROMISED errors on windows. This fixed it.
npm_config_cache: ${{ runner.temp }}/npm-cache
SKIP_PLAYWRIGHT: true
SKIP_PLAYGROUND: true
run: |
set -euo pipefail
attempt=1
max_attempts=3
delay_seconds=20
while true; do
echo "::group::epicshop add attempt ${attempt}/${max_attempts}"
set +e
npx --yes epicshop@latest add ${{ github.event.repository.name }}#${{ github.head_ref || github.ref_name }} ./workshop
status=$?
set -e
echo "::endgroup::"
if [ "$status" -eq 0 ]; then
exit 0
fi
if [ "$attempt" -ge "$max_attempts" ]; then
echo "epicshop add failed after ${max_attempts} attempts" >&2
exit "$status"
fi
echo "epicshop add failed (exit ${status}); retrying in ${delay_seconds}s..."
sleep "$delay_seconds"
delay_seconds=$((delay_seconds * 2))
attempt=$((attempt + 1))
done
# Some workshops generate React Router route types and Prisma clients for
# their exercise apps here. Generate them before the root project-reference
# typecheck, while allowing older workshops without this script to proceed.
- name: ⚙️ Generate app artifacts
run: npm run generate --if-present
working-directory: ./workshop
# Not every workshop has TypeScript. `--if-present` skips only a missing
# script; npm still returns a failure when an existing typecheck fails.
- name: ʦ TypeScript
run: npm run typecheck --if-present
working-directory: ./workshop
- name: ⬣ Lint
run: npm run lint
working-directory: ./workshop
env:
NODE_OPTIONS: --max-old-space-size=8192
tests:
name: 🧪 Test
timeout-minutes: 15
runs-on: ubuntu-latest
steps:
- name: ⬇️ Checkout repo
uses: actions/checkout@v4
- name: ⎔ Setup node
uses: actions/setup-node@v4
with:
node-version: 26
- name: 📦 Install dependencies
run: npm ci
- name: 🎭 Install Playwright browser
if: ${{ hashFiles('epicshop/test.js') != '' }}
run: npx playwright install --with-deps chromium
- name: ◭ Generate Prisma clients
if: ${{ hashFiles('epicshop/test.js') != '' }}
run: |
set -euo pipefail
shopt -s globstar nullglob
schemas=()
for schema in ./exercises/**/prisma/schema.prisma; do
app_dir="${schema%/prisma/schema.prisma}"
app_name="${app_dir##*/}"
if [[ "$app_name" =~ ^[0-9]+\.solution(\..+)?$ ]]; then
schemas+=("$schema")
fi
done
if [ "${#schemas[@]}" -eq 0 ]; then
echo "No solution-app Prisma schemas found; skipping Prisma client generation."
exit 0
fi
for schema in "${schemas[@]}"; do
app_dir="${schema%/prisma/schema.prisma}"
echo "::group::prisma generate in ${app_dir}"
(cd "$app_dir" && npx prisma generate)
echo "::endgroup::"
done
# Workshops ship epicshop/test.js (not .ts). `..s` runs solution apps only.
- name: 🧪 Run solution tests
run: |
set -euo pipefail
if [ -f ./epicshop/test.js ]; then
node ./epicshop/test.js ..s
else
echo "No epicshop/test.js in this workshop; skipping solution-app test runner."
fi
deploy:
name: 🚀 Deploy
# Retries + Fly health waits need headroom beyond a single deploy attempt.
timeout-minutes: 30
runs-on: ubuntu-latest
# Deploy main on push or manual dispatch (non-forks). Dispatch is needed for
# sequenced recovery / re-deploys without inventing empty commits.
if:
${{ github.ref == 'refs/heads/main' && github.repository_owner ==
'epicweb-dev' && (github.event_name == 'push' || github.event_name ==
'workflow_dispatch') }}
steps:
- name: ⬇️ Checkout repo
uses: actions/checkout@v4
- name: 🎈 Setup Fly
uses: superfly/flyctl-actions/setup-flyctl@1.5
- name: 🚀 Deploy
working-directory: ./epicshop
env:
FLY_API_TOKEN: ${{ secrets.FLY_API_TOKEN }}
run: |
set -euo pipefail
# Mass epicshop updates can fan out dozens of Fly deploys at once.
# Fly lease contention and machines API health-check waits then flake.
# Retry with exponential backoff so a single thundering-herd event does
# not leave workshop apps stranded on a failed rolling update.
max_attempts=4
delay_seconds=45
attempt=1
# Support both `app: my-app` and `app: 'my-app'` / `app: "my-app"`.
app_name="$(
awk '
/^app:[[:space:]]*/ {
value = $0
sub(/^app:[[:space:]]*/, "", value)
gsub(/^['\''"]|['\''"]$/, "", value)
print value
exit
}
' fly.yaml
)"
if [ -z "$app_name" ]; then
echo "Could not parse app name from fly.yaml" >&2
exit 1
fi
while true; do
echo "::group::flyctl deploy attempt ${attempt}/${max_attempts}"
set +e
flyctl deploy --remote-only \
--build-arg EPICSHOP_GITHUB_REPO=https://github.com/${{ github.repository }} \
--build-arg EPICSHOP_COMMIT_SHA=${{ github.sha }}
status=$?
set -e
echo "::endgroup::"
if [ "$status" -eq 0 ]; then
# flyctl can treat a stopped scale-to-zero machine as "good" without
# ever proving the new image boots. Wake the app and require health.
echo "::group::post-deploy health check for ${app_name}"
healthy=0
for probe in 1 2 3 4 5 6 7 8 9 10; do
set +e
code="$(curl -sS -o /tmp/epicshop-health -w '%{http_code}' --max-time 30 \
"https://${app_name}.fly.dev/resources/healthcheck")"
curl_status=$?
set -e
body="$(tr -d '\n' </tmp/epicshop-health | head -c 80)"
echo "probe ${probe}: curl_exit=${curl_status} http=${code} body=${body}"
if [ "$curl_status" -eq 0 ] && [ "$code" = "200" ]; then
healthy=1
break
fi
sleep 15
done
echo "::endgroup::"
if [ "$healthy" -eq 1 ]; then
exit 0
fi
echo "Deploy reported success but app failed post-deploy health checks" >&2
status=1
fi
if [ "$attempt" -ge "$max_attempts" ]; then
echo "flyctl deploy failed after ${max_attempts} attempts" >&2
exit "$status"
fi
echo "Deploy attempt ${attempt} failed (exit ${status}); retrying in ${delay_seconds}s..."
sleep "$delay_seconds"
delay_seconds=$((delay_seconds * 2))
attempt=$((attempt + 1))
done