name: Deploy Web # SPA CD. GitHub Actions syncs the committed placeholder to the S3 origin # bucket root, then invalidates CloudFront. Terraform owns the bucket and the # distribution and never touches content. Do not run a SPA production build. # # push to main -> GitHub Environment dev, at github.sha # workflow_dispatch -> GitHub Environment dev. The workflow file must be main. # inputs.ref selects the placeholder tree to publish. # Job steps are the workflow file, not scripts from that ref. # # Nothing here creates an HCP run. Prod is AP-12. on: push: branches: [main] paths-ignore: - "terraform/**" - "packages/api/**" - "packages/shared/**" - "docs/**" - "**/*.md" - "Dockerfile" - ".dockerignore" - "scripts/verify-api-health.sh" - "scripts/test-verify-api-health.sh" - "scripts/test-terraform-dev-only.py" - "scripts/patch-ecs-task-def.py" - ".github/workflows/deploy-api.yaml" - ".github/workflows/ci.yaml" workflow_dispatch: inputs: environment: description: "Target Environment" required: true type: choice options: [dev] ref: description: "Git ref to deploy (branch or SHA). Empty means the workflow ref." required: false type: string default: "" permissions: contents: read jobs: target: name: Resolve target runs-on: ubuntu-latest timeout-minutes: 5 outputs: environment: ${{ steps.resolve.outputs.environment }} ref: ${{ steps.resolve.outputs.ref }} steps: - id: resolve env: EVENT_NAME: ${{ github.event_name }} GITHUB_REF_NAME_IN: ${{ github.ref }} GITHUB_SHA_IN: ${{ github.sha }} INPUT_ENVIRONMENT: ${{ inputs.environment }} INPUT_REF: ${{ inputs.ref }} run: | set -euo pipefail case "${EVENT_NAME}" in push) if [ "${GITHUB_REF_NAME_IN}" != "refs/heads/main" ]; then echo "push deploys only run from main" >&2 exit 1 fi environment=dev ref="${GITHUB_SHA_IN}" ;; workflow_dispatch) if [ "${GITHUB_REF_NAME_IN}" != "refs/heads/main" ]; then echo "workflow_dispatch deploys only run from main" >&2 exit 1 fi environment="${INPUT_ENVIRONMENT:-dev}" if [ "${environment}" != "dev" ]; then echo "only GitHub Environment dev is allowed" >&2 exit 1 fi ref="${INPUT_REF:-${GITHUB_SHA_IN}}" ;; *) echo "unsupported event ${EVENT_NAME}" >&2 exit 1 ;; esac { echo "environment=${environment}" echo "ref=${ref}" } >> "${GITHUB_OUTPUT}" echo "Deploying ${ref} to ${environment}" deploy: name: Deploy SPA to ${{ needs.target.outputs.environment }} needs: target runs-on: ubuntu-latest timeout-minutes: 30 environment: ${{ needs.target.outputs.environment }} concurrency: group: deploy-web-${{ needs.target.outputs.environment }} cancel-in-progress: false permissions: contents: read id-token: write env: AWS_REGION: us-east-1 DEPLOY_ROLE_ARN: ${{ vars.DEPLOY_ROLE_ARN }} steps: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: ref: ${{ needs.target.outputs.ref }} persist-credentials: false - name: Resolve commit id: commit run: | set -euo pipefail sha="$(git rev-parse HEAD)" echo "sha=${sha}" >> "${GITHUB_OUTPUT}" echo "Deploying ${sha}" test -f placeholder/index.html - name: Require deploy role run: | set -euo pipefail if [ -z "${DEPLOY_ROLE_ARN}" ]; then echo "DEPLOY_ROLE_ARN is empty. Create GitHub Environment dev and set it after the seahaven-ap-dev apply, then re-run." >&2 exit 1 fi - name: Configure AWS credentials using OIDC uses: aws-actions/configure-aws-credentials@e1253824e5c10ff9df46874f81ed3ec929e19cfd # v6.3.0 with: role-to-assume: ${{ env.DEPLOY_ROLE_ARN }} aws-region: us-east-1 audience: sts.amazonaws.com - name: Get deploy parameters id: deploy run: | set -euo pipefail get_param() { local name="$1" err value err="$(mktemp)" if ! value="$(aws ssm get-parameter --name "${name}" --query Parameter.Value --output text 2>"${err}")"; then if grep -q ParameterNotFound "${err}"; then echo "SSM parameter ${name} does not exist yet. Apply the seahaven-ap-dev workspace, then re-run this workflow." >&2 else cat "${err}" >&2 fi rm -f "${err}" exit 1 fi rm -f "${err}" printf '%s\n' "${value}" } BUCKET=$(get_param /seahaven-ap/deploy/bucket) DIST_ID=$(get_param /seahaven-ap/deploy/distribution-id) DOMAIN=$(aws cloudfront get-distribution --id "${DIST_ID}" --query Distribution.DomainName --output text) { echo "bucket=${BUCKET}" echo "distribution_id=${DIST_ID}" echo "site_url=https://${DOMAIN}" } >> "${GITHUB_OUTPUT}" - name: Sync placeholder/ to the bucket root env: SITE_BUCKET: ${{ steps.deploy.outputs.bucket }} run: | set -euo pipefail aws s3 sync placeholder/ "s3://${SITE_BUCKET}/" \ --exclude "index.html" \ --cache-control "public,max-age=31536000,immutable" aws s3 cp placeholder/index.html "s3://${SITE_BUCKET}/index.html" \ --cache-control "no-cache,no-store,must-revalidate" \ --content-type "text/html" aws s3 sync placeholder/ "s3://${SITE_BUCKET}/" \ --delete \ --exclude "index.html" \ --cache-control "public,max-age=31536000,immutable" aws s3api head-object --bucket "${SITE_BUCKET}" --key index.html - name: Invalidate CloudFront env: DISTRIBUTION_ID: ${{ steps.deploy.outputs.distribution_id }} run: | set -euo pipefail invalidation_id="$(aws cloudfront create-invalidation \ --distribution-id "${DISTRIBUTION_ID}" \ --paths "/*" \ --query Invalidation.Id --output text)" echo "Invalidation ${invalidation_id} created; waiting" aws cloudfront wait invalidation-completed \ --distribution-id "${DISTRIBUTION_ID}" \ --id "${invalidation_id}"