diff --git a/.github/workflows/deploy.yaml b/.github/workflows/deploy.yaml index b9d4775..e8b8a09 100644 --- a/.github/workflows/deploy.yaml +++ b/.github/workflows/deploy.yaml @@ -1,8 +1,14 @@ name: Deploy -# Terraform owns Lambda skeletons. This workflow ships zips to prod and calls -# update-function-code. It never creates an HCP run. No GitHub Releases and no -# tagging in this workflow. +# Lambda zip CD. The org reusable builds the zips, uploads them, and calls +# update-function-code. Terraform owns the functions and ignores code attributes. +# +# push to main -> dev, at github.sha +# release: published -> prod, at the release tag +# workflow_dispatch -> chosen environment at a chosen ref +# +# Releases are cut by a human with `gh release create vX.Y.Z --target main`. +# Nothing here creates an HCP run. on: push: @@ -10,11 +16,19 @@ on: paths-ignore: - "terraform/**" - "docs/**" - - "README.md" - - "SETUP.md" - - "AGENTS.md" + - "*.md" + - ".github/workflows/ci.yaml" + - ".github/workflows/labeler.yml" + - ".github/workflows/dependency-review.yml" + release: + types: [published] workflow_dispatch: inputs: + environment: + description: "Target Environment" + required: true + type: choice + options: [dev, prod] ref: description: "Git ref to build and deploy (tag, branch, or SHA). Empty means the workflow ref." required: false @@ -25,119 +39,31 @@ permissions: contents: read jobs: - deploy: - name: Deploy to prod - runs-on: ubuntu-latest - timeout-minutes: 30 - environment: prod - concurrency: - group: deploy-payments-dashboard-prod - cancel-in-progress: false + deploy-dev: + name: Deploy to dev + if: github.event_name == 'push' || (github.event_name == 'workflow_dispatch' && inputs.environment == 'dev') + uses: Sea-Haven-Industries/.github/.github/workflows/cd-hcp-lambda.yaml@ee5b843ca105422b679c3fdeb9eaa68c6e500a85 # v1.0.20 permissions: contents: read id-token: write - env: - AWS_REGION: us-east-1 - DEPLOY_ROLE_ARN: ${{ vars.DEPLOY_ROLE_ARN }} - steps: - - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - with: - ref: ${{ github.event_name == 'workflow_dispatch' && inputs.ref || github.sha }} - persist-credentials: false + secrets: inherit + with: + environment: dev + ref: ${{ inputs.ref }} + ssm-prefix: /payments-dashboard/deploy + function-keys: process_csv,slack_app_home,fetch_boa,expense_receiver,expense_processor - - name: Resolve commit - id: commit - run: | - set -euo pipefail - sha="$(git rev-parse HEAD)" - echo "sha=${sha}" >> "$GITHUB_OUTPUT" - echo "Building ${sha}" - - - uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 - with: - node-version: "24" - cache: npm - - - name: Build function zips - env: - GIT_SHA: ${{ steps.commit.outputs.sha }} - run: | - set -euo pipefail - node scripts/package_lambdas.mjs --git-sha "${GIT_SHA}" --out-dir build/packages - python3 - <<'PY' - import os, zipfile - from pathlib import Path - sha = os.environ["GIT_SHA"] - names = [ - "process_csv", - "slack_app_home", - "fetch_boa", - "expense_receiver", - "expense_processor", - ] - for name in names: - path = Path("build/packages") / f"{name}.zip" - if not path.is_file(): - raise SystemExit(f"missing {path}") - with zipfile.ZipFile(path) as zf: - info = zf.read("src/buildInfo.js").decode() - if sha not in info: - raise SystemExit(f"{path} missing GIT_SHA {sha}") - if "src/processPaymentCsv.js" not in zf.namelist(): - raise SystemExit(f"{path} missing src/") - print("zips ok") - PY - - - name: Configure AWS credentials using OIDC - uses: aws-actions/configure-aws-credentials@e1253824e5c10ff9df46874f81ed3ec929e19cfd # v6.3.0 - with: - role-to-assume: ${{ env.DEPLOY_ROLE_ARN }} - aws-region: us-east-1 - audience: sts.amazonaws.com - - - name: Get deploy parameters - id: deploy - run: | - set -euo pipefail - prefix=/payments-dashboard/deploy - ARTIFACTS_BUCKET=$(aws ssm get-parameter --name "${prefix}/artifacts-bucket" --query Parameter.Value --output text) - { - echo "artifacts_bucket=${ARTIFACTS_BUCKET}" - echo "process_csv=$(aws ssm get-parameter --name "${prefix}/process_csv-function-name" --query Parameter.Value --output text)" - echo "slack_app_home=$(aws ssm get-parameter --name "${prefix}/slack_app_home-function-name" --query Parameter.Value --output text)" - echo "fetch_boa=$(aws ssm get-parameter --name "${prefix}/fetch_boa-function-name" --query Parameter.Value --output text)" - echo "expense_receiver=$(aws ssm get-parameter --name "${prefix}/expense_receiver-function-name" --query Parameter.Value --output text)" - echo "expense_processor=$(aws ssm get-parameter --name "${prefix}/expense_processor-function-name" --query Parameter.Value --output text)" - } >> "${GITHUB_OUTPUT}" - - - name: Upload zips and update function code - env: - ARTIFACTS_BUCKET: ${{ steps.deploy.outputs.artifacts_bucket }} - GIT_SHA: ${{ steps.commit.outputs.sha }} - PROCESS_CSV: ${{ steps.deploy.outputs.process_csv }} - SLACK_APP_HOME: ${{ steps.deploy.outputs.slack_app_home }} - FETCH_BOA: ${{ steps.deploy.outputs.fetch_boa }} - EXPENSE_RECEIVER: ${{ steps.deploy.outputs.expense_receiver }} - EXPENSE_PROCESSOR: ${{ steps.deploy.outputs.expense_processor }} - run: | - set -euo pipefail - keys=( - process_csv:"${PROCESS_CSV}" - slack_app_home:"${SLACK_APP_HOME}" - fetch_boa:"${FETCH_BOA}" - expense_receiver:"${EXPENSE_RECEIVER}" - expense_processor:"${EXPENSE_PROCESSOR}" - ) - for pair in "${keys[@]}"; do - name="${pair%%:*}" - fn="${pair#*:}" - key="functions/${name}/${GIT_SHA}.zip" - aws s3 cp "build/packages/${name}.zip" "s3://${ARTIFACTS_BUCKET}/${key}" - aws lambda update-function-code \ - --function-name "${fn}" \ - --s3-bucket "${ARTIFACTS_BUCKET}" \ - --s3-key "${key}" \ - --query '{Function:FunctionName,Sha256:CodeSha256,Updated:LastModified}' \ - --output table - aws lambda wait function-updated-v2 --function-name "${fn}" - done + deploy-prod: + name: Deploy to prod + if: github.event_name == 'release' || (github.event_name == 'workflow_dispatch' && inputs.environment == 'prod') + uses: Sea-Haven-Industries/.github/.github/workflows/cd-hcp-lambda.yaml@ee5b843ca105422b679c3fdeb9eaa68c6e500a85 # v1.0.20 + permissions: + contents: read + id-token: write + secrets: inherit + with: + environment: prod + ref: ${{ github.event.release.tag_name || inputs.ref }} + ssm-prefix: /payments-dashboard/deploy + function-keys: process_csv,slack_app_home,fetch_boa,expense_receiver,expense_processor + ship-gate: true diff --git a/README.md b/README.md index d19f1ad..8c13691 100644 --- a/README.md +++ b/README.md @@ -5,7 +5,7 @@ ![Slack](https://img.shields.io/badge/Slack-integration-4A154B?logo=slack&logoColor=white) ![CI](https://github.com/Sea-Haven-Industries/payments-dashboard/actions/workflows/ci.yaml/badge.svg) -HCP Terraform application that ingests payment CSVs, syncs check data with Bank of America CashPro APIs, surfaces an outstanding-payments dashboard in Slack, and routes expense approvals through a Slack reaction-driven workflow. Prod workspace: `payments-dashboard-prod` (trigger prefix `terraform/**`). Zip CD is GitHub Actions Environment `prod`. +HCP Terraform application that ingests payment CSVs, syncs check data with Bank of America CashPro APIs, surfaces an outstanding-payments dashboard in Slack, and routes expense approvals through a Slack reaction-driven workflow. Workspaces `payments-dashboard-dev` and `payments-dashboard-prod` share tag `app:payments-dashboard` (trigger prefix `terraform/**`). Push to `main` deploys function zips to dev. A human GitHub Release deploys prod. ## Architecture diff --git a/SETUP.md b/SETUP.md index dfebd56..1f2a25e 100644 --- a/SETUP.md +++ b/SETUP.md @@ -1,13 +1,19 @@ # Payments Dashboard — Setup Guide -Prod only. Workspace `payments-dashboard-prod` in project `seahaven-prod` -(account `011934824531`). No seahaven-dev workspace. +Two workspaces, one configuration, selected by HCP variable `environment`: + +| Workspace | Project | Account | `environment` | `boa_base_url` | +|-----------|---------|---------|---------------|----------------| +| `payments-dashboard-prod` | `seahaven-prod` | `011934824531` | `prod` | `https://api.bofa.com` | +| `payments-dashboard-dev` | `seahaven-dev` | `710827005802` | `dev` | `https://api-sb.bofa.com` | + +Both carry tag `app:payments-dashboard`. `schedules_enabled` stays false until cutover. ## 1. Secrets -Six Secrets Manager names already exist in seahaven-prod (copied from mgmt -with trailing newlines stripped). Terraform reads them by name; values stay -out of state. +Six Secrets Manager names exist in each account. Terraform pins the exact +ARNs in `terraform/locals.tf`. Values stay out of state. Dev shells are not +copies of the prod secrets. | Name | Used by | |------|---------| @@ -18,32 +24,34 @@ out of state. | `payments-dashboard/expense-slack-token` | expenseProcessor | | `payments-dashboard/expense-slack-signing-secret` | expenseReceiver | -## 2. HCP Terraform and GitHub Environment +## 2. HCP Terraform and GitHub Environments -First apply uses the hcptf-bootstrap window (exact `StringEquals` trust, never -`StringLike`): +Prod already applied. A new workspace (dev) uses one bootstrap window: -1. Create the HCP workspace. Auto-apply off. No project-level variable set. - Working directory `terraform`. File trigger prefix `terraform/**` only. - Speculative plans on. VCS on `main`. +1. Tag the workspace `app:payments-dashboard`. Working directory `terraform`. + VCS on `main`. Trigger prefix `terraform/**`. Speculative plans on. + Set `environment`, `schedules_enabled=false`, and `boa_base_url`. + No project-level variable set. 2. From `seahaven-org-baseline`: - `scripts/create-hcptf-bootstrap-roles.sh --account prod --allow-workspace payments-dashboard-prod` -3. Point workspace `TFC_AWS_APPLY_ROLE_ARN` / `TFC_AWS_PLAN_ROLE_ARN` at + `scripts/create-hcptf-bootstrap-roles.sh --account --allow-workspace payments-dashboard-` +3. Point that workspace's `TFC_AWS_APPLY_ROLE_ARN` / `TFC_AWS_PLAN_ROLE_ARN` at `hcptf-bootstrap` / `hcptf-bootstrap-plan`. Set `TFC_AWS_PROVIDER_AUTH=true`. -4. One manual apply with `schedules_enabled=false`. This creates the scoped - `hcptf-*` roles, the Lambda boundary, VPC/NAT, and the rest of the stack. +4. One manual apply. This creates the scoped `hcptf-*` roles, the Lambda + boundary, VPC/NAT, and the rest of the stack. 5. Retarget `TFC_AWS_*` to `hcptf-payments-dashboard` / `hcptf-payments-dashboard-plan`. Re-run the create script with no `--allow-workspace`. -6. Second manual apply as the scoped role. Then seal auto-apply on after - live-path proof. -GitHub Environment `prod`: reviewers, branch policy `main` only, Environment -variable `DEPLOY_ROLE_ARN` = Terraform output `github_deploy_role_arn`. +GitHub Environment `dev`: no reviewers. `DEPLOY_ROLE_ARN` is the dev +`github_deploy_role_arn`. Environment `prod`: reviewers, branch policy `main` +and `v*`, prod `github_deploy_role_arn`. -Function zips: Actions → Deploy on push to `main`, or `workflow_dispatch`. +Function zips: push to `main` deploys dev. A human +`gh release create vX.Y.Z --target main` deploys prod (`ship-gate` on). +`workflow_dispatch` takes `environment` and `ref`. The caller is +`.github/workflows/deploy.yaml`. It calls org reusable `cd-hcp-lambda.yaml`. Keep `schedules_enabled=false` until Slack Request URLs and the Stampli -uploader point at this stack. +uploader point at the prod stack. HCP outputs to copy: `slack_request_url`, `expense_slack_events_url`, `csv_bucket_name`, `static_outbound_ip`, `github_deploy_role_arn`. @@ -63,8 +71,8 @@ Avoid weekday 9am ET and the 16:00/19:00/22:00 UTC intraday slots. 2. Copy DynamoDB `PaymentsDashboard` mgmt → prod. Verify item counts for `payment#`, `boa_recon#`, and `boa_balance#`. Do not copy `seahaven-payments-boa-raw-*`. -3. GHA `workflow_dispatch` (or the merge deploy; re-run if it raced apply) to - overwrite stubs. +3. Prod zip update is a human release, or `workflow_dispatch` with + `environment=prod`, after the HCP apply. Re-run if the job raced apply. 4. Instant cut: Slack App Home and Expense bot Request URLs → prod; Stampli uploader bucket → `seahaven-payments-csv-011934824531`; `schedules_enabled=true` via a terraform-only merge; disable mgmt diff --git a/terraform/iam_github_deploy.tf b/terraform/iam_github_deploy.tf index 1aa8eff..b60cafa 100644 --- a/terraform/iam_github_deploy.tf +++ b/terraform/iam_github_deploy.tf @@ -1,8 +1,10 @@ -# GitHub Actions OIDC role for .github/workflows/deploy.yaml. +# GitHub Actions OIDC role for the thin deploy.yaml caller of +# org reusable cd-hcp-lambda.yaml. # -# Trust is pinned three ways: aud, sub to Environment prod (immutable and -# classic subject forms), and job_workflow_ref to deploy.yaml at -# refs/heads/main only. No v* tags until a later release ticket. +# One role per account: GitHub Environments have a single DEPLOY_ROLE_ARN. +# The prod role trusts environment:prod only. The dev role trusts environment:dev only. +# job_workflow_ref is StringEquals on the reusable SHA pinned by deploy.yaml. +# AWS STS GitHub condition keys include job_workflow_ref, not workflow_ref. # # Not a Lambda execution role: no permissions_boundary. Path /tf-managed/ so # seahaven-hcptf-iam-management DenySelfMutation (role/githubdeploy-*) does not @@ -28,18 +30,13 @@ data "aws_iam_policy_document" "github_deploy_assume" { condition { test = "StringEquals" variable = "token.actions.githubusercontent.com:sub" - values = [ - local.github_oidc_sub, - "repo:${var.github_repo}:environment:prod", - ] + values = local.github_oidc_subs } condition { test = "StringEquals" variable = "token.actions.githubusercontent.com:job_workflow_ref" - values = [ - "${var.github_repo}/.github/workflows/deploy.yaml@refs/heads/${var.github_deploy_branch}", - ] + values = [local.github_deploy_workflow_ref] } } } @@ -47,7 +44,7 @@ data "aws_iam_policy_document" "github_deploy_assume" { resource "aws_iam_role" "github_deploy" { name = local.deploy_role path = "/tf-managed/" - description = "GitHub Actions Lambda deploy role for ${var.github_repo} Environment prod" + description = "GitHub Actions Lambda deploy role for ${var.github_repo}" assume_role_policy = data.aws_iam_policy_document.github_deploy_assume.json max_session_duration = 3600 } diff --git a/terraform/locals.tf b/terraform/locals.tf index cd61802..c97d247 100644 --- a/terraform/locals.tf +++ b/terraform/locals.tf @@ -1,10 +1,11 @@ locals { project = "payments-dashboard" - account_id = "011934824531" - environment = "prod" + is_prod = var.environment == "prod" + account_id = local.is_prod ? "011934824531" : "710827005802" + environment = var.environment - hcp_project = "seahaven-prod" - hcp_workspace = "payments-dashboard-prod" + hcp_project = "seahaven-${var.environment}" + hcp_workspace = "${local.project}-${var.environment}" apply_role = "hcptf-payments-dashboard" plan_role = "hcptf-payments-dashboard-plan" deploy_role = "githubdeploy-payments-dashboard" @@ -20,21 +21,48 @@ locals { dynamodb_cmk_ssm = "/seahaven/dynamodb/cmk-arn" github_oidc_provider_arn = "arn:aws:iam::${local.account_id}:oidc-provider/token.actions.githubusercontent.com" - # Org has Actions OIDC use_immutable_subject=true. - github_oidc_sub = "repo:Sea-Haven-Industries@183236204/payments-dashboard@1206210946:environment:prod" + # Repo OIDC subject customization is the default (use_default=true), so tokens + # use the classic repo:owner/name:environment: form. Each account's role + # trusts only its own Environment. The prod role must not trust dev. + github_oidc_subs_prod = [ + "repo:${var.github_repo}:environment:prod", + ] + github_oidc_subs_dev = [ + "repo:${var.github_repo}:environment:dev", + ] + github_oidc_subs = local.is_prod ? local.github_oidc_subs_prod : local.github_oidc_subs_dev + # Matches the SHA pin in .github/workflows/deploy.yaml. A reusable bump + # updates both together. StringEquals, not @*. + github_deploy_workflow_ref = "Sea-Haven-Industries/.github/.github/workflows/cd-hcp-lambda.yaml@ee5b843ca105422b679c3fdeb9eaa68c6e500a85" - dynamodb_cmk_arn = "arn:aws:kms:us-east-1:011934824531:key/be5fa4cb-c546-40fe-a13d-c7bec79f5d12" + dynamodb_cmk_arns = { + prod = "arn:aws:kms:us-east-1:011934824531:key/be5fa4cb-c546-40fe-a13d-c7bec79f5d12" + dev = "arn:aws:kms:us-east-1:710827005802:key/600997e6-418e-4b7f-9d63-cd42a7505a95" + } + dynamodb_cmk_arn = local.dynamodb_cmk_arns[var.environment] # Exact ARNs (ticket rule). Hardcoded so the first plan can run as # hcptf-bootstrap-plan, which cannot ssm:GetParameter / DescribeSecret. - secret_arns = { - "payments-dashboard/slack-bot-token" = "arn:aws:secretsmanager:us-east-1:011934824531:secret:payments-dashboard/slack-bot-token-0pAM3S" - "payments-dashboard/slack-signing-secret" = "arn:aws:secretsmanager:us-east-1:011934824531:secret:payments-dashboard/slack-signing-secret-u0T6h8" - "payments-dashboard/boa-check-mgmt" = "arn:aws:secretsmanager:us-east-1:011934824531:secret:payments-dashboard/boa-check-mgmt-LEbC65" - "payments-dashboard/boa-reporting" = "arn:aws:secretsmanager:us-east-1:011934824531:secret:payments-dashboard/boa-reporting-JoR9lq" - "payments-dashboard/expense-slack-token" = "arn:aws:secretsmanager:us-east-1:011934824531:secret:payments-dashboard/expense-slack-token-SeMg3s" - "payments-dashboard/expense-slack-signing-secret" = "arn:aws:secretsmanager:us-east-1:011934824531:secret:payments-dashboard/expense-slack-signing-secret-lbb78J" + # Dev values are shells created for this workspace. They are not prod secrets. + secret_arns_by_env = { + prod = { + "payments-dashboard/slack-bot-token" = "arn:aws:secretsmanager:us-east-1:011934824531:secret:payments-dashboard/slack-bot-token-0pAM3S" + "payments-dashboard/slack-signing-secret" = "arn:aws:secretsmanager:us-east-1:011934824531:secret:payments-dashboard/slack-signing-secret-u0T6h8" + "payments-dashboard/boa-check-mgmt" = "arn:aws:secretsmanager:us-east-1:011934824531:secret:payments-dashboard/boa-check-mgmt-LEbC65" + "payments-dashboard/boa-reporting" = "arn:aws:secretsmanager:us-east-1:011934824531:secret:payments-dashboard/boa-reporting-JoR9lq" + "payments-dashboard/expense-slack-token" = "arn:aws:secretsmanager:us-east-1:011934824531:secret:payments-dashboard/expense-slack-token-SeMg3s" + "payments-dashboard/expense-slack-signing-secret" = "arn:aws:secretsmanager:us-east-1:011934824531:secret:payments-dashboard/expense-slack-signing-secret-lbb78J" + } + dev = { + "payments-dashboard/slack-bot-token" = "arn:aws:secretsmanager:us-east-1:710827005802:secret:payments-dashboard/slack-bot-token-KhPaLp" + "payments-dashboard/slack-signing-secret" = "arn:aws:secretsmanager:us-east-1:710827005802:secret:payments-dashboard/slack-signing-secret-CDxsUK" + "payments-dashboard/boa-check-mgmt" = "arn:aws:secretsmanager:us-east-1:710827005802:secret:payments-dashboard/boa-check-mgmt-kkEnCw" + "payments-dashboard/boa-reporting" = "arn:aws:secretsmanager:us-east-1:710827005802:secret:payments-dashboard/boa-reporting-uMHHVo" + "payments-dashboard/expense-slack-token" = "arn:aws:secretsmanager:us-east-1:710827005802:secret:payments-dashboard/expense-slack-token-05OZg3" + "payments-dashboard/expense-slack-signing-secret" = "arn:aws:secretsmanager:us-east-1:710827005802:secret:payments-dashboard/expense-slack-signing-secret-DQAoXS" + } } + secret_arns = local.secret_arns_by_env[var.environment] functions = { process_csv = { diff --git a/terraform/outputs.tf b/terraform/outputs.tf index 58f51f4..1770aef 100644 --- a/terraform/outputs.tf +++ b/terraform/outputs.tf @@ -34,7 +34,7 @@ output "table_name" { } output "github_deploy_role_arn" { - description = "OIDC role ARN for .github/workflows/deploy.yaml (GitHub Environment prod variable DEPLOY_ROLE_ARN)." + description = "OIDC role ARN for the deploy.yaml caller (GitHub Environment variable DEPLOY_ROLE_ARN)." value = aws_iam_role.github_deploy.arn } diff --git a/terraform/variables.tf b/terraform/variables.tf index 013f31c..0a0ecc2 100644 --- a/terraform/variables.tf +++ b/terraform/variables.tf @@ -16,10 +16,15 @@ variable "github_repo" { default = "Sea-Haven-Industries/payments-dashboard" } -variable "github_deploy_branch" { - description = "Git branch pinned in job_workflow_ref for the deploy role." +variable "environment" { + description = "HCP workspace stage. Selects account and workspace name." type = string - default = "main" + default = "prod" + + validation { + condition = contains(["dev", "prod"], var.environment) + error_message = "environment must be \"dev\" or \"prod\"." + } } variable "boa_base_url" { diff --git a/terraform/versions.tf b/terraform/versions.tf index 106c297..422f26a 100644 --- a/terraform/versions.tf +++ b/terraform/versions.tf @@ -16,7 +16,7 @@ terraform { organization = "seahaven" workspaces { - name = "payments-dashboard-prod" + tags = ["app:payments-dashboard"] } } } diff --git a/tests/infra/hcpContract.test.js b/tests/infra/hcpContract.test.js index c05d01d..242f178 100644 --- a/tests/infra/hcpContract.test.js +++ b/tests/infra/hcpContract.test.js @@ -34,11 +34,13 @@ describe("HCP Terraform seam (PLAT-79)", () => { assert.match(chunk, /default\s+= false/); }); - it("is prod-only", () => { - assert.match(versions, /payments-dashboard-prod/); - assert.doesNotMatch(versions, /payments-dashboard-dev/); - assert.match(locals, /environment = "prod"/); - assert.doesNotMatch(locals, /seahaven-dev/); + it("selects dev and prod workspaces by tag", () => { + assert.match(versions, /app:payments-dashboard/); + assert.doesNotMatch(versions, /name = "payments-dashboard-prod"/); + assert.match(locals, /seahaven-\$\{var\.environment\}/); + assert.match(locals, /710827005802/); + assert.match(locals, /011934824531/); + assert.match(variables, /contains\(\["dev", "prod"\], var\.environment\)/); }); it("declares in-repo hcptf roles", () => { @@ -48,14 +50,18 @@ describe("HCP Terraform seam (PLAT-79)", () => { assert.match(hcpIam, /DenyCreatePolicy/); }); - it("uses prod zip CD without SAM or GitHub Releases", () => { - assert.doesNotMatch(deploy, /release: published/); + it("calls the Lambda zip reusable for dev and prod", () => { + assert.match(deploy, /release:\s*\n\s*types: \[published\]/); assert.doesNotMatch(deploy, /cd-sam/); + assert.doesNotMatch(deploy, /aws lambda update-function-code/); + assert.match(deploy, /gh release create vX\.Y\.Z --target main/); + assert.doesNotMatch(deploy, /release\.yaml@/); + assert.match(deploy, /cd-hcp-lambda\.yaml@/); + assert.match(deploy, /environment: dev/); assert.match(deploy, /environment: prod/); - assert.match(deploy, /deploy-payments-dashboard-prod/); - assert.doesNotMatch(deploy, /gh release create/); - assert.match(deploy, /package_lambdas\.mjs/); - assert.match(deploy, /update-function-code/); + assert.match(deploy, /ship-gate: true/); + assert.match(deploy, /ssm-prefix: \/payments-dashboard\/deploy/); + assert.match(deploy, /function-keys: process_csv,slack_app_home,fetch_boa,expense_receiver,expense_processor/); }); it("runs npm test and terraform validate behind ci / ci", () => { @@ -81,11 +87,18 @@ describe("HCP Terraform seam (PLAT-79)", () => { assert.doesNotMatch(locals, /payments-processPayrollEmail/); }); - it("pins GitHub deploy trust to Environment prod", () => { - assert.match(githubDeploy, /environment:prod/); - assert.match(githubDeploy, /deploy.yaml@refs\/heads\/\$\{var.github_deploy_branch\}/); - assert.doesNotMatch(githubDeploy, /deploy.yaml@\*/); - assert.doesNotMatch(githubDeploy, /refs\/tags\/v\*/); + it("pins GitHub deploy trust to the Lambda reusable", () => { + const prodSubs = locals.split("github_oidc_subs_prod")[1].split("github_oidc_subs_dev")[0]; + assert.match(prodSubs, /environment:prod/); + assert.doesNotMatch(prodSubs, /environment:dev/); + assert.match(githubDeploy, /github_oidc_subs/); + assert.match(githubDeploy, /job_workflow_ref/); + assert.match(locals, /cd-hcp-lambda\.yaml@ee5b843ca105422b679c3fdeb9eaa68c6e500a85/); + assert.match(deploy, /cd-hcp-lambda\.yaml@ee5b843ca105422b679c3fdeb9eaa68c6e500a85/); + assert.doesNotMatch(githubDeploy, /cd-hcp-lambda\.yaml@\*/); + assert.doesNotMatch(locals, /cd-hcp-lambda\.yaml@\*/); + assert.doesNotMatch(githubDeploy, /deploy\.yaml@refs\/heads/); + assert.doesNotMatch(variables, /github_deploy_branch/); }); it("includes provider-6 S3 Get* needed for refresh", () => {