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
# 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

View file

@ -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

View file

@ -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 <dev|prod> --allow-workspace payments-dashboard-<env>`
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

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
# 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
}

View file

@ -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:<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
# 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 = {

View file

@ -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
}

View file

@ -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" {

View file

@ -16,7 +16,7 @@ terraform {
organization = "seahaven"
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/);
});
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", () => {