Skip to content

build(deps-dev): Bump @types/node from 20.19.0 to 26.1.1 in /frontend… #20

build(deps-dev): Bump @types/node from 20.19.0 to 26.1.1 in /frontend…

build(deps-dev): Bump @types/node from 20.19.0 to 26.1.1 in /frontend… #20

name: Release staging frontend to CloudFront
# Phase 4a Slice 5 — builds the React+Vite SPA bundle, uploads to S3,
# invalidates CloudFront edge cache. ADR-0027 documents the strategy.
#
# Trust scope (per ldz #90 + #91): GitHub OIDC role
# `github-actions-aegis-core-frontend` accepts only:
# sub = repo:BinHsu/aegis-core:ref:refs/heads/main
# job_workflow_ref = BinHsu/aegis-core/.github/workflows/release-staging-frontend.yml@refs/heads/main
# (latter pinned by ldz on the IAM trust policy;
# same pattern as release-staging-image.yml)
#
# Workflow does NOT trigger on PRs by design. PRs already validate the
# frontend via the existing `Frontend live-browser smoke (Playwright)`
# job in ci-baseline.yml; release to S3 is a release-only concern that
# fires only after merge to main.
#
# OWNERSHIP SPLIT (Camp B / asymmetric defense, ADR-0027 + ldz #90):
# - This workflow is the SINGLE WRITER to the frontend bucket.
# - ldz adds an S3 bucket policy denying s3:PutObject from any
# principal except this OIDC role (defense-in-depth, mirrors
# ECR resource policy in ldz #83).
# - Mac dev who runs `pnpm build` locally produces dist/ — that
# bundle never reaches S3. Only this workflow does.
on:
push:
branches: [main]
paths:
# Only fire on actual frontend changes (or workflow file edits).
# Engine / gateway / proto / docs changes don't need a frontend redeploy.
- "frontend_web/**"
- "tools/scripts/frontend.sh"
- "pnpm-lock.yaml"
- "package.json"
- ".github/workflows/release-staging-frontend.yml"
# workflow_dispatch enables manual re-trigger from the GitHub Actions UI
# for break-glass scenarios — e.g., re-deploying the same SHA after a
# CloudFront cache hiccup, or validating IAM/role changes on the ldz
# side without forcing a no-op frontend commit. The same OIDC trust
# scope (refs/heads/main + job_workflow_ref) applies; manual dispatch
# only works from the main branch's workflow file, so the trust
# boundary is unchanged.
workflow_dispatch:
permissions:
id-token: write # OIDC token exchange to AWS
contents: read # checkout
env:
# All AWS infra identifiers come from GitHub Repository Variables
# (`gh variable list`, also visible at Settings → Secrets and
# variables → Actions → Variables tab). Variables are non-encrypted,
# readable from UI/CLI for debugging, and forker-friendly: a fork
# operator overrides them in their repo settings without editing
# this YAML at all. ADR-0027 §"GH Variables over hardcode/Secrets"
# documents the pivot. Real secrets (BUILDBUDDY_API_KEY, future
# Cosign keys) stay in GH Secrets.
AWS_REGION: ${{ vars.AWS_REGION }}
FRONTEND_ROLE_ARN: arn:aws:iam::${{ vars.AWS_ACCOUNT_ID }}:role/${{ vars.FRONTEND_PUSH_ROLE_NAME }}
FRONTEND_BUCKET: ${{ vars.FRONTEND_S3_BUCKET }}
FRONTEND_DISTRIBUTION_ID: ${{ vars.FRONTEND_CLOUDFRONT_DISTRIBUTION_ID }}
# Build-time env injected into Vite — baked into the JS bundle as
# string literals at `vite build` time. Per ADR-0027 the SPA reaches
# the gateway at the API subdomain (cross-origin `aegis-app.` →
# `aegis-api.`); the GATEWAY_DOMAIN variable holds just the
# hostname so the protocol prefix is composed here.
VITE_AEGIS_GATEWAY_ENDPOINT: https://${{ vars.GATEWAY_DOMAIN }}
VITE_AEGIS_DEPLOY_MODE: cloud
# Cognito User Pool — the WS3 platform tier (aegis-platform-aws
# envs/platform/cognito.tf) provisions its OWN user pool + app client
# with NO prevent_destroy: the pool id and client id are REASSIGNED on
# every apply/teardown cycle. So these are GH Repository Variables, set
# post-apply from the platform Cognito outputs — the same pattern as
# FRONTEND_CLOUDFRONT_DISTRIBUTION_ID above — which keeps the SPA in
# lockstep with the gateway (the gateway reads the same new-pool issuer
# via the ArgoCD ConfigMap patch). Previously hardcoded to the retired
# ldz pool, which a fresh platform apply no longer owns — the stale
# redirect host + pool id broke PKCE login and JWT validation (caught by
# the WS3 pre-apply audit). None are secrets: pool id, client id and
# hostnames are public SPA surface.
#
# COGNITO_AUTHORITY = terraform -chdir=envs/platform output cognito_issuer
# (https://cognito-idp.<region>.amazonaws.com/<pool-id>)
# COGNITO_CLIENT_ID = terraform -chdir=envs/platform output cognito_app_client_id
# COGNITO_REDIRECT_URI = https://app.<env>.<zone>/auth/callback (= cognito.tf callback_urls)
# COGNITO_LOGOUT_URI = https://app.<env>.<zone>/
VITE_AEGIS_COGNITO_AUTHORITY: ${{ vars.COGNITO_AUTHORITY }}
VITE_AEGIS_COGNITO_CLIENT_ID: ${{ vars.COGNITO_CLIENT_ID }}
VITE_AEGIS_COGNITO_REDIRECT_URI: ${{ vars.COGNITO_REDIRECT_URI }}
VITE_AEGIS_COGNITO_LOGOUT_URI: ${{ vars.COGNITO_LOGOUT_URI }}
jobs:
release-staging-frontend:
name: Build + sync staging frontend bundle
runs-on: ubuntu-latest
timeout-minutes: 15
steps:
- name: Checkout
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6
- name: Cache pnpm store
uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5
with:
path: |
${{ github.workspace }}/.pnpm-store
${{ github.workspace }}/.bazelisk
/tmp/aegis-bazel-*
# pnpm-lock.yaml hash drives cache key; lockfile bumps invalidate.
key: pnpm-frontend-${{ runner.os }}-${{ hashFiles('pnpm-lock.yaml') }}
restore-keys: |
pnpm-frontend-${{ runner.os }}-
- name: Install frontend deps (hermetic Node + pnpm via aspect_rules_js)
run: ./tools/scripts/frontend.sh install
- name: Build production SPA bundle
# tools/scripts/frontend.sh build invokes `pnpm build` which runs
# `tsc -b && vite build`. VITE_* env vars from the workflow `env:`
# block are substituted into the bundle as `import.meta.env.*`
# values at build time — see ADR-0027 §"Build-time env injection".
run: ./tools/scripts/frontend.sh build
- name: Write runtime config.json (ADR-15)
# The SPA reads /config.json at boot and lets it override anything
# baked into the bundle (the committed public/config.json ships
# LOCAL mode for a fresh clone). This step is the runtime source of
# truth for the staging deploy; values mirror the VITE_* env above,
# which now serves only as a build-time fallback.
run: |
set -euxo pipefail
cat > frontend_web/dist/config.json <<EOF
{
"deployMode": "cloud",
"gatewayEndpoint": "${VITE_AEGIS_GATEWAY_ENDPOINT}",
"cognito": {
"authority": "${VITE_AEGIS_COGNITO_AUTHORITY}",
"clientId": "${VITE_AEGIS_COGNITO_CLIENT_ID}",
"redirectUri": "${VITE_AEGIS_COGNITO_REDIRECT_URI}",
"logoutUri": "${VITE_AEGIS_COGNITO_LOGOUT_URI}"
}
}
EOF
cat frontend_web/dist/config.json
# OIDC token → STS AssumeRoleWithWebIdentity → temporary creds
# exposed as env vars for subsequent aws CLI calls.
- name: Configure AWS credentials (OIDC)
# aws-actions/configure-aws-credentials v6.1.0 (released 2026-04-06)
uses: aws-actions/configure-aws-credentials@517a711dbcd0e402f90c77e7e2f81e849156e31d
with:
aws-region: ${{ env.AWS_REGION }}
role-to-assume: ${{ env.FRONTEND_ROLE_ARN }}
role-session-name: aegis-core-frontend-deploy-${{ github.run_id }}
- name: Sync bundle to S3
run: |
set -euxo pipefail
# --delete removes objects in S3 that aren't in dist/ — keeps
# the bucket exactly mirroring the latest build (Vite uses
# content-hashed filenames so this is safe; old hashes are
# what cache invalidation handles below).
aws s3 sync frontend_web/dist/ "s3://${FRONTEND_BUCKET}/" \
--delete \
--cache-control "public, max-age=31536000, immutable" \
--exclude "index.html" \
--exclude "config.json"
# index.html + config.json get a short cache so users see new
# builds and config changes promptly without waiting for the
# year-long immutable TTL on the content-hashed assets.
aws s3 cp frontend_web/dist/index.html "s3://${FRONTEND_BUCKET}/index.html" \
--cache-control "public, max-age=60, must-revalidate" \
--content-type "text/html; charset=utf-8"
# config.json is the ADR-15 runtime config — never immutable-cached.
aws s3 cp frontend_web/dist/config.json "s3://${FRONTEND_BUCKET}/config.json" \
--cache-control "public, max-age=60, must-revalidate" \
--content-type "application/json; charset=utf-8"
- name: Invalidate CloudFront edge cache
run: |
set -euxo pipefail
# Invalidation is THE single most important step for SPA deploys
# — without it, users see stale index.html pointing at
# already-deleted asset filenames. Vite's content-hashed asset
# names mean the "/*" invalidation pattern is correct (no fancy
# selective invalidation needed, and CloudFront's first 1000
# invalidations/month are free).
INVALIDATION_ID=$(aws cloudfront create-invalidation \
--distribution-id "${FRONTEND_DISTRIBUTION_ID}" \
--paths "/*" \
--query 'Invalidation.Id' \
--output text)
echo "::notice title=Frontend deployed::Bundle synced to s3://${FRONTEND_BUCKET}/, CloudFront invalidation ${INVALIDATION_ID} in flight"