feat(ci): deploy Lambda zips through the org reusable (PLAT-79) (#119)
Some checks failed
Deploy / Deploy to dev (push) Has been cancelled
Deploy / Deploy to prod (push) Has been cancelled

* feat(ci): deploy Lambda zips through the org reusable (PLAT-79)

* fix(iam): trust only this account's deploy environment (PLAT-79)
This commit is contained in:
Adam Moussa 2026-09-28 19:41:09 +00:00 • committed by GitHub
parent 0bf3c7121c
commit 30a1737345
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
9 changed files with 166 additions and 189 deletions

View file

@ -1,8 +1,14 @@
name: Deploy name: Deploy
# Terraform owns Lambda skeletons. This workflow ships zips to prod and calls # Lambda zip CD. The org reusable builds the zips, uploads them, and calls
# update-function-code. It never creates an HCP run. No GitHub Releases and no # update-function-code. Terraform owns the functions and ignores code attributes.
# tagging in this workflow. #
# 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: on:
push: push:
@ -10,11 +16,19 @@ on:
paths-ignore: paths-ignore:
- "terraform/**" - "terraform/**"
- "docs/**" - "docs/**"
- "README.md" - "*.md"
- "SETUP.md" - ".github/workflows/ci.yaml"
- "AGENTS.md" - ".github/workflows/labeler.yml"
- ".github/workflows/dependency-review.yml"
release:
types: [published]
workflow_dispatch: workflow_dispatch:
inputs: inputs:
environment:
description: "Target Environment"
required: true
type: choice
options: [dev, prod]
ref: ref:
description: "Git ref to build and deploy (tag, branch, or SHA). Empty means the workflow ref." description: "Git ref to build and deploy (tag, branch, or SHA). Empty means the workflow ref."
required: false required: false
@ -25,119 +39,31 @@ permissions:
contents: read contents: read
jobs: jobs:
deploy: deploy-dev:
name: Deploy to prod name: Deploy to dev
runs-on: ubuntu-latest if: github.event_name == 'push' || (github.event_name == 'workflow_dispatch' && inputs.environment == 'dev')
timeout-minutes: 30 uses: Sea-Haven-Industries/.github/.github/workflows/cd-hcp-lambda.yaml@ee5b843ca105422b679c3fdeb9eaa68c6e500a85 # v1.0.20
environment: prod
concurrency:
group: deploy-payments-dashboard-prod
cancel-in-progress: false
permissions: permissions:
contents: read contents: read
id-token: write id-token: write
env: secrets: inherit
AWS_REGION: us-east-1
DEPLOY_ROLE_ARN: ${{ vars.DEPLOY_ROLE_ARN }}
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with: with:
ref: ${{ github.event_name == 'workflow_dispatch' && inputs.ref || github.sha }} environment: dev
persist-credentials: false ref: ${{ inputs.ref }}
ssm-prefix: /payments-dashboard/deploy
function-keys: process_csv,slack_app_home,fetch_boa,expense_receiver,expense_processor
- name: Resolve commit deploy-prod:
id: commit name: Deploy to prod
run: | if: github.event_name == 'release' || (github.event_name == 'workflow_dispatch' && inputs.environment == 'prod')
set -euo pipefail uses: Sea-Haven-Industries/.github/.github/workflows/cd-hcp-lambda.yaml@ee5b843ca105422b679c3fdeb9eaa68c6e500a85 # v1.0.20
sha="$(git rev-parse HEAD)" permissions:
echo "sha=${sha}" >> "$GITHUB_OUTPUT" contents: read
echo "Building ${sha}" id-token: write
secrets: inherit
- uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
with: with:
node-version: "24" environment: prod
cache: npm ref: ${{ github.event.release.tag_name || inputs.ref }}
ssm-prefix: /payments-dashboard/deploy
- name: Build function zips function-keys: process_csv,slack_app_home,fetch_boa,expense_receiver,expense_processor
env: ship-gate: true
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

View file

@ -5,7 +5,7 @@
![Slack](https://img.shields.io/badge/Slack-integration-4A154B?logo=slack&logoColor=white) ![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) ![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 ## Architecture

View file

@ -1,13 +1,19 @@
# Payments Dashboard — Setup Guide # Payments Dashboard — Setup Guide
Prod only. Workspace `payments-dashboard-prod` in project `seahaven-prod` Two workspaces, one configuration, selected by HCP variable `environment`:
(account `011934824531`). No seahaven-dev workspace.
| 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 ## 1. Secrets
Six Secrets Manager names already exist in seahaven-prod (copied from mgmt Six Secrets Manager names exist in each account. Terraform pins the exact
with trailing newlines stripped). Terraform reads them by name; values stay ARNs in `terraform/locals.tf`. Values stay out of state. Dev shells are not
out of state. copies of the prod secrets.
| Name | Used by | | Name | Used by |
|------|---------| |------|---------|
@ -18,32 +24,34 @@ out of state.
| `payments-dashboard/expense-slack-token` | expenseProcessor | | `payments-dashboard/expense-slack-token` | expenseProcessor |
| `payments-dashboard/expense-slack-signing-secret` | expenseReceiver | | `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 Prod already applied. A new workspace (dev) uses one bootstrap window:
`StringLike`):
1. Create the HCP workspace. Auto-apply off. No project-level variable set. 1. Tag the workspace `app:payments-dashboard`. Working directory `terraform`.
Working directory `terraform`. File trigger prefix `terraform/**` only. VCS on `main`. Trigger prefix `terraform/**`. Speculative plans on.
Speculative plans on. VCS on `main`. Set `environment`, `schedules_enabled=false`, and `boa_base_url`.
No project-level variable set.
2. From `seahaven-org-baseline`: 2. From `seahaven-org-baseline`:
`scripts/create-hcptf-bootstrap-roles.sh --account prod --allow-workspace payments-dashboard-prod` `scripts/create-hcptf-bootstrap-roles.sh --account <dev|prod> --allow-workspace payments-dashboard-<env>`
3. Point workspace `TFC_AWS_APPLY_ROLE_ARN` / `TFC_AWS_PLAN_ROLE_ARN` at 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`. `hcptf-bootstrap` / `hcptf-bootstrap-plan`. Set `TFC_AWS_PROVIDER_AUTH=true`.
4. One manual apply with `schedules_enabled=false`. This creates the scoped 4. One manual apply. This creates the scoped `hcptf-*` roles, the Lambda
`hcptf-*` roles, the Lambda boundary, VPC/NAT, and the rest of the stack. boundary, VPC/NAT, and the rest of the stack.
5. Retarget `TFC_AWS_*` to `hcptf-payments-dashboard` / 5. Retarget `TFC_AWS_*` to `hcptf-payments-dashboard` /
`hcptf-payments-dashboard-plan`. Re-run the create script with no `hcptf-payments-dashboard-plan`. Re-run the create script with no
`--allow-workspace`. `--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 GitHub Environment `dev`: no reviewers. `DEPLOY_ROLE_ARN` is the dev
variable `DEPLOY_ROLE_ARN` = Terraform output `github_deploy_role_arn`. `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 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`, HCP outputs to copy: `slack_request_url`, `expense_slack_events_url`,
`csv_bucket_name`, `static_outbound_ip`, `github_deploy_role_arn`. `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 2. Copy DynamoDB `PaymentsDashboard` mgmt → prod. Verify item counts for
`payment#`, `boa_recon#`, and `boa_balance#`. Do not copy `payment#`, `boa_recon#`, and `boa_balance#`. Do not copy
`seahaven-payments-boa-raw-*`. `seahaven-payments-boa-raw-*`.
3. GHA `workflow_dispatch` (or the merge deploy; re-run if it raced apply) to 3. Prod zip update is a human release, or `workflow_dispatch` with
overwrite stubs. `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; 4. Instant cut: Slack App Home and Expense bot Request URLs → prod;
Stampli uploader bucket → `seahaven-payments-csv-011934824531`; Stampli uploader bucket → `seahaven-payments-csv-011934824531`;
`schedules_enabled=true` via a terraform-only merge; disable mgmt `schedules_enabled=true` via a terraform-only merge; disable mgmt

View file

@ -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 # One role per account: GitHub Environments have a single DEPLOY_ROLE_ARN.
# classic subject forms), and job_workflow_ref to deploy.yaml at # The prod role trusts environment:prod only. The dev role trusts environment:dev only.
# refs/heads/main only. No v* tags until a later release ticket. # 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 # Not a Lambda execution role: no permissions_boundary. Path /tf-managed/ so
# seahaven-hcptf-iam-management DenySelfMutation (role/githubdeploy-*) does not # seahaven-hcptf-iam-management DenySelfMutation (role/githubdeploy-*) does not
@ -28,18 +30,13 @@ data "aws_iam_policy_document" "github_deploy_assume" {
condition { condition {
test = "StringEquals" test = "StringEquals"
variable = "token.actions.githubusercontent.com:sub" variable = "token.actions.githubusercontent.com:sub"
values = [ values = local.github_oidc_subs
local.github_oidc_sub,
"repo:${var.github_repo}:environment:prod",
]
} }
condition { condition {
test = "StringEquals" test = "StringEquals"
variable = "token.actions.githubusercontent.com:job_workflow_ref" variable = "token.actions.githubusercontent.com:job_workflow_ref"
values = [ values = [local.github_deploy_workflow_ref]
"${var.github_repo}/.github/workflows/deploy.yaml@refs/heads/${var.github_deploy_branch}",
]
} }
} }
} }
@ -47,7 +44,7 @@ data "aws_iam_policy_document" "github_deploy_assume" {
resource "aws_iam_role" "github_deploy" { resource "aws_iam_role" "github_deploy" {
name = local.deploy_role name = local.deploy_role
path = "/tf-managed/" 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 assume_role_policy = data.aws_iam_policy_document.github_deploy_assume.json
max_session_duration = 3600 max_session_duration = 3600
} }

View file

@ -1,10 +1,11 @@
locals { locals {
project = "payments-dashboard" project = "payments-dashboard"
account_id = "011934824531" is_prod = var.environment == "prod"
environment = "prod" account_id = local.is_prod ? "011934824531" : "710827005802"
environment = var.environment
hcp_project = "seahaven-prod" hcp_project = "seahaven-${var.environment}"
hcp_workspace = "payments-dashboard-prod" hcp_workspace = "${local.project}-${var.environment}"
apply_role = "hcptf-payments-dashboard" apply_role = "hcptf-payments-dashboard"
plan_role = "hcptf-payments-dashboard-plan" plan_role = "hcptf-payments-dashboard-plan"
deploy_role = "githubdeploy-payments-dashboard" deploy_role = "githubdeploy-payments-dashboard"
@ -20,14 +21,31 @@ locals {
dynamodb_cmk_ssm = "/seahaven/dynamodb/cmk-arn" dynamodb_cmk_ssm = "/seahaven/dynamodb/cmk-arn"
github_oidc_provider_arn = "arn:aws:iam::${local.account_id}:oidc-provider/token.actions.githubusercontent.com" github_oidc_provider_arn = "arn:aws:iam::${local.account_id}:oidc-provider/token.actions.githubusercontent.com"
# Org has Actions OIDC use_immutable_subject=true. # Repo OIDC subject customization is the default (use_default=true), so tokens
github_oidc_sub = "repo:Sea-Haven-Industries@183236204/payments-dashboard@1206210946:environment:prod" # use the classic repo:owner/name:environment:<env> 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 # Exact ARNs (ticket rule). Hardcoded so the first plan can run as
# hcptf-bootstrap-plan, which cannot ssm:GetParameter / DescribeSecret. # hcptf-bootstrap-plan, which cannot ssm:GetParameter / DescribeSecret.
secret_arns = { # 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-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/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-check-mgmt" = "arn:aws:secretsmanager:us-east-1:011934824531:secret:payments-dashboard/boa-check-mgmt-LEbC65"
@ -35,6 +53,16 @@ locals {
"payments-dashboard/expense-slack-token" = "arn:aws:secretsmanager:us-east-1:011934824531:secret:payments-dashboard/expense-slack-token-SeMg3s" "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" "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 = { functions = {
process_csv = { process_csv = {

View file

@ -34,7 +34,7 @@ output "table_name" {
} }
output "github_deploy_role_arn" { 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 value = aws_iam_role.github_deploy.arn
} }

View file

@ -16,10 +16,15 @@ variable "github_repo" {
default = "Sea-Haven-Industries/payments-dashboard" default = "Sea-Haven-Industries/payments-dashboard"
} }
variable "github_deploy_branch" { variable "environment" {
description = "Git branch pinned in job_workflow_ref for the deploy role." description = "HCP workspace stage. Selects account and workspace name."
type = string 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" { variable "boa_base_url" {

View file

@ -16,7 +16,7 @@ terraform {
organization = "seahaven" organization = "seahaven"
workspaces { workspaces {
name = "payments-dashboard-prod" tags = ["app:payments-dashboard"]
} }
} }
} }

View file

@ -34,11 +34,13 @@ describe("HCP Terraform seam (PLAT-79)", () => {
assert.match(chunk, /default\s+= false/); assert.match(chunk, /default\s+= false/);
}); });
it("is prod-only", () => { it("selects dev and prod workspaces by tag", () => {
assert.match(versions, /payments-dashboard-prod/); assert.match(versions, /app:payments-dashboard/);
assert.doesNotMatch(versions, /payments-dashboard-dev/); assert.doesNotMatch(versions, /name = "payments-dashboard-prod"/);
assert.match(locals, /environment = "prod"/); assert.match(locals, /seahaven-\$\{var\.environment\}/);
assert.doesNotMatch(locals, /seahaven-dev/); assert.match(locals, /710827005802/);
assert.match(locals, /011934824531/);
assert.match(variables, /contains\(\["dev", "prod"\], var\.environment\)/);
}); });
it("declares in-repo hcptf roles", () => { it("declares in-repo hcptf roles", () => {
@ -48,14 +50,18 @@ describe("HCP Terraform seam (PLAT-79)", () => {
assert.match(hcpIam, /DenyCreatePolicy/); assert.match(hcpIam, /DenyCreatePolicy/);
}); });
it("uses prod zip CD without SAM or GitHub Releases", () => { it("calls the Lambda zip reusable for dev and prod", () => {
assert.doesNotMatch(deploy, /release: published/); assert.match(deploy, /release:\s*\n\s*types: \[published\]/);
assert.doesNotMatch(deploy, /cd-sam/); 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, /environment: prod/);
assert.match(deploy, /deploy-payments-dashboard-prod/); assert.match(deploy, /ship-gate: true/);
assert.doesNotMatch(deploy, /gh release create/); assert.match(deploy, /ssm-prefix: \/payments-dashboard\/deploy/);
assert.match(deploy, /package_lambdas\.mjs/); assert.match(deploy, /function-keys: process_csv,slack_app_home,fetch_boa,expense_receiver,expense_processor/);
assert.match(deploy, /update-function-code/);
}); });
it("runs npm test and terraform validate behind ci / ci", () => { 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/); assert.doesNotMatch(locals, /payments-processPayrollEmail/);
}); });
it("pins GitHub deploy trust to Environment prod", () => { it("pins GitHub deploy trust to the Lambda reusable", () => {
assert.match(githubDeploy, /environment:prod/); const prodSubs = locals.split("github_oidc_subs_prod")[1].split("github_oidc_subs_dev")[0];
assert.match(githubDeploy, /deploy.yaml@refs\/heads\/\$\{var.github_deploy_branch\}/); assert.match(prodSubs, /environment:prod/);
assert.doesNotMatch(githubDeploy, /deploy.yaml@\*/); assert.doesNotMatch(prodSubs, /environment:dev/);
assert.doesNotMatch(githubDeploy, /refs\/tags\/v\*/); 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", () => { it("includes provider-6 S3 Get* needed for refresh", () => {