build(deps-dev): Bump @types/node from 20.19.0 to 26.1.1 in /frontend… #20
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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" |