# 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}"