Skip to content

Commit 7adc082

Browse files
committed
chore: harden fly deploy workflow
1 parent 7f365d4 commit 7adc082

1 file changed

Lines changed: 110 additions & 14 deletions

File tree

.github/workflows/validate.yml

Lines changed: 110 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -9,12 +9,12 @@ on:
99
push:
1010
branches:
1111
- 'main'
12-
pull_request:
13-
branches:
14-
- 'main'
12+
pull_request: {}
13+
1514
jobs:
1615
setup:
17-
timeout-minutes: 20
16+
name: 🔧 Setup
17+
timeout-minutes: 10
1818
strategy:
1919
matrix:
2020
os: [ubuntu-latest, windows-latest, macos-latest]
@@ -37,23 +37,44 @@ jobs:
3737
run: npm run typecheck
3838
working-directory: ./workshop
3939

40-
- name: ESLint
40+
- name: Oxlint
4141
run: npm run lint
4242
working-directory: ./workshop
4343

44-
# TODO: get this working again
45-
# - name: ⬇️ Install Playwright
46-
# run: npm --prefix epicshop run test:setup
44+
tests:
45+
name: 🧪 Test
46+
timeout-minutes: 10
47+
runs-on: ubuntu-latest
48+
# Use continue-on-error to ensure this job doesn't fail the workflow
49+
continue-on-error: true
4750

48-
# - name: 🧪 In-browser tests
49-
# run: npm --prefix epicshop test
51+
steps:
52+
- name: ⬇️ Checkout repo
53+
uses: actions/checkout@v4
54+
55+
- name: ⎔ Setup node
56+
uses: actions/setup-node@v4
57+
with:
58+
node-version: 26
59+
60+
- name: 📦 Install dependencies
61+
run: npm ci
62+
63+
- name: 🧪 Run tests
64+
id: run_tests
65+
run: node ./epicshop/test.ts ..s
5066

5167
deploy:
5268
name: 🚀 Deploy
53-
timeout-minutes: 10
69+
# Retries + Fly health waits need headroom beyond a single deploy attempt.
70+
timeout-minutes: 30
5471
runs-on: ubuntu-latest
55-
# only deploy main branch on pushes
56-
if: ${{ github.ref == 'refs/heads/main' && github.event_name == 'push' }}
72+
# Deploy main on push or manual dispatch (non-forks). Dispatch is needed for
73+
# sequenced recovery / re-deploys without inventing empty commits.
74+
if:
75+
${{ github.ref == 'refs/heads/main' && github.repository_owner ==
76+
'epicweb-dev' && (github.event_name == 'push' || github.event_name ==
77+
'workflow_dispatch') }}
5778

5879
steps:
5980
- name: ⬇️ Checkout repo
@@ -63,7 +84,82 @@ jobs:
6384
uses: superfly/flyctl-actions/setup-flyctl@1.5
6485

6586
- name: 🚀 Deploy
66-
run: flyctl deploy --remote-only
6787
working-directory: ./epicshop
6888
env:
6989
FLY_API_TOKEN: ${{ secrets.FLY_API_TOKEN }}
90+
run: |
91+
set -euo pipefail
92+
93+
# Mass epicshop updates can fan out dozens of Fly deploys at once.
94+
# Fly lease contention and machines API health-check waits then flake.
95+
# Retry with exponential backoff so a single thundering-herd event does
96+
# not leave workshop apps stranded on a failed rolling update.
97+
max_attempts=4
98+
delay_seconds=45
99+
attempt=1
100+
# Support both `app: my-app` and `app: 'my-app'` / `app: "my-app"`.
101+
app_name="$(
102+
awk '
103+
/^app:[[:space:]]*/ {
104+
value = $0
105+
sub(/^app:[[:space:]]*/, "", value)
106+
gsub(/^['\''"]|['\''"]$/, "", value)
107+
print value
108+
exit
109+
}
110+
' fly.yaml
111+
)"
112+
if [ -z "$app_name" ]; then
113+
echo "Could not parse app name from fly.yaml" >&2
114+
exit 1
115+
fi
116+
117+
while true; do
118+
echo "::group::flyctl deploy attempt ${attempt}/${max_attempts}"
119+
set +e
120+
flyctl deploy --remote-only \
121+
--build-arg EPICSHOP_GITHUB_REPO=https://github.com/${{ github.repository }} \
122+
--build-arg EPICSHOP_COMMIT_SHA=${{ github.sha }}
123+
status=$?
124+
set -e
125+
echo "::endgroup::"
126+
127+
if [ "$status" -eq 0 ]; then
128+
# flyctl can treat a stopped scale-to-zero machine as "good" without
129+
# ever proving the new image boots. Wake the app and require health.
130+
echo "::group::post-deploy health check for ${app_name}"
131+
healthy=0
132+
for probe in 1 2 3 4 5 6 7 8 9 10; do
133+
set +e
134+
code="$(curl -sS -o /tmp/epicshop-health -w '%{http_code}' --max-time 30 \
135+
"https://${app_name}.fly.dev/resources/healthcheck")"
136+
curl_status=$?
137+
set -e
138+
body="$(tr -d '\n' </tmp/epicshop-health | head -c 80)"
139+
echo "probe ${probe}: curl_exit=${curl_status} http=${code} body=${body}"
140+
if [ "$curl_status" -eq 0 ] && [ "$code" = "200" ]; then
141+
healthy=1
142+
break
143+
fi
144+
sleep 15
145+
done
146+
echo "::endgroup::"
147+
148+
if [ "$healthy" -eq 1 ]; then
149+
exit 0
150+
fi
151+
152+
echo "Deploy reported success but app failed post-deploy health checks" >&2
153+
status=1
154+
fi
155+
156+
if [ "$attempt" -ge "$max_attempts" ]; then
157+
echo "flyctl deploy failed after ${max_attempts} attempts" >&2
158+
exit "$status"
159+
fi
160+
161+
echo "Deploy attempt ${attempt} failed (exit ${status}); retrying in ${delay_seconds}s..."
162+
sleep "$delay_seconds"
163+
delay_seconds=$((delay_seconds * 2))
164+
attempt=$((attempt + 1))
165+
done

0 commit comments

Comments
 (0)