Skip to content

One job title, in one place #91

One job title, in one place

One job title, in one place #91

Workflow file for this run

# Deploys asifuddin.com whenever main changes.
#
# Why this file exists: Sveltia CMS commits straight to this repository, and
# nothing was watching. Twenty-five plates added through /admin sat on main for
# days without reaching the site, because every deploy until now was somebody
# running `wrangler deploy` by hand.
#
# The verification step is deliberate. A save from /admin can break a schema, a
# build guard or the embargo rule, and shipping a broken site is worse than
# shipping late — so the deploy runs only if `npm run verify` passes. When it
# does not, GitHub emails the failure and the live site keeps the last good
# version.
name: Deploy
on:
push:
branches: [main]
# Lets you re-run a deploy from the Actions tab without pushing anything.
workflow_dispatch:
# A burst of CMS saves would otherwise start several overlapping deploys that
# finish out of order, and the site could end up on an older build than the one
# that started last. Cancel the earlier run instead.
concurrency:
group: deploy-main
cancel-in-progress: true
jobs:
deploy:
runs-on: ubuntu-latest
timeout-minutes: 15
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 22
cache: npm
# The reproduction snippets are executed by `verify-snippets`, so the
# runner needs a Python that matches the one their output was recorded
# under (spec I-4).
- uses: actions/setup-python@v5
with:
python-version: '3.12'
- name: Install
run: npm ci
# The credential is inspected before the build, so a missing one costs
# ten seconds instead of three minutes. The result is turned into three
# separately named steps rather than one message, so the *name of the
# failing step* states the cause — visible at a glance in the Actions
# list, and readable through the API without opening a log.
- name: Inspect the deploy credential
id: cred
env:
TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}
ACCOUNT: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
run: |
set +e
# A length is not a value. GitHub masks the secret itself; this is the
# one fact that separates "never arrived" from "arrived and refused".
echo "token characters visible to this job: ${#TOKEN}"
echo "account characters visible to this job: ${#ACCOUNT}"
if [ -z "$TOKEN" ]; then
echo "state=absent" >> "$GITHUB_OUTPUT"; exit 0
fi
export CLOUDFLARE_API_TOKEN="$TOKEN"
if [ -n "$ACCOUNT" ]; then export CLOUDFLARE_ACCOUNT_ID="$ACCOUNT"; fi
OUT=$(npx wrangler whoami 2>&1); RC=$?
echo "$OUT"
if [ $RC -ne 0 ]; then
echo "state=rejected" >> "$GITHUB_OUTPUT"; exit 0
fi
if echo "$OUT" | grep -qi "more than one account\|multiple accounts"; then
echo "state=ambiguous" >> "$GITHUB_OUTPUT"; exit 0
fi
echo "state=ok" >> "$GITHUB_OUTPUT"
- name: "Blocked - no token reached this job"
if: steps.cred.outputs.state == 'absent'
run: |
{
echo "### No token reached this job"
echo ""
echo "\`CLOUDFLARE_API_TOKEN\` arrived empty. Everything else passed."
echo ""
echo "GitHub keeps **four** secret stores and a workflow can read only"
echo "one of them. Open"
echo "<https://github.com/asifuddin01/portfolio/settings/secrets/actions>"
echo "and confirm the name is listed under **Repository secrets** —"
echo "not *Environment*, not *Dependabot*, not *Codespaces* — spelled"
echo "exactly \`CLOUDFLARE_API_TOKEN\`, with no trailing space."
} >> "$GITHUB_STEP_SUMMARY"
exit 1
- name: "Blocked - Cloudflare rejected the token"
if: steps.cred.outputs.state == 'rejected'
run: |
{
echo "### Cloudflare rejected the token"
echo ""
echo "The secret arrived, so the GitHub side is correct. \`wrangler whoami\`"
echo "refused it — see the previous step's output for Cloudflare's own words."
echo ""
echo "The usual cause is that the token **ID** was copied rather than the"
echo "token **value**. Cloudflare shows the value once, on the screen"
echo "immediately after creation. Create a fresh one from the"
echo "**Edit Cloudflare Workers** template at"
echo "<https://dash.cloudflare.com/profile/api-tokens> and replace the secret."
} >> "$GITHUB_STEP_SUMMARY"
exit 1
- name: "Blocked - the token reaches several accounts"
if: steps.cred.outputs.state == 'ambiguous'
run: |
{
echo "### The token reaches more than one account"
echo ""
echo "Add a second repository secret \`CLOUDFLARE_ACCOUNT_ID\` naming the"
echo "account that owns the \`portfolio\` Worker, then re-run."
} >> "$GITHUB_STEP_SUMMARY"
exit 1
- name: Verify
env:
# The public demo, embedded on /researchlens. Set here rather than
# as a repository secret: it is a public URL, and a value the build
# depends on should be readable in the file that builds it.
#
# Unset it and the page falls back to probing for a local instance,
# then to the offline message — which is what it should say when
# there is nothing to embed.
PUBLIC_RESEARCHLENS_SPACE: https://asifuddin01-researchlens.hf.space
run: npm run verify
- name: Deploy
env:
CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}
CF_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
run: |
if [ -n "$CF_ACCOUNT_ID" ]; then export CLOUDFLARE_ACCOUNT_ID="$CF_ACCOUNT_ID"; fi
# Verify built dist a moment ago, but assert it rather than assume it:
# wrangler uploads the directory as it finds it and cannot tell that
# it predates the content it is meant to serve.
npm run check:dist
npx wrangler deploy
- name: Report
run: |
{
echo "### Deployed"
echo ""
echo "<https://asifuddin.com> now serves \`${GITHUB_SHA:0:7}\`."
} >> "$GITHUB_STEP_SUMMARY"