Files
adr-sml/scripts/deploy-local.sh
T
Pouya LajevardiandClaude Opus 5 7514a49803 feat: upgrade to Astro 7; harden the content schema; wire a11y linting
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
2026-08-26 14:10:09 -04:00

96 lines
3.6 KiB
Bash
Executable File

#!/usr/bin/env bash
#
# Local deploy — the fallback while Gitea Actions is unavailable.
#
# Gitea Actions needs `[actions] ENABLED = true` in app.ini and a registered
# act_runner. The instance is jointly administered, so both depend on a second
# administrator (AGENTS.md Q23). Until that lands, this script is how the site
# ships.
#
# It matches .gitea/workflows/deploy.yml on everything that determines what gets
# published: the same guard coverage, `npm run check` before the build, the same
# three sync passes in the same order with the same cache headers, and the same
# invalidation. Any change to one must be made to the other.
#
# Two deliberate differences: it does not run `npm ci` (your node_modules is
# already installed, and CI starts empty), and it refuses to run as user/pouya,
# which CI cannot do because CI has no such credential.
#
# Required environment (values are in AGENTS.md §7 — deliberately not restated
# here; §7 is the single source of truth for operational facts):
#
# AWS_REGION S3_BUCKET CLOUDFRONT_DISTRIBUTION_ID INTAKE_ENDPOINT
#
# Credentials: use the scoped deploy user. AGENTS.md Q22 records that it does
# NOT yet exist. NEVER run this as user/pouya — see AGENTS.md §10.
set -euo pipefail
# Same six values the workflow guards. Emptiness only — no value is echoed.
missing=''
[ -n "${AWS_REGION:-}" ] || missing="$missing AWS_REGION"
[ -n "${S3_BUCKET:-}" ] || missing="$missing S3_BUCKET"
[ -n "${CLOUDFRONT_DISTRIBUTION_ID:-}" ] || missing="$missing CLOUDFRONT_DISTRIBUTION_ID"
[ -n "${INTAKE_ENDPOINT:-}" ] || missing="$missing INTAKE_ENDPOINT"
[ -n "${AWS_ACCESS_KEY_ID:-}" ] || missing="$missing AWS_ACCESS_KEY_ID"
[ -n "${AWS_SECRET_ACCESS_KEY:-}" ] || missing="$missing AWS_SECRET_ACCESS_KEY"
if [ -n "$missing" ]; then
echo "Not set:$missing" >&2
echo >&2
echo "Values are in AGENTS.md §7. An empty INTAKE_ENDPOINT does not fail the" >&2
echo "build — it ships a live contact form posting to nothing." >&2
exit 1
fi
export AWS_DEFAULT_REGION="$AWS_REGION"
echo "==> Identity check"
caller=$(aws sts get-caller-identity --query Arn --output text)
echo " $caller"
case "$caller" in
*:user/pouya)
echo >&2
echo "REFUSING: that is the broadly-permissioned personal user." >&2
echo "AGENTS.md §10 — never use user/pouya to deploy. Use the scoped" >&2
echo "deploy user (Q22: not yet created)." >&2
exit 1
;;
esac
echo "==> Type and template check"
npm run check
echo "==> Build"
PUBLIC_SITE_URL="https://adr.smlcompany.ca" \
PUBLIC_INTAKE_ENDPOINT="$INTAKE_ENDPOINT" \
PUBLIC_BOOKING_URL="${BOOKING_URL:-}" \
npm run build
echo "==> Pass 1/3 — hashed assets and fonts (immutable)"
aws s3 sync ./dist "s3://${S3_BUCKET}" \
--exclude "*" \
--include "_astro/*" --include "fonts/*" \
--cache-control "public, max-age=31536000, immutable" \
--no-progress
echo "==> Pass 2/3 — images"
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
echo "==> Pass 3/3 — HTML and the rest (must-revalidate, --delete)"
aws s3 sync ./dist "s3://${S3_BUCKET}" \
--exclude "_astro/*" --exclude "fonts/*" \
--cache-control "public, max-age=0, must-revalidate" \
--delete --no-progress
echo "==> Invalidate CloudFront"
aws cloudfront create-invalidation \
--distribution-id "${CLOUDFRONT_DISTRIBUTION_ID}" \
--paths "/*" >/dev/null
echo "==> Deployed to https://adr.smlcompany.ca ($(git rev-parse --short HEAD))"