mirror of
https://github.com/Sea-Haven-Industries/shoc-frontend-new.git
synced 2026-09-30 19:43:12 +00:00
Some checks are pending
Dev events bake shoc-frontend@<sha>. Stop tagging artifacts with Terraform version_label so Sentry can symbolicate them.
300 lines
13 KiB
YAML
300 lines
13 KiB
YAML
name: Deploy dev content
|
|
|
|
# Dev content CD through Terraform (SH-300). GitHub uploads an immutable
|
|
# releases/<sha>-<run>-<attempt>/ prefix. Terraform owns the pointer, origin
|
|
# group, and invalidation. Push-to-dev stays off until
|
|
# vars.TERRAFORM_CONTENT_CD_ENABLED is the string true.
|
|
#
|
|
# Quality gates live in Frontend checks (`ci.yaml`). This workflow does not
|
|
# re-run those gates on pull requests, pushes, or workflow_dispatch.
|
|
|
|
on:
|
|
push:
|
|
branches: [dev]
|
|
paths-ignore:
|
|
- "terraform/**"
|
|
workflow_dispatch: {}
|
|
|
|
permissions:
|
|
contents: read
|
|
|
|
jobs:
|
|
deploy-dev:
|
|
name: Deploy shoc-frontend-new-dev through Terraform
|
|
if: >
|
|
(github.event_name == 'push' && github.ref == 'refs/heads/dev' &&
|
|
vars.TERRAFORM_CONTENT_CD_ENABLED == 'true') ||
|
|
(github.event_name == 'workflow_dispatch' && github.ref == 'refs/heads/dev')
|
|
runs-on: ubuntu-latest
|
|
timeout-minutes: 180
|
|
permissions:
|
|
contents: read
|
|
id-token: write
|
|
concurrency:
|
|
group: deploy-dev
|
|
cancel-in-progress: false
|
|
env:
|
|
AWS_REGION: us-east-1
|
|
TF_CLOUD_ORGANIZATION: seahaven
|
|
TF_API_TOKEN: ${{ secrets.TF_API_TOKEN }}
|
|
SITE_BUCKET: seahaven-shoc-frontend-dev
|
|
DISTRIBUTION_ID: E2CWLM1AFB964P
|
|
SITE_URL: https://dev.seahaven.com
|
|
VITE_API_URL: https://api.dev.seahaven.com/api
|
|
VITE_APP_COMMIT_SHA: ${{ github.sha }}
|
|
steps:
|
|
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
|
|
|
- uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
|
|
with:
|
|
node-version: "24"
|
|
cache: npm
|
|
|
|
- name: Build SPA
|
|
run: |
|
|
set -euo pipefail
|
|
npm ci
|
|
npm run build
|
|
if grep -Rq "api.staging.seahaven.com" dist/; then
|
|
echo "::error::Built assets contain the staging API URL." >&2
|
|
exit 1
|
|
fi
|
|
if grep -Rq "localhost:5141" dist/; then
|
|
echo "::error::Built assets contain the Vite proxy target localhost:5141." >&2
|
|
exit 1
|
|
fi
|
|
grep -Rq "api.dev.seahaven.com" dist/
|
|
|
|
- name: Configure AWS credentials (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
|
|
audience: sts.amazonaws.com
|
|
|
|
- name: Assign immutable release identity
|
|
id: release
|
|
run: |
|
|
set -euo pipefail
|
|
version_label="${GITHUB_SHA}-${GITHUB_RUN_ID}-${GITHUB_RUN_ATTEMPT}"
|
|
prefix="releases/${version_label}"
|
|
{
|
|
echo "version_label=${version_label}"
|
|
echo "prefix=${prefix}"
|
|
} >> "${GITHUB_OUTPUT}"
|
|
|
|
# Sentry release is shoc-frontend@${GITHUB_SHA} via VITE_APP_COMMIT_SHA,
|
|
# distinct from the S3/Terraform version_label.
|
|
- name: Upload private source maps
|
|
run: bash scripts/upload-sourcemaps.sh
|
|
env:
|
|
SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }}
|
|
|
|
- name: Read previous release pointer
|
|
id: pointer
|
|
run: |
|
|
set -euo pipefail
|
|
body="$(aws s3 cp "s3://${SITE_BUCKET}/.release/current" - --only-show-errors || true)"
|
|
printf '%s' "${body}" | python3 scripts/read-release-pointer.py
|
|
|
|
- name: Upload immutable release prefix
|
|
run: |
|
|
set -euo pipefail
|
|
prefix="${{ steps.release.outputs.prefix }}"
|
|
aws s3 sync dist/ "s3://${SITE_BUCKET}/${prefix}/" \
|
|
--exclude "index.html" \
|
|
--exclude "*.map" \
|
|
--cache-control "public,max-age=31536000,immutable"
|
|
aws s3 cp dist/index.html "s3://${SITE_BUCKET}/${prefix}/index.html" \
|
|
--cache-control "no-cache,no-store,must-revalidate" \
|
|
--content-type "text/html"
|
|
aws s3api head-object \
|
|
--bucket "${SITE_BUCKET}" \
|
|
--key "${prefix}/index.html"
|
|
index_sha="$(python3 -c 'import hashlib,pathlib; print(hashlib.sha256(pathlib.Path("dist/index.html").read_bytes()).hexdigest())')"
|
|
echo "INDEX_SHA256=${index_sha}" >> "${GITHUB_ENV}"
|
|
echo "Uploaded ${prefix}; index.html sha256=${index_sha}"
|
|
|
|
- name: Capture previous served hash
|
|
id: previous-hash
|
|
run: |
|
|
set -euo pipefail
|
|
hash="$(curl -fsS --max-time 30 "${SITE_URL}/" | python3 -c 'import hashlib,sys; print(hashlib.sha256(sys.stdin.buffer.read()).hexdigest())' || true)"
|
|
echo "sha256=${hash}" >> "${GITHUB_OUTPUT}"
|
|
|
|
- name: Discard blocking VCS run before GitHub CD
|
|
id: discard-vcs
|
|
env:
|
|
TF_API_TOKEN: ${{ secrets.TF_API_TOKEN }}
|
|
run: python3 scripts/hcp-run-guard.py check-and-discard --workspace shoc-frontend-new-dev
|
|
|
|
- name: Create Terraform release run
|
|
id: release-run
|
|
uses: hashicorp/tfc-workflows-github/actions/create-run@8e08d1ba957673f5fbf971a22b3219639dc45661 # v1.3.2
|
|
env:
|
|
TF_VAR_release_version_label: '"${{ steps.release.outputs.version_label }}"'
|
|
TF_VAR_previous_release_version_label: '"${{ steps.pointer.outputs.live_current }}"'
|
|
with:
|
|
workspace: shoc-frontend-new-dev
|
|
message: "Release ${{ steps.release.outputs.version_label }} from GitHub Actions"
|
|
|
|
- name: Read Terraform release plan counts
|
|
id: release-plan
|
|
uses: hashicorp/tfc-workflows-github/actions/plan-output@8e08d1ba957673f5fbf971a22b3219639dc45661 # v1.3.2
|
|
with:
|
|
plan: ${{ steps.release-run.outputs.plan_id }}
|
|
|
|
- name: Reject non-release resource counts
|
|
env:
|
|
PLAN_ADD: ${{ steps.release-plan.outputs.add }}
|
|
PLAN_CHANGE: ${{ steps.release-plan.outputs.change }}
|
|
PLAN_DESTROY: ${{ steps.release-plan.outputs.destroy }}
|
|
run: |
|
|
set -euo pipefail
|
|
if [ "$PLAN_ADD" != "0" ] || [ "$PLAN_CHANGE" != "2" ] || [ "$PLAN_DESTROY" != "0" ]; then
|
|
echo "HCP plan counts are add=${PLAN_ADD} change=${PLAN_CHANGE} destroy=${PLAN_DESTROY}; expected 0/2/0." >&2
|
|
exit 1
|
|
fi
|
|
|
|
- name: Guard pointer-and-origin-path Terraform plan
|
|
run: |
|
|
set -euo pipefail
|
|
# Flags must match check-terraform-release-plan.py. Pointer `before`
|
|
# and origin-ID-set stability are asserted from the plan JSON.
|
|
python3 scripts/check-terraform-release-plan.py \
|
|
--plan-id "${{ steps.release-run.outputs.plan_id }}" \
|
|
--expected-version-label "${{ steps.release.outputs.version_label }}" \
|
|
--expected-previous-version-label "${{ steps.pointer.outputs.live_current }}"
|
|
|
|
- name: Discard release run when the guard fails
|
|
if: failure() && steps.release-run.outcome == 'success'
|
|
uses: hashicorp/tfc-workflows-github/actions/discard-run@8e08d1ba957673f5fbf971a22b3219639dc45661 # v1.3.2
|
|
with:
|
|
run: ${{ steps.release-run.outputs.run_id }}
|
|
comment: Rejected by the pointer-and-origin-path plan guard from GitHub Actions
|
|
|
|
- name: Apply Terraform release run
|
|
id: release-apply
|
|
continue-on-error: true
|
|
uses: hashicorp/tfc-workflows-github/actions/apply-run@8e08d1ba957673f5fbf971a22b3219639dc45661 # v1.3.2
|
|
with:
|
|
run: ${{ steps.release-run.outputs.run_id }}
|
|
comment: Apply pointer-and-origin-path release from GitHub Actions ${{ github.sha }}
|
|
|
|
- name: Treat already-applied release run as success
|
|
env:
|
|
TF_API_TOKEN: ${{ secrets.TF_API_TOKEN }}
|
|
run: |
|
|
python3 scripts/hcp-run-guard.py reconcile-apply \
|
|
--run-id "${{ steps.release-run.outputs.run_id }}" \
|
|
--apply-outcome "${{ steps.release-apply.outcome }}"
|
|
|
|
- name: Verify CloudFront release
|
|
env:
|
|
EXPECTED_LABEL: ${{ steps.release.outputs.version_label }}
|
|
EXPECTED_INDEX_SHA256: ${{ env.INDEX_SHA256 }}
|
|
PREVIOUS_INDEX_SHA256: ${{ steps.previous-hash.outputs.sha256 }}
|
|
run: bash scripts/verify-cloudfront-release.sh
|
|
|
|
- name: Restore previous release on failure
|
|
if: failure()
|
|
id: rollback-prepare
|
|
run: |
|
|
set -euo pipefail
|
|
prev="${{ steps.pointer.outputs.live_current }}"
|
|
if [[ ! "${prev}" =~ ^[0-9a-f]{40}-[0-9]+-[0-9]+$ ]]; then
|
|
echo "No Terraform-managed previous label; cannot roll back through HCP." >&2
|
|
exit 0
|
|
fi
|
|
echo "rollback_label=${prev}" >> "${GITHUB_OUTPUT}"
|
|
echo "rollback_previous=${{ steps.release.outputs.version_label }}" >> "${GITHUB_OUTPUT}"
|
|
|
|
- name: Discard blocking VCS run before GitHub rollback
|
|
id: rollback-discard-vcs
|
|
if: failure() && steps.rollback-prepare.outputs.rollback_label != ''
|
|
env:
|
|
TF_API_TOKEN: ${{ secrets.TF_API_TOKEN }}
|
|
run: python3 scripts/hcp-run-guard.py check-and-discard --workspace shoc-frontend-new-dev
|
|
|
|
- name: Create Terraform rollback run
|
|
id: rollback-run
|
|
if: failure() && steps.rollback-prepare.outputs.rollback_label != '' && steps.rollback-discard-vcs.outcome == 'success'
|
|
uses: hashicorp/tfc-workflows-github/actions/create-run@8e08d1ba957673f5fbf971a22b3219639dc45661 # v1.3.2
|
|
env:
|
|
TF_VAR_release_version_label: '"${{ steps.rollback-prepare.outputs.rollback_label }}"'
|
|
TF_VAR_previous_release_version_label: '"${{ steps.rollback-prepare.outputs.rollback_previous }}"'
|
|
with:
|
|
workspace: shoc-frontend-new-dev
|
|
message: "Rollback to ${{ steps.rollback-prepare.outputs.rollback_label }} from GitHub Actions"
|
|
|
|
- name: Read Terraform rollback plan counts
|
|
id: rollback-plan
|
|
if: failure() && steps.rollback-run.outcome == 'success'
|
|
uses: hashicorp/tfc-workflows-github/actions/plan-output@8e08d1ba957673f5fbf971a22b3219639dc45661 # v1.3.2
|
|
with:
|
|
plan: ${{ steps.rollback-run.outputs.plan_id }}
|
|
|
|
- name: Reject non-release rollback counts
|
|
id: rollback-count-guard
|
|
if: failure() && steps.rollback-plan.outcome == 'success'
|
|
env:
|
|
PLAN_ADD: ${{ steps.rollback-plan.outputs.add }}
|
|
PLAN_CHANGE: ${{ steps.rollback-plan.outputs.change }}
|
|
PLAN_DESTROY: ${{ steps.rollback-plan.outputs.destroy }}
|
|
run: |
|
|
set -euo pipefail
|
|
if [ "$PLAN_ADD" != "0" ] || [ "$PLAN_CHANGE" != "2" ] || [ "$PLAN_DESTROY" != "0" ]; then
|
|
echo "Rollback HCP plan counts are add=${PLAN_ADD} change=${PLAN_CHANGE} destroy=${PLAN_DESTROY}; expected 0/2/0." >&2
|
|
exit 1
|
|
fi
|
|
|
|
- name: Guard pointer-and-origin-path Terraform rollback plan
|
|
id: rollback-json-guard
|
|
if: failure() && steps.rollback-count-guard.outcome == 'success'
|
|
run: |
|
|
set -euo pipefail
|
|
python3 scripts/check-terraform-release-plan.py \
|
|
--plan-id "${{ steps.rollback-run.outputs.plan_id }}" \
|
|
--expected-version-label "${{ steps.rollback-prepare.outputs.rollback_label }}" \
|
|
--expected-previous-version-label "${{ steps.rollback-prepare.outputs.rollback_previous }}"
|
|
|
|
- name: Discard rollback run when the guard fails
|
|
if: failure() && steps.rollback-run.outcome == 'success' && steps.rollback-json-guard.outcome != 'success'
|
|
uses: hashicorp/tfc-workflows-github/actions/discard-run@8e08d1ba957673f5fbf971a22b3219639dc45661 # v1.3.2
|
|
with:
|
|
run: ${{ steps.rollback-run.outputs.run_id }}
|
|
comment: Rejected by the pointer-and-origin-path rollback plan guard from GitHub Actions
|
|
|
|
- name: Apply Terraform rollback run
|
|
id: rollback-apply
|
|
if: failure() && steps.rollback-json-guard.outcome == 'success'
|
|
continue-on-error: true
|
|
uses: hashicorp/tfc-workflows-github/actions/apply-run@8e08d1ba957673f5fbf971a22b3219639dc45661 # v1.3.2
|
|
with:
|
|
run: ${{ steps.rollback-run.outputs.run_id }}
|
|
comment: Apply pointer-and-origin-path rollback from GitHub Actions ${{ github.sha }}
|
|
|
|
- name: Treat already-applied rollback run as success
|
|
id: rollback-apply-result
|
|
if: failure() && steps.rollback-apply.outcome != 'skipped'
|
|
env:
|
|
TF_API_TOKEN: ${{ secrets.TF_API_TOKEN }}
|
|
run: |
|
|
python3 scripts/hcp-run-guard.py reconcile-apply \
|
|
--run-id "${{ steps.rollback-run.outputs.run_id }}" \
|
|
--apply-outcome "${{ steps.rollback-apply.outcome }}"
|
|
|
|
- name: Verify CloudFront rollback
|
|
if: failure() && steps.rollback-apply-result.outcome == 'success'
|
|
env:
|
|
EXPECTED_LABEL: ${{ steps.rollback-prepare.outputs.rollback_label }}
|
|
run: |
|
|
set -euo pipefail
|
|
expected_sha="$(aws s3 cp "s3://${SITE_BUCKET}/releases/${EXPECTED_LABEL}/index.html" - | python3 -c 'import hashlib,sys; print(hashlib.sha256(sys.stdin.buffer.read()).hexdigest())')"
|
|
export EXPECTED_INDEX_SHA256="${expected_sha}"
|
|
bash scripts/verify-cloudfront-release.sh
|
|
|
|
- name: Live-state summary
|
|
if: always()
|
|
continue-on-error: true
|
|
run: bash scripts/summarize-cloudfront-live-state.sh
|