Skip to content

www: regen .dockerignore — whitelist FeedHealth.tsx + the Smg* #325

www: regen .dockerignore — whitelist FeedHealth.tsx + the Smg*

www: regen .dockerignore — whitelist FeedHealth.tsx + the Smg* #325

Workflow file for this run

name: Deploy ctbk.dev
on:
push:
branches:
- www
- dev
- scrns-playwright-test
workflow_dispatch:
jobs:
build:
name: Build + deploy www branch to ctbk.dev (CF Workers Assets)
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v5
with:
ssh-key: ${{ secrets.WWW_DEPLOY_KEY }}
- uses: pnpm/action-setup@v4
with:
version: 9
- uses: actions/setup-node@v5
with:
node-version: 22 # wrangler ≥4.110 requires ≥22
cache: 'pnpm'
cache-dependency-path: www/pnpm-lock.yaml
# uv for the Slack-deploy-notification step (`thrds`).
- uses: astral-sh/setup-uv@v7
- name: Install
working-directory: www
run: pnpm install
- name: Typecheck
working-directory: www
run: pnpm tc
- name: Install Playwright browsers
working-directory: www
run: pnpm exec playwright install --with-deps chromium
- name: e2e tests (gates the deploy)
working-directory: www
env:
CI: '1'
run: pnpm test --reporter=list
# ─── Screenshots: input-gated regen, HR S3 home
# (specs/www-screenshots-s3-hr.md). Regenerate exactly when a
# declared dep changed since the provenance recorded in the S3
# manifest (`s3://ctbk/screenshots/.deps.json`), BEFORE the build
# consumes them. Nothing screenshot-related is git-tracked: the
# old pointer-commit flow triggered a second, content-identical
# deploy and raced concurrent pushes to `www`/`main`.
- name: Screenshots dep-check
id: scrgate
if: github.ref != 'refs/heads/dev'
run: |
data_md5=$(awk '/md5:/{print $3; exit}' s3/ctbk/stations/ymdgtb.dvc)
www_tree=$(git ls-files -s -- www ':!www/public/screenshots' | git hash-object --stdin)
echo "data_md5=$data_md5" >> $GITHUB_OUTPUT
echo "www_tree=$www_tree" >> $GITHUB_OUTPUT
deps=$(curl -sf https://ctbk.s3.amazonaws.com/screenshots/.deps.json || echo '{}')
rec_data=$(echo "$deps" | jq -r '.ymdgtb_md5 // "none"')
rec_tree=$(echo "$deps" | jq -r '.www_tree // "none"')
if [ "$data_md5" != "$rec_data" ] || [ "$www_tree" != "$rec_tree" ]; then
echo "stale=true" >> $GITHUB_OUTPUT
echo "stale: data $rec_data -> $data_md5 | tree $rec_tree -> $www_tree"
else
echo "stale=false" >> $GITHUB_OUTPUT
echo "fresh (data $data_md5, tree $www_tree)"
fi
- name: Regenerate screenshots (deps changed)
if: steps.scrgate.outputs.stale == 'true'
working-directory: www
env:
AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
AWS_DEFAULT_REGION: us-east-1
run: |
docker build --platform linux/amd64 -f Dockerfile.screenshots -t ctbk-screenshots .
docker run --platform linux/amd64 --name ctbk-screenshots-run ctbk-screenshots
docker cp ctbk-screenshots-run:/app/public/screenshots/. public/screenshots/
docker rm ctbk-screenshots-run
# ubuntu-24.04 runner images dropped ImageMagick.
command -v magick >/dev/null || command -v convert >/dev/null \
|| sudo apt-get install -y -qq imagemagick
bash scripts/compose-og.sh
- name: Publish screenshots to S3
if: steps.scrgate.outputs.stale == 'true' && github.ref == 'refs/heads/www'
working-directory: www
env:
AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
AWS_DEFAULT_REGION: us-east-1
run: |
# Overwrite the HR keys + manifest in place — no git commit, so
# this run's build (below) is the only deploy.
uv run scripts/push-screenshots.py \
-d "${{ steps.scrgate.outputs.data_md5 }}" \
-t "${{ steps.scrgate.outputs.www_tree }}"
- name: Upload screenshots as artifact
if: steps.scrgate.outputs.stale == 'true' && github.ref != 'refs/heads/www'
uses: actions/upload-artifact@v4
with:
name: screenshots
path: www/public/screenshots/
- name: Build
working-directory: www
run: pnpm build
# Workers-Assets deploy (`specs/done/www-cfw-migration.md`): atomic
# manifest-based upload serving ctbk.dev via the `ctbk-dev` worker's
# custom domain (attached once in the dashboard — deliberately NOT
# declared in wrangler.jsonc, so this narrow-scope token needs no
# zone permissions).
# The live site self-reports its sha (`version.json`, written at
# postbuild) — capture it BEFORE deploying to get the previous
# deploy's sha for the Slack notify's compare range. Deterministic,
# unlike the GH API runs-list archaeology it replaced (eventual
# consistency mis-picked a week-old run as "previous deploy",
# 2026-08-15).
- name: Record previous deploy sha
id: prev
if: github.ref == 'refs/heads/www' || github.ref == 'refs/heads/dev'
env:
HOST: ${{ github.ref == 'refs/heads/dev' && 'dev.ctbk.dev' || 'ctbk.dev' }}
run: |
prev=$(curl -sf "https://$HOST/version.json" | jq -r '.sha // empty' || true)
echo "sha=$prev" >> $GITHUB_OUTPUT
echo "previous deploy: ${prev:-unknown (no version.json yet)}"
- name: Deploy to Cloudflare Workers (ctbk-dev)
id: deploy
if: github.ref == 'refs/heads/www' || github.ref == 'refs/heads/dev'
working-directory: www
env:
CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}
CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
# `dev` branch → staging worker `ctbk-dev-dev` (dev.ctbk.dev)
ENV_FLAG: ${{ github.ref == 'refs/heads/dev' && '--env dev' || '' }}
run: pnpm exec wrangler deploy $ENV_FLAG
- name: Post-deploy smoke check
id: smoke
if: steps.deploy.outcome == 'success'
working-directory: www
env:
HOST: ${{ github.ref == 'refs/heads/dev' && 'dev.ctbk.dev' || 'ctbk.dev' }}
run: |
# One-shot-ish (3 tries): Workers-Assets deploys are atomic
# (index + chunks flip together), unlike the GHP era's
# out-of-sync windows that needed a 5-min polling loop.
for i in 1 2 3; do
sleep 10
live=$(curl -sf "https://$HOST/version.json" | jq -r '.sha // empty' || true)
if [ "$live" != "${{ github.sha }}" ]; then
echo " version.json sha: ${live:-none} != ${{ github.sha }}"
continue
fi
html=$(curl -sf "https://$HOST/" || true)
[ -z "$html" ] && continue
chunks=$(echo "$html" | grep -oE 'assets/[A-Za-z0-9._/-]+\.(js|css)' | sort -u)
[ -z "$chunks" ] && continue
fail=0
for c in $chunks; do
code=$(curl -s -o /dev/null -w '%{http_code}' "https://$HOST/$c")
[ "$code" = "200" ] || { fail=1; echo " $code $c"; break; }
done
if [ "$fail" = "0" ]; then
echo "smoke OK: version.json matches, $(echo $chunks | wc -w) chunk(s) all 200"
exit 0
fi
done
echo 'smoke check failed after 3 attempts'
exit 1
- name: Notify Slack — site deployed
if: github.ref == 'refs/heads/www' && steps.deploy.outcome == 'success' && steps.smoke.outcome == 'success'
env:
SLACK_BOT_TOKEN: ${{ secrets.SLACK_BOT_TOKEN }}
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
SHA: ${{ github.sha }}
PREV_SHA: ${{ steps.prev.outputs.sha }}
MSG: ${{ github.event.head_commit.message }}
RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}
REPO_URL: ${{ github.server_url }}/${{ github.repository }}
REPO_SLUG: ${{ github.repository }}
run: |
uv run --no-project --with 'thrds==0.4.0' --python 3.12 python - <<'PY'
import json, os, subprocess, urllib.request
from thrds import SlackClient, Thread
gh_token = os.environ['GITHUB_TOKEN']
sha = os.environ['SHA']
short = sha[:7]
# `head_commit` is absent on workflow_dispatch triggers; fall back
# to the checked-out commit's subject instead of "(no message)".
msg = os.environ.get('MSG') or subprocess.run(
['git', 'log', '-1', '--format=%s'], capture_output=True, text=True,
).stdout
subject = msg.splitlines()[0] if msg.strip() else '(no message)'
repo = os.environ['REPO_URL']
run_url = os.environ['RUN_URL']
repo_slug = os.environ['REPO_SLUG']
def gh(path):
req = urllib.request.Request(f'https://api.github.com{path}', headers={
'Authorization': f'Bearer {gh_token}',
'Accept': 'application/vnd.github+json',
})
with urllib.request.urlopen(req) as r:
return json.load(r)
# Range floor = what the live site served just before this
# deploy (its `version.json`, captured by the `prev` step).
prev_sha = os.environ.get('PREV_SHA') or None
commits = []
if prev_sha and prev_sha != sha:
try:
commits = gh(f'/repos/{repo_slug}/compare/{prev_sha}...{sha}').get('commits', [])
except Exception as e:
# Force-push or rebase can make `prev_sha` unreachable; degrade gracefully.
print(f'compare {prev_sha}..{sha} failed: {e}; falling back to single-commit shape')
n = len(commits)
if n <= 1:
# Single commit (or unknown range): show that commit's subject inline.
op_text = (
f":rocket: *ctbk.dev deployed* — <{repo}/commit/{sha}|`{short}`> {subject}\n"
f"<https://ctbk.dev|ctbk.dev> · <{run_url}|run>"
)
messages = [op_text]
else:
# Multi-commit deploy: OP shows range + count, thread reply has per-commit detail.
short_prev = prev_sha[:7]
compare_url = f"{repo}/compare/{prev_sha}...{sha}"
op_text = (
f":rocket: *ctbk.dev deployed* — <{compare_url}|`{short_prev}..{short}`> ({n} commits since last deploy)\n"
f"<https://ctbk.dev|ctbk.dev> · <{run_url}|run>"
)
bullets = '\n'.join(
f"• <{repo}/commit/{c['sha']}|`{c['sha'][:7]}`> {(c['commit']['message'] or '').splitlines()[0]}"
for c in commits
)
messages = [op_text, bullets]
# `Thread.sync` posts `messages[0]` as the thread OP, the rest
# as replies — no manual `thread_ts` plumbing.
SlackClient(
token=os.environ['SLACK_BOT_TOKEN'],
channel='C0B5MKF28NP',
username='ctbk-bot',
icon_emoji=':bike:',
).sync(Thread(messages=messages))
PY
# Failed deploys were previously SILENT (the success notify simply
# didn't fire) — e.g. 2026-08-14's two e2e-gated failures left the
# site undeployable for ~5h with no ping.
- name: Notify Slack — deploy failed
if: failure() && github.ref == 'refs/heads/www'
env:
SLACK_BOT_TOKEN: ${{ secrets.SLACK_BOT_TOKEN }}
SHA: ${{ github.sha }}
MSG: ${{ github.event.head_commit.message }}
RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}
REPO_URL: ${{ github.server_url }}/${{ github.repository }}
run: |
uv run --no-project --with 'thrds==0.4.0' --python 3.12 python - <<'PY'
import os, subprocess
from thrds import SlackClient
sha = os.environ['SHA']
msg = os.environ.get('MSG') or subprocess.run(
['git', 'log', '-1', '--format=%s'], capture_output=True, text=True,
).stdout
subject = msg.splitlines()[0] if msg.strip() else '(no message)'
SlackClient(
token=os.environ['SLACK_BOT_TOKEN'],
channel='C0B5MKF28NP',
username='ctbk-bot',
icon_emoji=':bike:',
).post(
f":x: *ctbk.dev deploy FAILED* — <{os.environ['REPO_URL']}/commit/{sha}|`{sha[:7]}`> {subject}\n"
f"<{os.environ['RUN_URL']}|logs>"
)
PY