Amends D1 to pin the major explicitly (v7.x) rather than inherit it. The ^5.0.0 pin was recalled rather than checked and was two majors stale the day it was written, which meant shipping a framework carrying high-severity XSS advisories. CLAUDE.md now requires every version pin to be verified against the registry, and R11 requires re-checking at each build-order boundary. npm audit now reports 0 vulnerabilities, down from 16. Every Astro advisory is cleared; the residual 10 all traced to @lhci/cli, which is removed — it was the sole source of 7 high-severity findings, 0.15.1 is latest so there was no clean upgrade, and it cannot run without pages or a lighthouserc. Re-added at build step 7 with a freshly verified pin. Content collections migrated to the Content Layer API: src/content.config.ts, loader: glob(), z from astro/zod. Two review passes found seven defects in the fix itself, all now closed: - z.coerce.date() read an unquoted 20260801 as epoch milliseconds and yielded 1970-01-01 silently; the first replacement then accepted 2026-13-45 as an Invalid Date and rolled 2026-02-30 over to 2026-03-02. Dates are now anchored, date-only, parsed as UTC and round-tripped. - The title bound applied the SEO spec's 50-60 to the headline rather than the rendered <title>, which guaranteed 68-78 on every article and rejected all five planned launch headlines. Articles are now the documented exception: the headline is the <title>, no suffix. - An article could ship an image with no alt text, or whitespace-only alt. - Two schema comments asserted controls nothing enforced; both are now real refinements, each tested with a failing and a passing case. - PRACTICE_SLUGS and PRACTICE_AREAS could drift silently; a compile-time check now catches both directions. - eslint.config.js imported globals and @eslint/js undeclared, resolving by hoisting accident. - scripts/deploy-local.sh claimed parity with CI while skipping npm run check and two credential guards — on the only path this site can ship today. Accessibility linting is on (36 jsx-a11y rules) before step 1 writes the layout. An earlier claim in §7 that none was possible was wrong twice, and is corrected in AGENTS.md entry (t) along with the reasoning. Opens Q30 and Q31 for two unregistered claims in src/data/site.ts. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012XquaEq4BgWMCwUqLEyNkF
152 lines
6.1 KiB
YAML
152 lines
6.1 KiB
YAML
# Gitea Actions — the live pipeline for this repository.
|
|
#
|
|
# Gitea Actions speaks GitHub Actions syntax, so this is a near-direct port of
|
|
# docs/reference/github-actions-oidc.yml.example (kept as the OIDC reference in
|
|
# case the repo ever moves to GitHub; it lives under docs/ rather than
|
|
# .github/workflows/ so Gitea can never fall back to it).
|
|
#
|
|
# ONE REAL DIFFERENCE: Gitea is not an AWS OIDC provider, so there is no role to
|
|
# assume. Deploys are designed to authenticate with a SCOPED IAM USER whose key
|
|
# lives only in this repository's Gitea secrets. Whether that user and key have
|
|
# actually been created is AGENTS.md Q22 — unanswered as of 2026-08-26.
|
|
#
|
|
# See docs/06-deployment.md for the exact IAM policy — it grants four actions on
|
|
# one bucket and one distribution, nothing more. Rotate the key quarterly; OIDC
|
|
# would have made that unnecessary.
|
|
#
|
|
# Requires a Gitea Actions runner registered to this repo or its organisation.
|
|
|
|
name: Build and deploy
|
|
|
|
on:
|
|
push:
|
|
branches: [main]
|
|
workflow_dispatch:
|
|
|
|
concurrency:
|
|
group: deploy-production
|
|
cancel-in-progress: false
|
|
|
|
jobs:
|
|
build-and-deploy:
|
|
runs-on: ubuntu-latest
|
|
|
|
env:
|
|
AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
|
|
AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
|
|
AWS_DEFAULT_REGION: ${{ vars.AWS_REGION }}
|
|
S3_BUCKET: ${{ vars.S3_BUCKET }}
|
|
CLOUDFRONT_DISTRIBUTION_ID: ${{ vars.CLOUDFRONT_DISTRIBUTION_ID }}
|
|
# Job-level so the guard can see it. An empty INTAKE_ENDPOINT does not
|
|
# fail the build - it ships a live contact form posting to nothing.
|
|
INTAKE_ENDPOINT: ${{ vars.INTAKE_ENDPOINT }}
|
|
|
|
steps:
|
|
# Runs first, before checkout and before any AWS call, so a
|
|
# misconfiguration costs one second instead of a full build.
|
|
#
|
|
# Repository variables live at Settings -> Actions -> Variables. Gitea
|
|
# only added the `vars` context in 1.21; this instance reports 1.27.2
|
|
# [verified 2026-08-26 - /api/v1/version, AGENTS.md §7], so the guard is
|
|
# belt-and-braces rather than load-bearing. It stays because an unset or
|
|
# mistyped variable degrades the sync target to "s3://" and the run dies
|
|
# obscurely somewhere in the middle, whatever the Gitea version.
|
|
#
|
|
# Covers the deploy-target variables, the intake endpoint, AND the two
|
|
# secrets. The secrets matter most: AGENTS.md Q22 records that nobody has
|
|
# confirmed the IAM user or its key exists, so an unset key is the single
|
|
# likeliest first-run failure - and without this it would burn a whole
|
|
# build before dying at `aws sts get-caller-identity`.
|
|
#
|
|
# Only emptiness is ever tested. No value is echoed, so nothing here can
|
|
# leak a secret into the run log.
|
|
- name: Guard - required variables and secrets are set
|
|
run: |
|
|
missing=''
|
|
[ -n "$AWS_DEFAULT_REGION" ] || missing="$missing AWS_REGION(var)"
|
|
[ -n "$S3_BUCKET" ] || missing="$missing S3_BUCKET(var)"
|
|
[ -n "$CLOUDFRONT_DISTRIBUTION_ID" ] || missing="$missing CLOUDFRONT_DISTRIBUTION_ID(var)"
|
|
[ -n "$INTAKE_ENDPOINT" ] || missing="$missing INTAKE_ENDPOINT(var)"
|
|
[ -n "$AWS_ACCESS_KEY_ID" ] || missing="$missing AWS_ACCESS_KEY_ID(secret)"
|
|
[ -n "$AWS_SECRET_ACCESS_KEY" ] || missing="$missing AWS_SECRET_ACCESS_KEY(secret)"
|
|
if [ -n "$missing" ]; then
|
|
echo "Not set:$missing"
|
|
echo
|
|
echo 'Variables: Settings -> Actions -> Variables.'
|
|
echo 'Secrets: Settings -> Actions -> Secrets.'
|
|
echo 'See docs/06-deployment.md.'
|
|
echo 'If the variables ARE set, this Gitea predates the vars context (1.21+).'
|
|
exit 1
|
|
fi
|
|
echo 'All required variables and secrets are set.'
|
|
|
|
- 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
|
|
# vars, not env — Gitea expression-context support is the very thing
|
|
# the guard above exists to not depend on.
|
|
PUBLIC_INTAKE_ENDPOINT: ${{ vars.INTAKE_ENDPOINT }}
|
|
PUBLIC_BOOKING_URL: ${{ vars.BOOKING_URL }}
|
|
|
|
# Some Gitea runner images ship without the AWS CLI. Install if missing.
|
|
- name: Ensure AWS CLI
|
|
run: |
|
|
if ! command -v aws >/dev/null 2>&1; then
|
|
curl -fsSL "https://awscli.amazonaws.com/awscli-exe-linux-x86_64.zip" -o /tmp/awscliv2.zip
|
|
unzip -q /tmp/awscliv2.zip -d /tmp
|
|
sudo /tmp/aws/install --update
|
|
fi
|
|
aws --version
|
|
|
|
- name: Verify credentials
|
|
run: aws sts get-caller-identity
|
|
|
|
# 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://${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://${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://${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 "${CLOUDFRONT_DISTRIBUTION_ID}" \
|
|
--paths "/*"
|
|
|
|
- name: Summary
|
|
run: echo "Deployed to https://adr.smlcompany.ca — commit ${GITHUB_SHA:0:7}"
|