One job title, in one place #91
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| # 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" |