Merge branch 'main' into feat/ab/sh-382-extra-doc-view

This commit is contained in:
Alexandre Brandizzi 2026-09-18 18:57:56 -03:00 • committed by GitHub
commit e1b03dc151
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
12 changed files with 61 additions and 137 deletions

View file

@ -15,6 +15,10 @@ name: Deploy Web
#
# Bucket and distribution come from SSM after assuming the Environment's
# DEPLOY_ROLE_ARN. Nothing here creates an HCP run. Quality gates live in CI.
#
# The SPA checkout is the resolved content ref. Deploy scripts are copied
# from github.workflow_sha so workflow_dispatch of an older SHA still runs
# the current upload/verify path.
on:
push:
@ -131,6 +135,27 @@ jobs:
echo "sha=${sha}" >> "${GITHUB_OUTPUT}"
echo "Building ${sha}"
- name: Checkout workflow deploy scripts
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
ref: ${{ github.workflow_sha }}
persist-credentials: false
sparse-checkout: |
scripts
sparse-checkout-cone-mode: true
path: .workflow-scripts
- name: Install workflow deploy scripts
run: |
set -euo pipefail
test -f .workflow-scripts/scripts/upload-sourcemaps.sh
test -f .workflow-scripts/scripts/verify-cloudfront-release.sh
test -f .workflow-scripts/scripts/summarize-cloudfront-live-state.sh
mkdir -p scripts
cp .workflow-scripts/scripts/upload-sourcemaps.sh scripts/
cp .workflow-scripts/scripts/verify-cloudfront-release.sh scripts/
cp .workflow-scripts/scripts/summarize-cloudfront-live-state.sh scripts/
- uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
with:
node-version: "24"
@ -145,6 +170,14 @@ jobs:
VITE_SENTRY_RELEASE: ${{ steps.commit.outputs.sha }}
run: |
set -euo pipefail
# vars.VITE_SENTRY_DSN is unset today. An empty env value would
# override .env.production and disable Sentry (Vite does not let
# .env overwrite an existing variable).
if [ -n "${VITE_SENTRY_DSN:-}" ]; then
export VITE_SENTRY_DSN
else
unset VITE_SENTRY_DSN
fi
case "${TARGET_ENVIRONMENT}" in
dev)
export VITE_API_URL="https://api.dev.seahaven.com/api"

View file

@ -51,7 +51,7 @@ isolation). A task is not done until this is green.
are migration evidence reviewed by a human before an approved apply
(`terraform/README.md`). G13 fails a diff that contains both `terraform/`
and deployable application files (`src/`, `public/`, `pages/`, `config/`,
`index.html`, Vite/tsconfig, `.env*`, or `scripts/deploy-web.sh`). Workflow,
`index.html`, Vite/tsconfig, or `.env*`). Workflow,
docs, and gate-script changes may travel with either side. Runtime isolation
stays: `deploy-web.yaml` ignores `terraform/**`, and app-only tags skip HCP
when workspace trigger patterns miss.

View file

@ -87,6 +87,6 @@ surface; keep comments inline and high-signal.
Infra and application **PRs** stay separate. GitHub Actions owns SPA content
(`deploy-web.yaml`). HCP Terraform owns the bucket and CloudFront. A change set
that includes both `terraform/` and deployable application files (`src/`,
`public/`, `pages/`, `config/`, `index.html`, Vite/tsconfig, `.env*`, or
`scripts/deploy-web.sh`) fails G13. Workflow, docs, and gate-script changes may
travel with either side.
`public/`, `pages/`, `config/`, `index.html`, Vite/tsconfig, or `.env*`)
fails G13. Workflow, docs, and gate-script changes may travel with either
side.

View file

