mirror of
https://github.com/Sea-Haven-Industries/payments-dashboard.git
synced 2026-09-30 06:33:11 +00:00
feat(ci): deploy Lambda zips through the org reusable (PLAT-79) (#119)
* 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:
parent
0bf3c7121c
commit
30a1737345
9 changed files with 166 additions and 189 deletions
158
.github/workflows/deploy.yaml
vendored
158
.github/workflows/deploy.yaml
vendored
|
|
@ -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
|
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,7 @@
|
||||||

|

|
||||||

|

|
||||||
|
|
||||||
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
|
||||||
|
|
||||||
|
|
|
||||||
54
SETUP.md
54
SETUP.md
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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 = {
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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" {
|
||||||
|
|
|
||||||
|
|
@ -16,7 +16,7 @@ terraform {
|
||||||
organization = "seahaven"
|
organization = "seahaven"
|
||||||
|
|
||||||
workspaces {
|
workspaces {
|
||||||
name = "payments-dashboard-prod"
|
tags = ["app:payments-dashboard"]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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", () => {
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue