From 87e79072ad6dcfa04abacfac5414161958b4a0dd Mon Sep 17 00:00:00 2001 From: Adam Moussa Date: Thu, 10 Sep 2026 19:15:12 -0400 Subject: [PATCH] ci(deploy): make dev content deploy workflow_dispatch only Remove the push-to-dev trigger and the org cd-cdk.yaml caller so CI no longer runs cdk deploy during the adoption. The workflow assumes the pinned dev role and runs the simple scripts/deploy-web.sh against a pinned bucket and distribution, which keeps content deploys working after CloudFormation relinquishes the stack outputs. Staging is untouched. --- .github/workflows/deploy.yml | 104 +++++++++++++++++++++++++---------- scripts/deploy-web.sh | 52 +++++++++++------- 2 files changed, 108 insertions(+), 48 deletions(-) diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index 6ad9bde2..099b55de 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -1,22 +1,22 @@ -name: Deploy +name: Deploy dev content -# Continuous deployment to AWS (S3 + CloudFront) on push to `dev`. +# Manual dev content deployment during the Terraform adoption (SH-300). # -# This is a thin caller of the org's reusable CD workflow. `cd-cdk.yaml` runs -# `cdk deploy` (provisioning the infra in infra/cdk) and then the -# post-deploy-script, which builds the SPA and syncs it to S3 + invalidates -# CloudFront. Both run as the OIDC deploy role created by the stack. +# The push-to-`dev` trigger and the org reusable `cd-cdk.yaml` caller are +# retired: `cdk deploy` no longer runs from CI. Infrastructure changes are +# administrator-run (`infra/cdk/README.md`) while CloudFormation still owns the +# resources, and move to HCP Terraform (`terraform/README.md`) as adoption +# completes. Automatic push-to-`dev` releases return with the Terraform +# content-CD change, gated on a repository variable. # -# When staging/prod accounts exist, add jobs keyed to their branches and their -# own AWS_DEPLOY_ROLE_ARN, reusing this same reusable workflow. +# This workflow publishes only content: verify, build, `aws s3 sync`, and a +# CloudFront invalidation through `scripts/deploy-web.sh`, as the pinned OIDC +# deploy role. The bucket and distribution are pinned here so a content deploy +# keeps working after CloudFormation relinquishes the stack outputs. on: - push: - branches: [dev] workflow_dispatch: {} -# OIDC needs id-token: write — it is never in the default token set and cannot -# be granted to the reusable workflow unless the caller has it. permissions: id-token: write contents: read @@ -27,22 +27,13 @@ concurrency: jobs: deploy: - uses: Sea-Haven-Industries/.github/.github/workflows/cd-cdk.yaml@af0f002e14a08cdbfd879c1183bfe7eb2604bce9 # v1.0.8 - with: - node-version: "24" - region: us-east-1 - cdk-dir: infra/cdk - stack-name: shoc-frontend-dev - post-deploy-script: scripts/deploy-web.sh - secrets: - deploy-role-arn: ${{ secrets.AWS_DEPLOY_ROLE_ARN }} - - upload-sourcemaps: - name: Upload private source maps - needs: deploy + name: Publish content to dev + # Deploy only the exact dev branch ref: workflow_dispatch can be invoked + # from arbitrary refs, and the deploy role trusts only refs/heads/dev. if: github.ref == 'refs/heads/dev' runs-on: ubuntu-latest env: + AWS_REGION: us-east-1 VITE_APP_COMMIT_SHA: ${{ github.sha }} steps: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 @@ -52,9 +43,66 @@ jobs: with: node-version: "24" cache: npm - - name: Build exact deployed release - run: npm ci && npm run build - - name: Upload source maps to Sentry + - name: Set up Terraform + # Required by `npm run verify` (governance runs terraform fmt/validate). + uses: hashicorp/setup-terraform@dfe3c3f87815947d99a8997f908cb6525fc44e9e # v4.0.1 + with: + terraform_version: "1.16.0" + terraform_wrapper: false + - name: Quality gates (full verify before any deploy) + run: npm ci && npm run verify + env: + GOVERNANCE_BASE: origin/dev + + - name: Assume dev deploy role (OIDC) + uses: aws-actions/configure-aws-credentials@e6de054238d6b7531b4efff3b6587d9aade6a06c # v6.2.3 + with: + role-to-assume: arn:aws:iam::396287094661:role/githubdeploy-shoc-frontend-new-dev + aws-region: us-east-1 + + # Builds with the dev values committed in .env.production (VITE_API_URL, + # Sentry DSN), syncs to the pinned bucket, and invalidates CloudFront. + - name: Build and publish SPA + run: bash scripts/deploy-web.sh + env: + SITE_BUCKET: seahaven-shoc-frontend-dev + CLOUDFRONT_DISTRIBUTION_ID: E2CWLM1AFB964P + WAIT_FOR_INVALIDATION: "true" + + - name: Upload private source maps run: bash scripts/upload-sourcemaps.sh env: SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }} + + - name: Verify deployment + run: | + set -euo pipefail + SITE_URL="https://dev.seahaven.com" + if grep -Rq "api.staging.seahaven.com" dist/; then + echo "::error::Built assets contain the staging API URL." >&2 + exit 1 + fi + grep -Rq "api.dev.seahaven.com" dist/ + echo "Built assets reference the dev API URL." + + # The invalidation has completed, but give edges a short window to + # converge before calling the served index.html wrong. + remote_dir="$(mktemp -d)" + trap 'rm -rf "${remote_dir}"' EXIT + matched=false + for i in 1 2 3 4 5 6; do + if curl -fsS --max-time 30 "${SITE_URL}" -o "${remote_dir}/index.html" \ + && cmp -s dist/index.html "${remote_dir}/index.html"; then + matched=true + break + fi + echo "Served index.html does not yet match the published build (attempt ${i}); retrying in 20s..." + sleep 20 + done + if [[ "${matched}" != "true" ]]; then + echo "::error::Served index.html does not match the build just published." >&2 + exit 1 + fi + echo "Served index.html matches the published build." + curl -fsS --max-time 30 -o /dev/null "${SITE_URL}/login" + echo "Extensionless SPA route serves." diff --git a/scripts/deploy-web.sh b/scripts/deploy-web.sh index 2dfb66a2..ec64a742 100755 --- a/scripts/deploy-web.sh +++ b/scripts/deploy-web.sh @@ -1,14 +1,16 @@ #!/usr/bin/env bash # -# Post-deploy step for the org reusable workflow `cd-cdk.yaml` -# (wired in via `.github/workflows/deploy.yml` -> `post-deploy-script`). +# Content publish step for the environment deploy workflows +# (`.github/workflows/deploy.yml`, `.github/workflows/deploy-staging.yml`). # -# Runs AFTER `cdk deploy` has provisioned/updated the infra, as the GitHub -# OIDC deploy role. Builds the SPA, uploads it to the stack's S3 bucket with -# the right cache headers, and invalidates CloudFront. +# 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. Reads the bucket + distribution from stack outputs, -# so it has no hardcoded resource IDs. +# 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}" @@ -20,21 +22,31 @@ export VITE_APP_COMMIT_SHA="${VITE_APP_COMMIT_SHA:-${GITHUB_SHA:-}}" npm ci npm run build -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="${SITE_BUCKET:-}" +DIST_ID="${CLOUDFRONT_DISTRIBUTION_ID:-}" -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 +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}..."