@ -9,10 +9,6 @@ from __future__ import annotations
import argparse
import sys
APP_SCRIPT_NAMES = {
"scripts/deploy-web.sh",
}
APP_ROOTS = (
"src/",
"public/",
@ -33,7 +29,7 @@ def is_terraform_path(path: str) -> bool:
def is_app_path(path: str) -> bool:
normalized = path.replace("\\", "/")
if normalized in APP_SCRIPT_NAMES or normalized in APP_FILES:
if normalized in APP_FILES:
return True
if normalized in {"src", "public", "pages", "config"}:
return True

View file

@ -1,79 +0,0 @@
#!/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."

View file

@ -1,29 +0,0 @@
#!/usr/bin/env python3
"""Read .release/current JSON from stdin and write GitHub Actions outputs."""
from __future__ import annotations
import json
import os
import sys
def main() -> int:
raw = sys.stdin.read().strip()
data = json.loads(raw) if raw else {}
current = data.get("current") or ""
previous = data.get("previous") or ""
output_path = os.environ["GITHUB_OUTPUT"]
with open(output_path, "a", encoding="utf-8") as handle:
handle.write(f"live_current={current}\n")
handle.write(f"live_previous={previous}\n")
print(
"Pointer live current="
+ (current or "<empty>")
+ " previous="
+ (previous or "<empty>")
)
return 0
if __name__ == "__main__":
raise SystemExit(main())

View file

@ -340,13 +340,13 @@ class ImportPlanCheckerTests(unittest.TestCase):
self.assertFalse((REPOSITORY / "terraform" / "versions.tf").exists())
self.assertFalse((REPOSITORY / "terraform" / "main.tf").exists())
def test_dev_adoption_complete_is_pinned_in_locals(self) -> None:
def test_adoption_complete_is_pinned_in_locals(self) -> None:
dev = (REPOSITORY / "terraform/live/dev/main.tf").read_text(encoding="utf-8")
staging = (REPOSITORY / "terraform/live/staging/main.tf").read_text(
encoding="utf-8"
)
self.assertRegex(dev, r"adoption_complete\s+= true")
self.assertRegex(staging, r"adoption_complete\s+= false")
self.assertRegex(staging, r"adoption_complete\s+= true")
self.assertNotIn('variable "adoption_complete"', dev)
self.assertNotIn('variable "environment"', dev)
self.assertNotIn('variable "release_version_label"', dev)

View file

@ -26,7 +26,6 @@ class IsolationTests(unittest.TestCase):
"src/app/routes.tsx",
"public/favicon.ico",
"index.html",
"scripts/deploy-web.sh",
]
)
)

View file

@ -14,6 +14,11 @@ fi
COMMIT_SHA="$(printf '%s' "${COMMIT_SHA}" | tr '[:upper:]' '[:lower:]')"
RELEASE="shoc-frontend@${COMMIT_SHA}"
if ! npm exec --no -- sentry-cli --version >/dev/null 2>&1; then
echo "sentry-cli is not in this SPA tree; skipping source-map upload"
exit 0
fi
npm exec --no -- sentry-cli sourcemaps upload \
--org "${SENTRY_ORG}" \
--project "${SENTRY_PROJECT}" \

View file

@ -7,10 +7,10 @@ to the bucket root and invalidates `/*`.
Creating, formatting, initializing with `-backend=false`, and validating these
files does not authorize an AWS, HCP Terraform, GitHub, or deployment
mutation. Live cutover waits for an explicit greenlight.
mutation.
Do not collapse these roots into one `terraform/` tree in this PR. Flattening
retargets two live HCP working directories and is its own change.
Do not collapse these roots into one `terraform/` tree. Flattening retargets
two live HCP working directories and is its own change.
## Fixed targets
@ -61,15 +61,14 @@ and gate-script changes may travel with either side. G13 is
`npm run test:terraform` and `npm run verify` wrap the same gates. They never
create an HCP run or touch AWS.
## Cutover (greenlight only)
## Workspaces
1. Keep HCP working directories `terraform/live/dev` and
`terraform/live/staging`. Dev VCS branch `main`. Staging tag regex
`^v[0-9]+\.[0-9]+\.[0-9]+-staging$`.
2. Auto-apply off. Apply the origin-path move for **dev** before any
`--delete` root sync.
3. Create GitHub Environment `dev` (staging already exists). Set
`DEPLOY_ROLE_ARN` on each.
4. Enable `deploy-web.yaml`. Then delete leftover `deploy.yml` /
`deploy-staging.yml` and repo `TF_API_TOKEN`, `TERRAFORM_CONTENT_CD_ENABLED`,
and `AWS_DEPLOY_ROLE_ARN`.
Dev (`shoc-frontend-new-dev`) watches `main` with working directory
`terraform/live/dev` and auto-apply on. Staging (`shoc-frontend-new-staging`)
watches tag regex `^v[0-9]+\.[0-9]+\.[0-9]+-staging$` with working directory
`terraform/live/staging` and auto-apply on. Merges to `main` do not apply
staging.
GitHub Environments `dev` and `staging` set `DEPLOY_ROLE_ARN` and allow `main`
plus tag `v*`. Promote staging with `gh release create vX.Y.Z-staging --target
main`.

View file

@ -1,7 +1,7 @@
# Live Terraform roots
`live/dev/` is the adopted HCP workspace `shoc-frontend-new-dev`.
`live/staging/` is `shoc-frontend-new-staging` (`adoption_complete = false`).
`live/staging/` is `shoc-frontend-new-staging` (`adoption_complete = true`).
Do not collapse these into one `terraform/` root in the same PR as application
CD. Flattening retargets two live HCP working directories and belongs in its

View file

@ -1,6 +1,6 @@
locals {
# Staging is not fully adopted. Pinned in code, never a workspace variable.
adoption_complete = false
# Controlled ownership transfer. Pinned in code, never a workspace variable.
adoption_complete = true
environment = "staging"
workspace_name = "shoc-frontend-new-staging"