#!/usr/bin/env bash # # Content publish step for the environment deploy workflows # (`.github/workflows/deploy.yml`, `.github/workflows/deploy-staging.yml`). # # Runs as the GitHub OIDC deploy role. Builds the SPA, uploads it to the # environment's S3 bucket with the right cache headers, and invalidates # CloudFront. It never touches infrastructure. # # Runs from the repo root. The target is resolved from, in order: # 1. SITE_BUCKET + CLOUDFRONT_DISTRIBUTION_ID (pinned by the workflow; used by # dev, whose CloudFormation outputs disappear during Terraform adoption) # 2. the BucketName/DistributionId outputs of STACK_NAME (staging) set -euo pipefail STACK_NAME="${STACK_NAME:-shoc-frontend-dev}" REGION="${AWS_REGION:-us-east-1}" WAIT_FOR_INVALIDATION="${WAIT_FOR_INVALIDATION:-false}" echo "Building SPA (VITE_API_URL comes from the process environment or .env.production)..." export VITE_APP_COMMIT_SHA="${VITE_APP_COMMIT_SHA:-${GITHUB_SHA:-}}" npm ci npm run build BUCKET="${SITE_BUCKET:-}" DIST_ID="${CLOUDFRONT_DISTRIBUTION_ID:-}" if [[ -n "${BUCKET}" && -n "${DIST_ID}" ]]; then echo "Using pinned target: bucket ${BUCKET}, distribution ${DIST_ID}." elif [[ -n "${BUCKET}" || -n "${DIST_ID}" ]]; then echo "::error::Set both SITE_BUCKET and CLOUDFRONT_DISTRIBUTION_ID, or neither." >&2 exit 1 else echo "Reading stack outputs from ${STACK_NAME}..." stack_output() { aws cloudformation describe-stacks \ --stack-name "${STACK_NAME}" \ --region "${REGION}" \ --query "Stacks[0].Outputs[?OutputKey=='$1'].OutputValue" \ --output text } BUCKET="$(stack_output BucketName)" DIST_ID="$(stack_output DistributionId)" if [[ -z "${BUCKET}" || "${BUCKET}" == "None" || -z "${DIST_ID}" || "${DIST_ID}" == "None" ]]; then echo "::error::Could not resolve BucketName/DistributionId from stack ${STACK_NAME}." >&2 exit 1 fi fi echo "Uploading hashed assets (immutable) to s3://${BUCKET}..." # Everything except index.html: long-lived + immutable, prune stale objects. aws s3 sync dist/ "s3://${BUCKET}/" \ --delete \ --exclude "index.html" \ --exclude "*.map" \ --cache-control "public,max-age=31536000,immutable" echo "Uploading index.html (never cached)..." aws s3 cp dist/index.html "s3://${BUCKET}/index.html" \ --cache-control "no-cache,no-store,must-revalidate" \ --content-type "text/html" echo "Invalidating CloudFront ${DIST_ID}..." INVALIDATION_ID="$(aws cloudfront create-invalidation \ --distribution-id "${DIST_ID}" \ --paths "/*" \ --query 'Invalidation.Id' \ --output text)" if [[ "${WAIT_FOR_INVALIDATION}" == "true" ]]; then echo "Waiting for CloudFront invalidation ${INVALIDATION_ID}..." aws cloudfront wait invalidation-completed \ --distribution-id "${DIST_ID}" \ --id "${INVALIDATION_ID}" fi echo "Web deploy complete."