99 push :
1010 branches :
1111 - ' main'
12- pull_request :
13- branches :
14- - ' main'
12+ pull_request : {}
13+
1514jobs :
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
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