Files
adr-sml/docs/reference/github-actions-oidc.yml.example
Pouya LajevardiandClaude Opus 5 9f2d2eeb04
Build and deploy / build-and-deploy (push) Failing after 4s
fix: resolve adversarial review round 2 — 9 findings, 8 of them in round 1's fixes
D19 caps the loop at two rounds, and this is what the second round is for.

BLOCKING. Round 1 made NO_RETAINER_NOTICE a requireEnv and added it to no
document, while the fix's own comment claimed docs/06 named it. The deployment
list said five variables for a handler that needs six, so an operator following
the cutover checklist would have deployed a function that throws at cold start
on every invocation — 5xx from API Gateway, every inquiry lost from the moment
/api/* was wired, loud in CloudWatch and silent to Pouya. docs/05 and docs/06
now name all six, and the comment that asserted the documentation existed is
corrected rather than deleted.

The intake route check added in round 1 could not fail: curl -w already prints
000 on a failed transfer, so `|| echo 000` double-appended and the failure arm
was unreachable, and the pass arm accepted anything that was not literally 404 —
including the 403 CloudFront returns when the /api/* behaviour is missing, which
is the one distinction the check exists to draw. It now sends the correct Origin
and asserts a positive: 303 to /contact/could-not-send/, which the handler
returns before any DynamoDB write or email. Probed on refused/501/403/303; the
old version passed the first three. Fixed in both deploy paths.

Removing priceRange left three statements saying it was present or pending, one
of them the stated reason /fees/ emits no Offer node. Deleting
overtimeStartsAfterSessionHours left AGENTS.md §9 naming it and left Q59
recorded as open. The Google-as-processor fix was applied to the privacy
policy's "Where it is stored" and not to "Who can see it", which still read
"Nobody else has access".

And the variable removal was justified with a path-scoped git grep — which also
cannot see untracked files. The unscoped sweep found docs/06's variable table,
the OIDC example, and .env.example still carrying them; .env.example also
restates the execute-api hostname, falsifying a live claim in intake.ts that has
been corrected. That file is not edited here: this environment denies read
access to it, and nothing may edit a file it cannot read. It is in the batched
list.

Also: og:image:alt was the page title rather than the card's headline on 20
pages; og-card.ts documented the wrong path and invocation for the contact
sheet; deploy-local.sh still said Q22's deploy credential "does NOT yet exist";
and the round-1 fix comments were trimmed per D19, though the ratio held at 0.44.

Round 2 also confirmed the round-1 fixes by measurement: all 56 .btn instances
across 22 pages, the consent checkbox's computed accessible name, the radio
labels hit-tested at 44px, and og:proof exercised against synthetic article
pages in a sandbox.

Verified: check/build/check:claims/og:proof/check:intake/lint/bio:pdf all exit 0
on a clean build; 22 pages; Lighthouse 99-100 / 100 / 100 / 100, CLS 0.000.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Md3GndFqWPzK78xAoebsg5
2026-08-31 11:21:07 -04:00

107 lines
3.9 KiB
Plaintext

# ---------------------------------------------------------------------------
# REFERENCE ONLY. This repository lives on self-hosted Gitea (AGENTS.md D3).
# The live pipeline is .gitea/workflows/deploy.yml.
#
# This file is kept because it is the better design: GitHub OIDC issues a
# short-lived token per run instead of a static key. If the project ever moves
# to GitHub, use this and delete the static IAM user. GitLab also federates
# to AWS by OIDC, but with entirely different CI syntax — this file is the
# design there, not the implementation.
# ---------------------------------------------------------------------------
name: Build and deploy
on:
push:
branches: [main]
workflow_dispatch:
# OIDC role assumption — a short-lived token per run, no static key.
# NOT the current posture: this repository deploys with a static IAM key.
# See docs/06-deployment.md for the live procedure and the IAM policy.
permissions:
contents: read
id-token: write
concurrency:
group: deploy-production
cancel-in-progress: false
jobs:
build-and-deploy:
runs-on: ubuntu-latest
environment: production
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version-file: .nvmrc
cache: npm
- name: Install
run: npm ci
- name: Type and template check
run: npm run check
- name: Build
run: npm run build
env:
PUBLIC_SITE_URL: https://adr.smlcompany.ca
# SUPERSEDED 2026-08-31 — do not copy these two lines. Build step 8
# moved the intake form to the same-origin path /api/intake, after
# which nothing in the build read either variable; both were removed
# from the live workflow and from scripts/deploy-local.sh. Kept visible
# rather than deleted because this whole file is a historical
# alternative, and a silent edit to it would make it disagree with the
# entry that recorded it.
# PUBLIC_INTAKE_ENDPOINT: ${{ vars.INTAKE_ENDPOINT }}
# PUBLIC_BOOKING_URL: ${{ vars.BOOKING_URL }}
# If adopting this: set AWS_DEPLOY_ROLE_ARN as a repository variable. The
# rest — AWS_REGION, S3_BUCKET and CLOUDFRONT_DISTRIBUTION_ID — are recorded
# in docs/06-deployment.md. (INTAKE_ENDPOINT and BOOKING_URL were listed here
# and are no longer required by either deploy path; see above.)
- name: Configure AWS credentials
uses: aws-actions/configure-aws-credentials@v4
with:
role-to-assume: ${{ vars.AWS_DEPLOY_ROLE_ARN }}
aws-region: ${{ vars.AWS_REGION }}
# Three passes: hashed immutable assets first, then images, HTML last.
# A visitor must
# never fetch a new page whose assets have not landed yet.
- name: Sync hashed assets
run: |
aws s3 sync ./dist "s3://${{ vars.S3_BUCKET }}" \
--exclude "*" \
--include "_astro/*" --include "fonts/*" \
--cache-control "public, max-age=31536000, immutable" \
--no-progress
- name: Sync images
run: |
aws s3 sync ./dist "s3://${{ vars.S3_BUCKET }}" \
--exclude "*" \
--include "*.avif" --include "*.webp" --include "*.jpg" \
--include "*.png" --include "*.svg" \
--cache-control "public, max-age=604800" \
--no-progress
- name: Sync HTML and the rest
run: |
aws s3 sync ./dist "s3://${{ vars.S3_BUCKET }}" \
--exclude "_astro/*" --exclude "fonts/*" \
--cache-control "public, max-age=0, must-revalidate" \
--delete --no-progress
- name: Invalidate CloudFront
run: |
aws cloudfront create-invalidation \
--distribution-id "${{ vars.CLOUDFRONT_DISTRIBUTION_ID }}" \
--paths "/*"
- name: Summary
run: echo "Deployed to https://adr.smlcompany.ca — commit ${GITHUB_SHA::7}" >> "$GITHUB_STEP_SUMMARY"