feat(ci): deploy the marketing site through the org static caller (PLAT-225) (#69)

* feat(ci): deploy the marketing site through the org static caller (PLAT-225)

Prod still ships on merge to main. Exec roles leave this workspace, and the deploy reads the bucket and distribution from SSM.

* fix(ci): run the static check after the legacy ci job

Both jobs call ci-static, which cancels the other in-progress run on the same ref, so ci-complete never saw both succeed.

* fix(ci): address review feedback

* fix(ci): address review feedback

* fix(ci): address review feedback

* fix(ci): address review feedback
This commit is contained in:
Adam Moussa 2026-09-25 15:18:40 +00:00 • committed by GitHub
parent f194d65ee5
commit 3a8de4e88b
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
9 changed files with 237 additions and 541 deletions

View file

@ -1,15 +1,70 @@
name: CI
# Parallel static-site and Terraform portions. ci-complete is the check the
# CI complete ruleset requires. The ci job remains so main branch protection
# still sees ci / ci until this repo is moved onto that ruleset.
on:
pull_request:
branches: [main]
branches: [main, hotfix/**, release/**]
merge_group:
push:
branches: [hotfix/**, release/**]
permissions:
contents: read
jobs:
autofix:
if: github.event_name == 'pull_request' && !github.event.pull_request.head.repo.fork
uses: Sea-Haven-Industries/.github/.github/workflows/ci-autofix.yaml@0a1010e63248c9ca9f042c870eb2c579ba6b9455 # v1.0.19
permissions:
contents: write
secrets: inherit
with:
presets: terraform
ci:
uses: Sea-Haven-Industries/.github/.github/workflows/ci-static.yaml@9781774f04b824b1182ff41638687f1c01c04361 # v1.0.11
needs: autofix
if: always() && !cancelled() && (needs.autofix.result == 'skipped' || needs.autofix.outputs.committed != 'true')
uses: Sea-Haven-Industries/.github/.github/workflows/ci-static.yaml@0a1010e63248c9ca9f042c870eb2c579ba6b9455 # v1.0.19
with:
build-command: "npx @11ty/eleventy"
check-dir: "_site"
# ci-static cancels other in-progress runs of this workflow on the same ref.
# Run after the legacy ci job so both calls can finish.
static:
needs: [autofix, ci]
if: always() && !cancelled() && needs.ci.result == 'success' && (needs.autofix.result == 'skipped' || needs.autofix.outputs.committed != 'true')
uses: Sea-Haven-Industries/.github/.github/workflows/ci-static.yaml@0a1010e63248c9ca9f042c870eb2c579ba6b9455 # v1.0.19
with:
build-command: "npx @11ty/eleventy"
check-dir: "_site"
terraform:
needs: autofix
if: always() && !cancelled() && (needs.autofix.result == 'skipped' || needs.autofix.outputs.committed != 'true')
uses: Sea-Haven-Industries/.github/.github/workflows/ci-terraform.yaml@0a1010e63248c9ca9f042c870eb2c579ba6b9455 # v1.0.19
with:
terraform-version: "1.16.0"
ci-complete:
name: ci-complete
needs: [autofix, ci, static, terraform]
if: always() && !cancelled() && (needs.autofix.result == 'skipped' || needs.autofix.outputs.committed != 'true')
runs-on: ubuntu-latest
timeout-minutes: 5
steps:
- name: Require portions
env:
AUTOFIX: ${{ needs.autofix.result }}
CI: ${{ needs.ci.result }}
STATIC: ${{ needs.static.result }}
TERRAFORM: ${{ needs.terraform.result }}
run: |
set -euo pipefail
test "${AUTOFIX}" = success -o "${AUTOFIX}" = skipped
test "${CI}" = success
test "${STATIC}" = success
test "${TERRAFORM}" = success

View file

@ -7,7 +7,7 @@ permissions:
jobs:
review:
uses: Sea-Haven-Industries/.github/.github/workflows/callable-dependency-review.yaml@9781774f04b824b1182ff41638687f1c01c04361 # v1.0.11
uses: Sea-Haven-Industries/.github/.github/workflows/callable-dependency-review.yaml@0a1010e63248c9ca9f042c870eb2c579ba6b9455 # v1.0.19
with:
# brace-expansion OOM DoS: no in-range fix (patch only in 5.0.8; @11ty/recursive-copy
# pins minimatch <10.0.3). Build-time-only exposure, adjudicated in

View file

@ -1,9 +1,27 @@
name: Deploy
# Static-site CD. The org reusable builds _site/, syncs the bucket root, and
# invalidates CloudFront. Terraform owns the bucket and the distribution.
# Nothing here creates an HCP run.
#
# push to main -> prod
# weekday cron -> prod (Paychex listings, no commit)
# workflow_dispatch -> prod, the main commit only
#
# Vercel previews branches other than main.
on:
push:
branches:
- main
branches: [main]
paths-ignore:
# deploy.yaml is included so this transition commit does not start a
# prod deploy before the exec role can write the SSM contract.
- "terraform/**"
- "**/*.md"
- ".github/workflows/ci.yaml"
- ".github/workflows/deploy.yaml"
- ".github/workflows/labeler.yml"
- ".github/workflows/dependency-review.yml"
schedule:
# Weekday morning US Eastern (13:00 UTC). Rebuilds listings from Paychex
# without a commit. A failed feed fetch aborts before the S3 sync.
@ -11,72 +29,20 @@ on:
workflow_dispatch:
permissions:
id-token: write # Required for OIDC
contents: read # Allows checkout of repo
# Never cancel a deploy mid-flight: cancelling between the S3 sync and the
# CloudFront invalidation (or mid `--delete`) would leave the bucket in a
# half-updated state. Queue instead.
concurrency:
group: deploy-${{ github.ref }}
cancel-in-progress: false
contents: read
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v7.0.1
- name: Set up Node
uses: actions/setup-node@v7
deploy-prod:
name: Deploy site to prod
uses: Sea-Haven-Industries/.github/.github/workflows/cd-hcp-static.yaml@0a1010e63248c9ca9f042c870eb2c579ba6b9455 # v1.0.19
permissions:
contents: read
id-token: write
secrets: inherit
with:
node-version: "24"
cache: npm
- name: Build site
run: |
npm ci --ignore-scripts
npm run build
# Fail closed: never let a silently-empty build reach the --delete sync.
test -f _site/index.html
test -f _site/contact/index.html
test -f _site/404.html
count=$(find _site -type f | wc -l)
if [ "$count" -lt 40 ]; then
echo "::error::build produced only $count files (expected >= 40); aborting deploy"
exit 1
fi
echo "Build OK: $count files."
- name: Configure AWS credentials using OIDC
uses: aws-actions/configure-aws-credentials@e1253824e5c10ff9df46874f81ed3ec929e19cfd # v6.3.0
with:
role-to-assume: arn:aws:iam::011934824531:role/tf-managed/githubdeploy-seahaven-site
aws-region: us-east-1
- name: Sync build output to S3
run: |
# 1) Static assets — 1-day browser cache (no filename fingerprinting yet,
# so do NOT go immutable). CloudFront /* invalidation below keeps the
# edge fresh; this only affects returning visitors' browser cache.
aws s3 sync _site/ s3://seahaven-site-prod --no-progress \
--exclude "*.html" --exclude "*.xml" --exclude "*.txt" \
--cache-control "public, max-age=86400"
# 2) HTML / sitemap / robots — always revalidate so a deploy is seen immediately.
aws s3 sync _site/ s3://seahaven-site-prod --no-progress \
--exclude "*" --include "*.html" --include "*.xml" --include "*.txt" \
--cache-control "no-cache"
# 3) Prune files removed from the build. This pass sets no metadata, so
# it skips already-uploaded objects (preserving the Cache-Control set
# above) and only deletes objects no longer present in _site/.
aws s3 sync _site/ s3://seahaven-site-prod --no-progress --delete
- name: Invalidate CloudFront cache
run: |
aws cloudfront create-invalidation \
--distribution-id E35OCA79OAJ03H \
--paths "/*"
environment: prod
ssm-prefix: /seahaven-site/deploy
output-dir: _site
required-paths: _site/index.html,_site/contact/index.html,_site/404.html
min-file-count: 40
ship-gate: true

View file

@ -10,4 +10,4 @@ permissions:
jobs:
label:
uses: Sea-Haven-Industries/.github/.github/workflows/callable-labeler.yaml@9781774f04b824b1182ff41638687f1c01c04361 # v1.0.11
uses: Sea-Haven-Industries/.github/.github/workflows/callable-labeler.yaml@0a1010e63248c9ca9f042c870eb2c579ba6b9455 # v1.0.19

View file

@ -11,7 +11,11 @@ A clean static website for Sea Haven Industries — authored as [Eleventy](https
- **Eleventy (11ty)** — thin static build: one base layout + partials, no client framework. Output is plain HTML/CSS/vanilla JS.
- **Design system** — CSS custom properties (DM Serif Display + Inter, `--rose: #cc3366`)
- **Forms** — Basin (AJAX submission, reCAPTCHA v3, honeypot spam protection)
- **Hosting** — S3 + CloudFront, deployed via GitHub Actions OIDC (`npm run build` → sync `_site/`)
- **Hosting** — S3 + CloudFront. A merge to `main`, the weekday Paychex cron, and `workflow_dispatch` deploy production through the org static-site workflow. Other branches preview on Vercel (`vercel.json` disables Vercel deploys of `main`).
## Infrastructure
HCP Terraform workspace `seahaven-site-prod` applies `terraform/` when `terraform/**` changes on `main`. Speculative plans run on pull requests. The workspace writes `/seahaven-site/deploy/bucket` and `/seahaven-site/deploy/distribution-id`. GitHub Environment `prod` holds `DEPLOY_ROLE_ARN`. Exec roles `hcptf-seahaven-site` and `hcptf-seahaven-site-plan` live in the `seahaven-site-hcptf` stack.
## Develop
```bash

View file

@ -1,455 +0,0 @@
# HCP plan/apply roles imported from seahaven-terraform-substrate (PLAT-146).
# Import, do not recreate. Role names stay hcptf-seahaven-site / hcptf-seahaven-site-plan.
#
# Live seahaven-hcptf-iam-management DenySelfMutation blocks DetachRolePolicy
# and PutRolePolicy on hcptf-* (including this role). Import apply sequence:
# 1. seahaven-org-baseline scripts/create-hcptf-bootstrap-roles.sh
# --account prod --allow-workspace seahaven-site-prod
# 2. Point this workspace's TFC_AWS_* at hcptf-bootstrap /
# hcptf-bootstrap-plan (workspace vars, never a project set).
# 3. One Manual apply (import + detach seahaven-hcptf-iam-management +
# put scoped inline).
# 4. Point TFC_AWS_* back at hcptf-seahaven-site / hcptf-seahaven-site-plan.
# 5. Re-run the script without --allow-workspace to pin trust back to
# iam-bootstrap-prod only.
# This stack has no Lambda execution-role boundary pin.
import {
to = aws_iam_role.hcptf_apply
id = "hcptf-seahaven-site"
}
import {
to = aws_iam_role.hcptf_plan
id = "hcptf-seahaven-site-plan"
}
import {
to = aws_iam_role_policy.hcptf_apply_services
id = "hcptf-seahaven-site:seahaven-site-services"
}
import {
to = aws_iam_role_policy.hcptf_plan_refresh
id = "hcptf-seahaven-site-plan:seahaven-site-plan-refresh"
}
import {
to = aws_iam_role_policy_attachments_exclusive.hcptf_apply
id = "hcptf-seahaven-site"
}
import {
to = aws_iam_role_policy_attachment.hcptf_plan_viewonly
id = "hcptf-seahaven-site-plan/arn:aws:iam::aws:policy/job-function/ViewOnlyAccess"
}
import {
to = aws_iam_role_policy_attachments_exclusive.hcptf_plan
id = "hcptf-seahaven-site-plan"
}
data "aws_iam_policy_document" "hcptf_apply_trust" {
statement {
sid = "HcpApply"
effect = "Allow"
actions = ["sts:AssumeRoleWithWebIdentity"]
principals {
type = "Federated"
identifiers = ["arn:aws:iam::${local.account_id}:oidc-provider/app.terraform.io"]
}
condition {
test = "StringEquals"
variable = "app.terraform.io:aud"
values = ["aws.workload.identity"]
}
condition {
test = "StringEquals"
variable = "app.terraform.io:sub"
values = [
"organization:seahaven:project:seahaven-prod:workspace:seahaven-site-prod:run_phase:apply",
]
}
}
}
data "aws_iam_policy_document" "hcptf_plan_trust" {
statement {
sid = "HcpPlan"
effect = "Allow"
actions = ["sts:AssumeRoleWithWebIdentity"]
principals {
type = "Federated"
identifiers = ["arn:aws:iam::${local.account_id}:oidc-provider/app.terraform.io"]
}
condition {
test = "StringEquals"
variable = "app.terraform.io:aud"
values = ["aws.workload.identity"]
}
condition {
test = "StringEquals"
variable = "app.terraform.io:sub"
values = [
"organization:seahaven:project:seahaven-prod:workspace:seahaven-site-prod:run_phase:plan",
]
}
}
}
data "aws_iam_policy_document" "hcptf_scoped_iam" {
statement {
sid = "DenyCreatePolicy"
effect = "Deny"
actions = ["iam:CreatePolicy", "iam:CreatePolicyVersion"]
resources = ["*"]
}
statement {
sid = "WriteDeployRoles"
effect = "Allow"
actions = [
"iam:AttachRolePolicy",
"iam:DeleteRolePolicy",
"iam:DetachRolePolicy",
"iam:PutRolePolicy",
"iam:TagRole",
"iam:UntagRole",
"iam:UpdateAssumeRolePolicy",
"iam:UpdateRole",
"iam:UpdateRoleDescription",
]
resources = ["arn:aws:iam::${local.account_id}:role/tf-managed/githubdeploy-seahaven-site"]
}
statement {
sid = "IamReadOnly"
effect = "Allow"
actions = [
"iam:GetPolicy",
"iam:GetPolicyVersion",
"iam:GetRole",
"iam:GetRolePolicy",
"iam:ListAttachedRolePolicies",
"iam:ListInstanceProfilesForRole",
"iam:ListPolicies",
"iam:ListPolicyVersions",
"iam:ListRolePolicies",
"iam:ListRoleTags",
"iam:ListRoles",
]
resources = ["*"]
}
statement {
sid = "DenySelfMutation"
effect = "Deny"
actions = [
"iam:AttachRolePolicy",
"iam:DeleteRole",
"iam:DeleteRolePolicy",
"iam:DeleteRolePermissionsBoundary",
"iam:DetachRolePolicy",
"iam:PutRolePolicy",
"iam:PutRolePermissionsBoundary",
"iam:UpdateAssumeRolePolicy",
"iam:UpdateRole",
"iam:UpdateRoleDescription",
]
resources = [
"arn:aws:iam::${local.account_id}:role/hcptf-*",
"arn:aws:iam::${local.account_id}:role/github-cfn-execution-role",
"arn:aws:iam::${local.account_id}:role/githubdeploy-*",
"arn:aws:iam::${local.account_id}:role/cdk-hnb659fds-*",
"arn:aws:iam::${local.account_id}:role/OrganizationAccountAccessRole",
"arn:aws:iam::${local.account_id}:role/seahaven-*",
]
}
statement {
sid = "DenyBoundaryTampering"
effect = "Deny"
actions = [
"iam:DeleteRolePermissionsBoundary",
"iam:DeleteUserPermissionsBoundary",
]
resources = [
"arn:aws:iam::${local.account_id}:role/*",
"arn:aws:iam::${local.account_id}:user/*",
]
}
statement {
sid = "DenyBoundaryPolicyEdit"
effect = "Deny"
actions = [
"iam:CreatePolicyVersion",
"iam:DeletePolicy",
"iam:DeletePolicyVersion",
"iam:SetDefaultPolicyVersion",
]
resources = ["arn:aws:iam::${local.account_id}:policy/seahaven-*"]
}
}
resource "aws_iam_role_policy" "hcptf_apply_services" {
name = "seahaven-site-services"
role = aws_iam_role.hcptf_apply.id
policy = jsonencode({
Version = "2012-10-17"
Statement = [
{
Action = [
"s3:*",
]
Resource = [
"arn:aws:s3:::seahaven-site-prod",
"arn:aws:s3:::seahaven-site-prod/*",
]
Effect = "Allow"
Sid = "OriginBucket"
},
{
Action = [
"iam:GetOpenIDConnectProvider",
]
Resource = [
"arn:aws:iam::${local.account_id}:oidc-provider/token.actions.githubusercontent.com",
]
Effect = "Allow"
Sid = "ReadGithubOidcProvider"
},
{
Action = [
"cloudfront:*",
]
Resource = "*"
Effect = "Allow"
Sid = "CloudFrontManage"
},
{
Condition = {
StringEquals = {
"aws:RequestTag/Project" = "seahaven-site"
}
}
Action = [
"acm:RequestCertificate",
]
Resource = "*"
Effect = "Allow"
Sid = "AcmCreate"
},
{
Action = [
"acm:ListCertificates",
"acm:ListTagsForCertificate",
]
Resource = "*"
Effect = "Allow"
Sid = "AcmList"
},
{
Condition = {
StringEquals = {
"aws:ResourceTag/Project" = "seahaven-site"
}
}
Action = [
"acm:DescribeCertificate",
"acm:GetCertificate",
"acm:DeleteCertificate",
"acm:AddTagsToCertificate",
"acm:RemoveTagsFromCertificate",
"acm:RenewCertificate",
]
Resource = "*"
Effect = "Allow"
Sid = "AcmManageTagged"
},
{
Action = [
"ssm:GetParameter",
"ssm:GetParameters",
]
Resource = [
"arn:aws:ssm:us-east-1:${local.account_id}:parameter/seahaven/waf/app-web-acl-arn",
]
Effect = "Allow"
Sid = "ReadAppWebAclSsm"
},
{
Action = [
"wafv2:GetWebACL",
"wafv2:GetWebACLForResource",
"wafv2:ListWebACLs",
"wafv2:ListResourcesForWebACL",
]
Resource = "*"
Effect = "Allow"
Sid = "ReadWafWebAcl"
},
]
})
}
resource "aws_iam_role_policy" "hcptf_plan_refresh" {
name = "seahaven-site-plan-refresh"
role = aws_iam_role.hcptf_plan.id
policy = jsonencode({
Version = "2012-10-17"
Statement = [
{
Action = [
"iam:GetRole",
"iam:GetRolePolicy",
"iam:ListRolePolicies",
"iam:ListAttachedRolePolicies",
"iam:ListRoleTags",
]
Resource = [
"arn:aws:iam::${local.account_id}:role/tf-managed/githubdeploy-seahaven-site",
"arn:aws:iam::${local.account_id}:role/hcptf-seahaven-site",
"arn:aws:iam::${local.account_id}:role/hcptf-seahaven-site-plan",
]
Effect = "Allow"
Sid = "RefreshDeployRole"
},
{
Action = [
"iam:GetOpenIDConnectProvider",
]
Resource = [
"arn:aws:iam::${local.account_id}:oidc-provider/token.actions.githubusercontent.com",
]
Effect = "Allow"
Sid = "RefreshGithubOidcProvider"
},
{
Action = [
"iam:GetPolicy",
"iam:GetPolicyVersion",
]
Resource = "*"
Effect = "Allow"
Sid = "RefreshManagedPolicies"
},
{
Action = [
"s3:Get*",
"s3:ListBucket",
]
Resource = [
"arn:aws:s3:::seahaven-site-prod",
"arn:aws:s3:::seahaven-site-prod/*",
]
Effect = "Allow"
Sid = "RefreshOriginBucket"
},
{
Action = [
"cloudfront:Get*",
"cloudfront:List*",
]
Resource = "*"
Effect = "Allow"
Sid = "RefreshCloudFront"
},
{
Action = [
"cloudfront:DescribeFunction",
]
Resource = [
"arn:aws:cloudfront::${local.account_id}:function/seahaven-site-prod-directory-index",
]
Effect = "Allow"
Sid = "RefreshCloudFrontFunction"
},
{
Action = [
"acm:DescribeCertificate",
"acm:ListCertificates",
"acm:ListTagsForCertificate",
"acm:GetCertificate",
]
Resource = "*"
Effect = "Allow"
Sid = "RefreshAcm"
},
{
Action = [
"ssm:GetParameter",
"ssm:GetParameters",
]
Resource = [
"arn:aws:ssm:us-east-1:${local.account_id}:parameter/seahaven/waf/app-web-acl-arn",
]
Effect = "Allow"
Sid = "RefreshAppWebAclSsm"
},
{
Action = [
"wafv2:GetWebACL",
"wafv2:ListWebACLs",
]
Resource = "*"
Effect = "Allow"
Sid = "RefreshWafWebAcl"
},
]
})
}
resource "aws_iam_role" "hcptf_apply" {
name = "hcptf-seahaven-site"
assume_role_policy = data.aws_iam_policy_document.hcptf_apply_trust.json
max_session_duration = 3600
tags = {
Project = "seahaven-site"
Owner = "adam@seahavenind.com"
ManagedBy = "terraform"
}
}
# Empty exclusive set keeps seahaven-hcptf-iam-management detached.
resource "aws_iam_role_policy_attachments_exclusive" "hcptf_apply" {
role_name = aws_iam_role.hcptf_apply.name
policy_arns = []
}
resource "aws_iam_role" "hcptf_plan" {
name = "hcptf-seahaven-site-plan"
assume_role_policy = data.aws_iam_policy_document.hcptf_plan_trust.json
max_session_duration = 3600
tags = {
Project = "seahaven-site"
Owner = "adam@seahavenind.com"
ManagedBy = "terraform"
}
}
resource "aws_iam_role_policy_attachment" "hcptf_plan_viewonly" {
role = aws_iam_role.hcptf_plan.name
policy_arn = "arn:aws:iam::aws:policy/job-function/ViewOnlyAccess"
}
resource "aws_iam_role_policy_attachments_exclusive" "hcptf_plan" {
role_name = aws_iam_role.hcptf_plan.name
policy_arns = [
aws_iam_role_policy_attachment.hcptf_plan_viewonly.policy_arn,
]
}
resource "aws_iam_role_policy" "hcptf_scoped_iam" {
name = "scoped-iam-management"
role = aws_iam_role.hcptf_apply.id
policy = data.aws_iam_policy_document.hcptf_scoped_iam.json
}

View file

@ -1,5 +1,8 @@
data "aws_iam_policy_document" "github_deploy_assume" {
# Legacy branch trust. Remove after one deploy through cd-hcp-static has
# succeeded. The branch subject stays until EnvironmentProd is applied.
statement {
sid = "LegacyBranch"
effect = "Allow"
actions = ["sts:AssumeRoleWithWebIdentity"]
@ -20,25 +23,56 @@ data "aws_iam_policy_document" "github_deploy_assume" {
values = ["repo:${var.github_repo}:ref:refs/heads/${var.github_deploy_branch}"]
}
}
statement {
sid = "EnvironmentProd"
effect = "Allow"
actions = ["sts:AssumeRoleWithWebIdentity"]
principals {
type = "Federated"
identifiers = [data.aws_iam_openid_connect_provider.github.arn]
}
condition {
test = "StringEquals"
variable = "token.actions.githubusercontent.com:aud"
values = ["sts.amazonaws.com"]
}
condition {
test = "StringEquals"
variable = "token.actions.githubusercontent.com:sub"
values = ["repo:${var.github_repo}:environment:prod"]
}
condition {
test = "StringLike"
variable = "token.actions.githubusercontent.com:job_workflow_ref"
values = ["Sea-Haven-Industries/.github/.github/workflows/cd-hcp-static.yaml@*"]
}
}
}
resource "aws_iam_policy" "github_deploy_boundary" {
name = "seahaven-site-githubdeploy-boundary"
path = "/tf-managed/"
description = "Permissions boundary for githubdeploy-seahaven-site"
policy = data.aws_iam_policy_document.github_deploy.json
# Request tag the hcptf apply role requires before it may CreatePolicy.
tags = {
BoundaryFor = "githubdeploy-seahaven-site"
}
}
resource "aws_iam_role" "github_deploy" {
name = local.deploy_role
path = "/tf-managed/"
description = "GitHub Actions content-deploy role for ${var.github_repo}@${var.github_deploy_branch}"
description = "GitHub Actions content-deploy role for ${var.github_repo} Environment prod"
assume_role_policy = data.aws_iam_policy_document.github_deploy_assume.json
permissions_boundary = aws_iam_policy.github_deploy_boundary.arn
max_session_duration = 3600
# Not a Lambda execution role. Config omits permissions_boundary so a later
# apply will not PutRolePermissionsBoundary the Lambda ceiling back. Live still
# has seahaven-lambda-execution-boundary; omitting without ignore_changes would
# plan DeleteRolePermissionsBoundary, which hcptf-seahaven-site is denied
# (DenyBoundaryTampering). Ignore the attribute so this apply does not touch
# the ceiling. An administrator deletes the live attachment, then a follow-up
# drops this lifecycle after refresh-only updates state to null.
lifecycle {
ignore_changes = [permissions_boundary]
}
}
data "aws_iam_policy_document" "github_deploy" {
@ -70,10 +104,21 @@ data "aws_iam_policy_document" "github_deploy" {
effect = "Allow"
actions = [
"cloudfront:CreateInvalidation",
"cloudfront:GetDistribution",
"cloudfront:GetInvalidation",
]
resources = [aws_cloudfront_distribution.site.arn]
}
statement {
sid = "ReadDeployContract"
effect = "Allow"
actions = [
"ssm:GetParameter",
"ssm:GetParameters",
]
resources = ["arn:aws:ssm:${var.aws_region}:${local.account_id}:parameter/seahaven-site/deploy/*"]
}
}
resource "aws_iam_role_policy" "github_deploy" {

67
terraform/removed.tf Normal file
View file

@ -0,0 +1,67 @@
# hcptf-seahaven-site and hcptf-seahaven-site-plan moved to the
# seahaven-site-hcptf stack in seahaven-org-baseline (PLAT-225).
# Forget them here. Do not delete the live roles.
removed {
from = aws_iam_role.hcptf_apply
lifecycle {
destroy = false
}
}
removed {
from = aws_iam_role.hcptf_plan
lifecycle {
destroy = false
}
}
removed {
from = aws_iam_role_policy.hcptf_apply_services
lifecycle {
destroy = false
}
}
removed {
from = aws_iam_role_policy.hcptf_scoped_iam
lifecycle {
destroy = false
}
}
removed {
from = aws_iam_role_policy.hcptf_plan_refresh
lifecycle {
destroy = false
}
}
removed {
from = aws_iam_role_policy_attachment.hcptf_plan_viewonly
lifecycle {
destroy = false
}
}
removed {
from = aws_iam_role_policy_attachments_exclusive.hcptf_apply
lifecycle {
destroy = false
}
}
removed {
from = aws_iam_role_policy_attachments_exclusive.hcptf_plan
lifecycle {
destroy = false
}
}

14
terraform/ssm.tf Normal file
View file

@ -0,0 +1,14 @@
# Names the static-site deploy workflow reads. Terraform does not manage the
# objects in the bucket, so a content deploy does not change this plan.
resource "aws_ssm_parameter" "deploy_bucket" {
name = "/seahaven-site/deploy/bucket"
type = "String"
value = aws_s3_bucket.origin.bucket
}
resource "aws_ssm_parameter" "deploy_distribution_id" {
name = "/seahaven-site/deploy/distribution-id"
type = "String"
value = aws_cloudfront_distribution.site.id
}