Compare commits

..

No commits in common. "main" and "v1.0.20" have entirely different histories.

28 changed files with 50 additions and 904 deletions

View file

@ -1,6 +0,0 @@
# actionlint 1.7.12 rejects `$/`, which GitHub accepts as a self-repository
# action reference (runner 2.336.0+). Drop this ignore when a release parses it.
paths:
.github/workflows/ci-terraform.yaml:
ignore:
- 'specifying action "\$/.github/actions/app-terraform-isolation" in invalid format because ref is missing'

View file

@ -1,64 +0,0 @@
name: App and Terraform isolation
description: Fail when a change set mixes Terraform with deployable application files.
inputs:
app-paths:
description: Newline-separated deployable paths. A trailing slash is a prefix. Any other entry is an exact file.
required: true
terraform-dir:
description: Directory containing Terraform sources.
required: true
default: terraform
event-name:
description: github.event_name from the calling workflow.
required: true
pr-base-sha:
description: pull_request base SHA. Empty outside pull_request.
required: false
default: ""
merge-group-base-sha:
description: merge_group base SHA. Empty outside merge_group.
required: false
default: ""
runs:
using: composite
steps:
- name: Classify changed paths
shell: bash
working-directory: ${{ github.workspace }}
env:
APP_PATHS: ${{ inputs.app-paths }}
TERRAFORM_DIR: ${{ inputs.terraform-dir }}
EVENT_NAME: ${{ inputs.event-name }}
PR_BASE_SHA: ${{ inputs.pr-base-sha }}
MERGE_GROUP_BASE_SHA: ${{ inputs.merge-group-base-sha }}
CHECKER: ${{ github.action_path }}/check_app_terraform_isolation.py
run: |
set -euo pipefail
classify() {
python3 "${CHECKER}"
}
case "${EVENT_NAME}" in
pull_request)
if [[ -z "${PR_BASE_SHA}" ]]; then
echo "FAIL: pull_request base SHA is empty" >&2
exit 1
fi
merge_base="$(git merge-base "${PR_BASE_SHA}" HEAD)"
git diff --name-only --diff-filter=ACMRD "${merge_base}" HEAD | classify
;;
merge_group)
if [[ -z "${MERGE_GROUP_BASE_SHA}" ]]; then
echo "FAIL: merge_group base SHA is empty" >&2
exit 1
fi
while IFS= read -r sha; do
[[ -z "${sha}" ]] && continue
git diff --name-only --diff-filter=ACMRD "${sha}^" "${sha}" | classify
done < <(git rev-list --reverse --first-parent "${MERGE_GROUP_BASE_SHA}..HEAD")
;;
*)
echo "SKIP: live isolation runs on pull_request and merge_group (event: ${EVENT_NAME})"
;;
esac

View file

@ -1,117 +0,0 @@
#!/usr/bin/env python3
"""Fail when a change set mixes Terraform with deployable application files.
APP_PATHS is newline-separated. A trailing slash is a directory prefix.
Any other entry is an exact file. Paths that are not listed are neutral, so
workflows, docs, and tests may travel with either side. An empty APP_PATHS
skips the check.
TERRAFORM_DIR is the Terraform working directory (default terraform). A path
is Terraform when it equals that directory or sits under it.
"""
from __future__ import annotations
import argparse
import os
import sys
def parse_app_rules(raw: str) -> tuple[frozenset[str], frozenset[str]]:
prefixes: set[str] = set()
exact: set[str] = set()
for line in raw.splitlines():
item = line.strip().replace("\\", "/")
if not item or item.startswith("#"):
continue
if item.endswith("/"):
prefixes.add(item)
else:
exact.add(item)
return frozenset(prefixes), frozenset(exact)
def terraform_prefix(terraform_dir: str) -> str:
prefix = terraform_dir.replace("\\", "/").strip().strip("/")
return prefix or "terraform"
def is_terraform_path(path: str, terraform_dir: str = "terraform") -> bool:
normalized = path.replace("\\", "/")
prefix = terraform_prefix(terraform_dir)
return normalized == prefix or normalized.startswith(f"{prefix}/")
def is_app_path(path: str, prefixes: frozenset[str], exact: frozenset[str]) -> bool:
normalized = path.replace("\\", "/")
if normalized in exact:
return True
for prefix in prefixes:
if normalized.startswith(prefix) or f"{normalized}/" == prefix:
return True
return False
def isolation_violation(
paths: list[str],
app_paths: str,
terraform_dir: str = "terraform",
) -> tuple[list[str], list[str]] | None:
prefixes, exact = parse_app_rules(app_paths)
if not prefixes and not exact:
return None
terraform_files = sorted(
{path for path in paths if is_terraform_path(path, terraform_dir)}
)
app_files = sorted({path for path in paths if is_app_path(path, prefixes, exact)})
if terraform_files and app_files:
return terraform_files, app_files
return None
def first_isolation_violation(
file_sets: list[list[str]],
app_paths: str,
terraform_dir: str = "terraform",
) -> tuple[list[str], list[str]] | None:
for paths in file_sets:
violation = isolation_violation(paths, app_paths, terraform_dir)
if violation is not None:
return violation
return None
def main() -> int:
parser = argparse.ArgumentParser()
parser.add_argument(
"paths",
nargs="*",
help="Changed paths. Omit and pass newline-separated paths on stdin.",
)
args = parser.parse_args()
paths = list(args.paths)
if not paths and not sys.stdin.isatty():
paths = [line.strip() for line in sys.stdin if line.strip()]
terraform_dir = terraform_prefix(os.environ.get("TERRAFORM_DIR", "terraform"))
violation = isolation_violation(
paths, os.environ.get("APP_PATHS", ""), terraform_dir
)
if violation is None:
print("PASS: application and Terraform changes are isolated")
return 0
terraform_files, app_files = violation
print(
f"FAIL: do not mix deployable application files with {terraform_dir}/",
file=sys.stderr,
)
print("terraform:", file=sys.stderr)
for path in terraform_files:
print(f" {path}", file=sys.stderr)
print("application:", file=sys.stderr)
for path in app_files:
print(f" {path}", file=sys.stderr)
return 1
if __name__ == "__main__":
raise SystemExit(main())

View file

@ -216,7 +216,7 @@ jobs:
echo "ship-gate: hotfix/release path accepted (${PREV} -> ${TAG} from ${TARGET_COMMITISH:-branch})"
- name: Configure AWS credentials using OIDC
uses: aws-actions/configure-aws-credentials@e1253824e5c10ff9df46874f81ed3ec929e19cfd # v6.3.0
uses: aws-actions/configure-aws-credentials@cbe3b392738ccf3f987d68400dafcf4b0624a56c # v6.2.4
with:
role-to-assume: ${{ env.DEPLOY_ROLE_ARN }}
aws-region: us-east-1
@ -255,10 +255,10 @@ jobs:
} >> "${GITHUB_OUTPUT}"
- name: Set up QEMU
uses: docker/setup-qemu-action@99012661954931238ded8c8b007157a8430204e1 # v4.4.0
uses: docker/setup-qemu-action@1f40c72289eff860ee54a304f1438e3cff362e0a # v4.3.0
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@8d2750c68a42422c14e847fe6c8ac0403b4cbd6f # v3.12.0
uses: docker/setup-buildx-action@e468171a9de216ec08956ac3ada2f0791b6bd435 # v3.11.1
- name: Login to Amazon ECR
uses: aws-actions/amazon-ecr-login@03f1aad4c6c7ffd436567f42f9384779290529bd # v2.1.7

View file

@ -1,291 +0,0 @@
name: CD — HCP Lambda (Python)
# Reusable Python Lambda zip CD for HCP app repos. The caller owns triggers and
# passes `environment` as a `with:` input. This job owns `environment:`,
# concurrency, OIDC, and `vars.DEPLOY_ROLE_ARN`. GitHub rejects `environment:`
# beside `uses:`.
#
# Caller example (one job per GitHub Environment):
# jobs:
# deploy-prod:
# uses: Sea-Haven-Industries/.github/.github/workflows/cd-hcp-lambda-python.yaml@<sha> # vX.Y.Z
# permissions: { contents: read, id-token: write }
# secrets: inherit
# with:
# environment: prod
# ref: ${{ github.event.release.tag_name || inputs.ref }}
# ssm-prefix: /paychex-integrations/deploy
# function-keys: webhook_ingest,webhook_processor
# ship-gate: true
#
# The caller repo must provide scripts/package_lambdas.sh, which accepts
# `--git-sha <sha> --out-dir <dir> --only <key>...`, writes <dir>/<key>.zip,
# and embeds the commit in build_info.py inside each zip. Terraform owns the
# functions and ignores code attributes. SSM under ssm-prefix supplies
# artifacts-bucket and <key>-function-name.
#
# Lambda reports CodeSha256 as the base64 SHA-256 of the uploaded zip. After
# update-function-code settles, each function's CodeSha256 must equal the
# local zip digest. That is the live-state check for functions with no
# health URL.
#
# Nothing here creates an HCP run.
on:
workflow_call:
inputs:
environment:
description: "GitHub Environment to deploy to (dev, staging, prod)"
type: string
required: true
ref:
description: "Git ref to build. Empty means github.sha."
type: string
required: false
default: ""
ssm-prefix:
description: "SSM prefix for deploy parameters (e.g. /payments-dashboard/deploy)"
type: string
required: true
function-keys:
description: "Comma-separated package keys. Each maps to SSM <prefix>/<key>-function-name."
type: string
required: true
python-version:
description: "Python version for setup-python and the packager"
type: string
required: false
default: "3.12"
ship-gate:
description: "Require the ref to be on main or a legal hotfix/release tag"
type: boolean
required: false
default: false
permissions:
contents: read
id-token: write
jobs:
deploy:
name: Deploy Lambda to ${{ inputs.environment }}
runs-on: ubuntu-latest
timeout-minutes: 30
environment: ${{ inputs.environment }}
concurrency:
group: deploy-${{ inputs.ssm-prefix }}-${{ inputs.environment }}
cancel-in-progress: false
env:
AWS_REGION: us-east-1
DEPLOY_ROLE_ARN: ${{ vars.DEPLOY_ROLE_ARN }}
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
ref: ${{ inputs.ref != '' && inputs.ref || github.sha }}
persist-credentials: false
fetch-tags: true
- name: Resolve commit
id: commit
run: |
set -euo pipefail
sha="$(git rev-parse HEAD)"
echo "sha=${sha}" >> "${GITHUB_OUTPUT}"
echo "Building ${sha}"
- name: Ship-gate
if: ${{ inputs.ship-gate }}
env:
GH_TOKEN: ${{ github.token }}
REPO: ${{ github.repository }}
INPUT_REF: ${{ inputs.ref != '' && inputs.ref || github.sha }}
ENVIRONMENT: ${{ inputs.environment }}
HEAD_SHA: ${{ steps.commit.outputs.sha }}
run: |
set -euo pipefail
status="$(gh api "repos/${REPO}/compare/main...${INPUT_REF}" --jq .status)"
if [ "${status}" = "behind" ] || [ "${status}" = "identical" ]; then
echo "ship-gate: ${INPUT_REF} is ${status} relative to main"
exit 0
fi
echo "ship-gate: ${INPUT_REF} is not on main (compare status: ${status}); checking hotfix/release path"
TAG="${INPUT_REF}"
if [[ ! "${TAG}" =~ ^v ]]; then
TAG="$(git describe --tags --exact-match "${HEAD_SHA}" 2>/dev/null || true)"
fi
if [ "${ENVIRONMENT}" = "staging" ]; then
PATTERN='^v[0-9]+\.[0-9]+\.[0-9]+-staging$'
else
PATTERN='^v[0-9]+\.[0-9]+\.[0-9]+$'
fi
if [[ -z "${TAG}" ]] || [[ ! "${TAG}" =~ ${PATTERN} ]]; then
echo "ship-gate: ref ${INPUT_REF} (tag ${TAG:-none}) does not match ${PATTERN} for environment ${ENVIRONMENT} and is not on main" >&2
exit 1
fi
export PATTERN TAG
# Newest matching release that is an ancestor of TAG. The highest
# release overall is not that ancestor when a hotfix is cut from an
# older line (v2.0.0 exists, v1.2.1 is cut from v1.2.0).
CANDIDATES="$(
gh api "repos/${REPO}/releases" --paginate --jq '.[].tag_name' | python3 -c '
import os, re, sys
pattern = re.compile(os.environ["PATTERN"])
current = os.environ["TAG"]
tags = [
line.strip()
for line in sys.stdin
if pattern.fullmatch(line.strip()) and line.strip() != current
]
def key(tag):
body = tag[1:]
core = body.split("-", 1)[0]
return tuple(int(part) for part in core.split("."))
tags.sort(key=key, reverse=True)
print("\n".join(tags))
'
)"
PREV=""
if [ -n "${CANDIDATES}" ]; then
while IFS= read -r candidate; do
if [ -z "${candidate}" ]; then
continue
fi
candidate_status="$(gh api "repos/${REPO}/compare/${candidate}...${TAG}" --jq .status)"
if [ "${candidate_status}" = "ahead" ]; then
PREV="${candidate}"
break
fi
done <<< "${CANDIDATES}"
fi
if [ -z "${PREV}" ]; then
echo "ship-gate: no prior ${PATTERN} release is an ancestor of ${TAG}" >&2
exit 1
fi
echo "ship-gate: ${TAG} is ahead of ${PREV}"
from_train=false
TARGET_COMMITISH="$(gh api "repos/${REPO}/releases/tags/${TAG}" --jq .target_commitish 2>/dev/null || true)"
if [[ "${TARGET_COMMITISH}" == hotfix/* || "${TARGET_COMMITISH}" == release/* ]]; then
from_train=true
fi
if [ "${from_train}" = false ]; then
git fetch origin '+refs/heads/hotfix/*:refs/remotes/origin/hotfix/*' '+refs/heads/release/*:refs/remotes/origin/release/*' || true
if git branch -r --contains "${HEAD_SHA}" | grep -Eq 'origin/(hotfix|release)/'; then
from_train=true
fi
fi
if [ "${from_train}" = false ]; then
echo "ship-gate: ${TAG} was not created from hotfix/* or release/* (target_commitish=${TARGET_COMMITISH:-none})" >&2
exit 1
fi
echo "ship-gate: hotfix/release path accepted (${PREV} -> ${TAG} from ${TARGET_COMMITISH:-branch})"
- uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0
with:
python-version: ${{ inputs.python-version }}
- name: Build function zips
env:
GIT_SHA: ${{ steps.commit.outputs.sha }}
FUNCTION_KEYS: ${{ inputs.function-keys }}
run: |
set -euo pipefail
if [ -z "${FUNCTION_KEYS}" ]; then
echo "function-keys is required" >&2
exit 1
fi
keys=()
IFS=',' read -r -a raw_keys <<< "${FUNCTION_KEYS}"
for raw in "${raw_keys[@]}"; do
key="${raw#"${raw%%[![:space:]]*}"}"
key="${key%"${key##*[![:space:]]}"}"
if [ -z "${key}" ]; then
echo "function-keys contains an empty key" >&2
exit 1
fi
if [[ ! "${key}" =~ ^[A-Za-z0-9_]+$ ]]; then
echo "invalid function key: ${key}" >&2
exit 1
fi
keys+=("${key}")
done
if [ "${#keys[@]}" -eq 0 ]; then
echo "function-keys is empty" >&2
exit 1
fi
clean="$(IFS=,; echo "${keys[*]}")"
echo "keys=${clean}" >> "${GITHUB_ENV}"
cmd=(bash scripts/package_lambdas.sh --git-sha "${GIT_SHA}" --out-dir build/packages)
for key in "${keys[@]}"; do
cmd+=(--only "${key}")
done
"${cmd[@]}"
FUNCTION_KEYS_CLEAN="${clean}" python3 - <<'PY'
import os, zipfile
from pathlib import Path
sha = os.environ["GIT_SHA"]
keys = [part for part in os.environ["FUNCTION_KEYS_CLEAN"].split(",") if part]
for name in keys:
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("build_info.py").decode()
if sha not in info:
raise SystemExit(f"{path} missing GIT_SHA {sha}")
print("zips ok")
PY
- name: Configure AWS credentials using OIDC
uses: aws-actions/configure-aws-credentials@cbe3b392738ccf3f987d68400dafcf4b0624a56c # v6.2.4
with:
role-to-assume: ${{ env.DEPLOY_ROLE_ARN }}
aws-region: us-east-1
audience: sts.amazonaws.com
- name: Upload zips and update function code
env:
SSM_PREFIX: ${{ inputs.ssm-prefix }}
GIT_SHA: ${{ steps.commit.outputs.sha }}
FUNCTION_KEYS_CLEAN: ${{ env.keys }}
run: |
set -euo pipefail
prefix="${SSM_PREFIX%/}"
bucket="$(aws ssm get-parameter --name "${prefix}/artifacts-bucket" --query Parameter.Value --output text)"
IFS=',' read -r -a keys <<< "${FUNCTION_KEYS_CLEAN}"
for key in "${keys[@]}"; do
fn="$(aws ssm get-parameter --name "${prefix}/${key}-function-name" --query Parameter.Value --output text)"
s3_key="functions/${key}/${GIT_SHA}.zip"
aws s3 cp "build/packages/${key}.zip" "s3://${bucket}/${s3_key}"
aws lambda update-function-code \
--function-name "${fn}" \
--s3-bucket "${bucket}" \
--s3-key "${s3_key}" \
--query '{Function:FunctionName,Sha256:CodeSha256,Updated:LastModified}' \
--output table
aws lambda wait function-updated-v2 --function-name "${fn}"
expected="$(openssl dgst -sha256 -binary "build/packages/${key}.zip" | base64)"
read -r actual status < <(aws lambda get-function-configuration --function-name "${fn}" \
--query '[CodeSha256,LastUpdateStatus]' --output text)
if [ "${status}" != "Successful" ]; then
echo "${fn}: LastUpdateStatus is ${status}" >&2
exit 1
fi
if [ "${actual}" != "${expected}" ]; then
echo "${fn}: CodeSha256 ${actual} does not match local zip ${expected}" >&2
exit 1
fi
echo "${fn}: CodeSha256 ${actual} matches ${GIT_SHA}"
done

View file

@ -244,7 +244,7 @@ jobs:
PY
- name: Configure AWS credentials using OIDC
uses: aws-actions/configure-aws-credentials@e1253824e5c10ff9df46874f81ed3ec929e19cfd # v6.3.0
uses: aws-actions/configure-aws-credentials@cbe3b392738ccf3f987d68400dafcf4b0624a56c # v6.2.4
with:
role-to-assume: ${{ env.DEPLOY_ROLE_ARN }}
aws-region: us-east-1

View file

@ -225,7 +225,7 @@ jobs:
echo "dist/index.html sha256=${index_sha}"
- name: Configure AWS credentials using OIDC
uses: aws-actions/configure-aws-credentials@e1253824e5c10ff9df46874f81ed3ec929e19cfd # v6.3.0
uses: aws-actions/configure-aws-credentials@cbe3b392738ccf3f987d68400dafcf4b0624a56c # v6.2.4
with:
role-to-assume: ${{ env.DEPLOY_ROLE_ARN }}
aws-region: us-east-1

View file

@ -22,34 +22,6 @@ name: CD — HCP static site
# min-file-count: 40
# ship-gate: true
#
# Small static trees without an index page (for example an MTA-STS policy
# host) set `index-required: false` and point `verify-path` at the file that
# proves the release landed. A matrix caller can deploy several buckets from
# one workflow because concurrency is keyed on `ssm-prefix`:
# jobs:
# deploy-prod:
# strategy:
# fail-fast: false
# matrix:
# include:
# - { domain: example.com, slug: example-com }
# - { domain: example.org, slug: example-org }
# uses: Sea-Haven-Industries/.github/.github/workflows/cd-hcp-static.yaml@<sha> # vX.Y.Z
# permissions: { contents: read, id-token: write }
# secrets: inherit
# with:
# environment: prod
# ssm-prefix: /mta-sts/deploy/${{ matrix.slug }}
# output-dir: dist/${{ matrix.domain }}
# required-paths: dist/${{ matrix.domain }}/.well-known/mta-sts.txt
# min-file-count: 1
# index-required: false
# verify-path: /.well-known/mta-sts.txt
# ship-gate: true
#
# `build-command` runs under `bash -euo pipefail` and still needs a
# package-lock.json in the caller for the setup-node npm cache.
#
# Nothing here creates an HCP run. Terraform owns the bucket and distribution.
on:
@ -88,21 +60,6 @@ on:
type: number
required: false
default: 1
build-command:
description: "Shell command that produces output-dir"
type: string
required: false
default: "npm ci --ignore-scripts && npm run build"
index-required:
description: "Require index.html at the output-dir and bucket root"
type: boolean
required: false
default: true
verify-path:
description: "Served path whose sha256 must match the built file after invalidation. A trailing slash means index.html."
type: string
required: false
default: "/"
permissions:
contents: read
@ -226,39 +183,15 @@ jobs:
node-version: "24"
cache: npm
- name: Resolve verify path
env:
VERIFY_PATH: ${{ inputs.verify-path }}
run: |
set -euo pipefail
verify_path="${VERIFY_PATH}"
case "${verify_path}" in
/*) ;;
*) verify_path="/${verify_path}" ;;
esac
case "${verify_path}" in
*/) verify_key="${verify_path#/}index.html" ;;
*) verify_key="${verify_path#/}" ;;
esac
case "${verify_key}" in
*..*) echo "verify-path must not contain '..': ${VERIFY_PATH}" >&2; exit 1 ;;
esac
{
echo "VERIFY_URL_PATH=${verify_path}"
echo "VERIFY_KEY=${verify_key}"
} >> "${GITHUB_ENV}"
echo "verify ${verify_path} against ${verify_key}"
- name: Build site
env:
BUILD_COMMAND: ${{ inputs.build-command }}
OUTPUT_DIR: ${{ inputs.output-dir }}
REQUIRED_PATHS: ${{ inputs.required-paths }}
MIN_FILE_COUNT: ${{ inputs.min-file-count }}
INDEX_REQUIRED: ${{ inputs.index-required }}
run: |
set -euo pipefail
bash -euo pipefail -c "${BUILD_COMMAND}"
npm ci --ignore-scripts
npm run build
python3 - <<'PY'
import os, sys
output_dir = os.environ["OUTPUT_DIR"]
@ -281,21 +214,17 @@ jobs:
print(f"build produced only {count} files (expected >= {minimum})", file=sys.stderr)
sys.exit(1)
index = os.path.join(output_dir, "index.html")
if os.environ["INDEX_REQUIRED"] == "true" and not os.path.isfile(index):
if not os.path.isfile(index):
print(f"missing {index}", file=sys.stderr)
sys.exit(1)
verify_file = os.path.join(output_dir, os.environ["VERIFY_KEY"])
if not os.path.isfile(verify_file):
print(f"missing verify file {verify_file}", file=sys.stderr)
sys.exit(1)
print(f"Build OK: {count} files.")
PY
verify_sha="$(python3 -c 'import hashlib,os,pathlib; print(hashlib.sha256(pathlib.Path(os.environ["OUTPUT_DIR"], os.environ["VERIFY_KEY"]).read_bytes()).hexdigest())')"
echo "VERIFY_SHA256=${verify_sha}" >> "${GITHUB_ENV}"
echo "${OUTPUT_DIR}/${VERIFY_KEY} sha256=${verify_sha}"
index_sha="$(python3 -c 'import hashlib,os,pathlib; print(hashlib.sha256(pathlib.Path(os.environ["OUTPUT_DIR"], "index.html").read_bytes()).hexdigest())')"
echo "INDEX_SHA256=${index_sha}" >> "${GITHUB_ENV}"
echo "${OUTPUT_DIR}/index.html sha256=${index_sha}"
- name: Configure AWS credentials using OIDC
uses: aws-actions/configure-aws-credentials@e1253824e5c10ff9df46874f81ed3ec929e19cfd # v6.3.0
uses: aws-actions/configure-aws-credentials@cbe3b392738ccf3f987d68400dafcf4b0624a56c # v6.2.4
with:
role-to-assume: ${{ env.DEPLOY_ROLE_ARN }}
aws-region: us-east-1
@ -328,7 +257,6 @@ jobs:
env:
SITE_BUCKET: ${{ steps.deploy.outputs.bucket }}
OUTPUT_DIR: ${{ inputs.output-dir }}
INDEX_REQUIRED: ${{ inputs.index-required }}
run: |
set -euo pipefail
aws s3 sync "${OUTPUT_DIR}/" "s3://${SITE_BUCKET}/" --no-progress \
@ -338,10 +266,7 @@ jobs:
--exclude "*" --include "*.html" --include "*.xml" --include "*.txt" \
--cache-control "no-cache"
aws s3 sync "${OUTPUT_DIR}/" "s3://${SITE_BUCKET}/" --no-progress --delete
aws s3api head-object --bucket "${SITE_BUCKET}" --key "${VERIFY_KEY}"
if [ "${INDEX_REQUIRED}" = "true" ] && [ "${VERIFY_KEY}" != "index.html" ]; then
aws s3api head-object --bucket "${SITE_BUCKET}" --key index.html
fi
- name: Invalidate CloudFront
env:
@ -361,7 +286,7 @@ jobs:
env:
DISTRIBUTION_ID: ${{ steps.deploy.outputs.distribution_id }}
SITE_URL: ${{ steps.deploy.outputs.site_url }}
EXPECTED_SHA256: ${{ env.VERIFY_SHA256 }}
EXPECTED_INDEX_SHA256: ${{ env.INDEX_SHA256 }}
run: |
set -euo pipefail
SITE_URL="${SITE_URL%/}"
@ -372,16 +297,16 @@ jobs:
last_hash="Unknown"
for attempt in $(seq 1 40); do
last_status="$(aws cloudfront get-distribution --id "${DISTRIBUTION_ID}" --query Distribution.Status --output text)"
if last_hash="$(curl -fsS --max-time 30 "${SITE_URL}${VERIFY_URL_PATH}" | sha256_of)" && [ -n "${last_hash}" ]; then
if last_hash="$(curl -fsS --max-time 30 "${SITE_URL}/" | sha256_of)" && [ -n "${last_hash}" ]; then
:
else
last_hash="unreachable"
fi
echo "poll ${attempt}/40: status=${last_status} path=${VERIFY_URL_PATH} served_sha256=${last_hash}"
if [ "${last_status}" = "Deployed" ] && [ "${last_hash}" = "${EXPECTED_SHA256}" ]; then
echo "poll ${attempt}/40: status=${last_status} served_sha256=${last_hash}"
if [ "${last_status}" = "Deployed" ] && [ "${last_hash}" = "${EXPECTED_INDEX_SHA256}" ]; then
exit 0
fi
sleep 15
done
echo "release did not converge: status=${last_status} path=${VERIFY_URL_PATH} served_sha256=${last_hash} expected=${EXPECTED_SHA256}" >&2
echo "release did not converge: status=${last_status} served_sha256=${last_hash} expected=${EXPECTED_INDEX_SHA256}" >&2
exit 1

View file

@ -82,7 +82,7 @@ jobs:
cache: npm
cache-dependency-path: ${{ inputs.cache-dependency-path }}
- uses: ruby/setup-ruby@14594264cd68ce8a2345dd349bc3d138a4ef85c8 # v1.327.0
- uses: ruby/setup-ruby@a0102e0972be65f351c307e2d64b9314a57c8073 # v1.324.0
with:
ruby-version: ${{ inputs.ruby-version }}
bundler-cache: true

View file

@ -214,7 +214,7 @@ jobs:
cache: npm
cache-dependency-path: ${{ inputs.cache-dependency-path }}
- uses: ruby/setup-ruby@14594264cd68ce8a2345dd349bc3d138a4ef85c8 # v1.327.0
- uses: ruby/setup-ruby@a0102e0972be65f351c307e2d64b9314a57c8073 # v1.324.0
with:
ruby-version: ${{ inputs.ruby-version }}
bundler-cache: true

View file

@ -10,20 +10,6 @@ name: CI — Terraform
# uses: Sea-Haven-Industries/.github/.github/workflows/ci-terraform.yaml@<sha> # vX.Y.Z
# with:
# terraform-version: "1.16.0"
# app-paths: |
# src/
# package.json
# package-lock.json
#
# app-paths is optional. Empty skips isolation and leaves fmt/init/validate
# unchanged. A trailing slash is a directory prefix. Any other line is an
# exact file. pull_request classifies the merge-base of the base SHA to HEAD.
# merge_group classifies each first-parent commit against its parent, so a
# Terraform-only PR stacked with an app-only PR still passes. The classified
# diff includes deletions. Terraform paths are those under working-directory.
# The checker is a composite action in this repository. `$/` resolves that
# action at this workflow's commit, so callers do not clone this private
# repository with their GITHUB_TOKEN.
on:
workflow_call:
@ -36,10 +22,6 @@ on:
description: "Directory containing Terraform sources"
type: string
default: "terraform"
app-paths:
description: "Newline-separated deployable paths. A trailing slash is a prefix. Any other entry is an exact file. Empty skips isolation."
type: string
default: ""
permissions:
contents: read
@ -59,7 +41,6 @@ jobs:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
fetch-depth: 0
- uses: hashicorp/setup-terraform@dfe3c3f87815947d99a8997f908cb6525fc44e9e # v4.0.1
with:
@ -74,13 +55,3 @@ jobs:
- name: Terraform validate
run: terraform validate
- name: App and Terraform isolation
if: inputs.app-paths != ''
uses: $/.github/actions/app-terraform-isolation
with:
app-paths: ${{ inputs.app-paths }}
terraform-dir: ${{ inputs.working-directory }}
event-name: ${{ github.event_name }}
pr-base-sha: ${{ github.event.pull_request.base.sha }}
merge-group-base-sha: ${{ github.event.merge_group.base_sha }}

View file

@ -2,22 +2,21 @@ name: ci
# Self-CI for this org `.github` repo.
#
# This repo is on the org "CI complete" ruleset, which requires the
# `ci-complete` status check on `main`. Consumer repos satisfy that ruleset
# with a caller workflow that fans out to the reusable workflows housed here
# and ends in a caller-owned `ci-complete` aggregator. This repo only HOUSES
# those reusable workflows (all `workflow_call`-only), so it needs its own
# portions and aggregator or every PR would sit permanently
# "Expected — Waiting for status to be reported" and could not merge.
# The org ruleset "main branch protection" requires the `ci / ci` status check on
# every repo. Consumer repos satisfy it via a short caller workflow that invokes
# the reusable workflows here. This repo only HOUSES those reusable workflows
# (all `workflow_call`-only), so nothing emitted `ci / ci` and every PR sat
# permanently "Expected — Waiting for status to be reported" and could not merge.
#
# The portions are genuinely useful CI for a repo whose whole product is
# GitHub Actions YAML: the isolation-checker unit tests and actionlint over
# every workflow file. They run in parallel; `ci-complete` requires both.
# This workflow produces that check by linting the workflow files with actionlint
# — genuinely useful CI for a repo whose whole product is GitHub Actions YAML.
#
# Naming is load-bearing: the ruleset matches the required status check against
# the JOB's check-run name, NOT "workflow / job". For a normal (non-reusable) job
# the check-run name IS the job name, so the aggregator is named literally
# "ci-complete". Do not put the portion job names in a ruleset.
# the check-run name IS the job name, so the job must be named literally "ci / ci"
# to emit that exact context. (A job named "ci" emits the context "ci" — which the
# PR UI cosmetically *displays* as "ci / ci" but does NOT satisfy the requirement.)
# This mirrors the org's aggregator-job convention.
#
# actionlint is pinned to a tagged release and installed by downloading the
# release tarball and verifying its SHA256 — not `curl | bash` — to keep the
@ -48,21 +47,9 @@ permissions:
contents: read
jobs:
isolation-tests:
name: isolation-tests
ci:
name: ci / ci
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- name: Isolation checker tests
run: python3 scripts/test_check_app_terraform_isolation.py
shell: bash
actionlint:
name: actionlint
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
@ -82,19 +69,3 @@ jobs:
shellcheck --version
./actionlint -color
shell: bash
ci-complete:
name: ci-complete
needs: [isolation-tests, actionlint]
if: always() && !cancelled()
runs-on: ubuntu-latest
timeout-minutes: 5
steps:
- name: Require portions
env:
ISOLATION_TESTS: ${{ needs.isolation-tests.result }}
ACTIONLINT: ${{ needs.actionlint.result }}
run: |
set -euo pipefail
test "${ISOLATION_TESTS}" = success
test "${ACTIONLINT}" = success

View file

@ -33,7 +33,7 @@ CI callers keep a `merge_group` trigger so native GitHub merge queues still run
Two org rulesets. A repo is on exactly one of them:
- **main branch protection** requires `ci / ci` for unconverted remaining-lane repos.
- **CI complete** requires `ci-complete` for converted HCP callers and for this repo. A repo joins it only when a cutover includes the repo and excludes it from the old ruleset in the same window.
- **CI complete** requires `ci-complete` for converted HCP callers. It targets no repos until a cutover includes the repo and excludes it from the old ruleset in the same window.
The formatter GitHub App is not on the main-branch bypass list.
@ -59,13 +59,9 @@ The formatter GitHub App is not on the main-branch bypass list.
**`.github/workflows/cd-hcp-fargate.yaml`** — HCP Fargate image CD. Checkout at `ref` (empty means `github.sha`), OIDC, SSM cluster/service/family/ecr/container/api-url, docker build+push tagged `$sha` and `$environment`, patch `GIT_SHA`, RegisterTaskDefinition + UpdateService + services-stable, poll health SHA. `environment` is a `with:` input. The reusable job owns `environment:`, concurrency, OIDC, and `vars.DEPLOY_ROLE_ARN`.
**`.github/workflows/cd-hcp-lambda.yaml`** — HCP Lambda zip CD for Node packagers. Checkout at `ref`, optional `ship-gate`, Node 24, `scripts/package_lambdas.mjs --git-sha --out-dir --only <key>`, verifies `src/buildInfo.js` carries the SHA, uploads `functions/<key>/<sha>.zip` to the SSM `artifacts-bucket`, `update-function-code` on each `<key>-function-name`, waits for `function-updated-v2`. Terraform owns the functions and ignores code attributes.
**`.github/workflows/cd-hcp-lambda-python.yaml`** — HCP Lambda zip CD for Python packagers. Same contract as `cd-hcp-lambda.yaml` with `python-version` (default `3.12`), `scripts/package_lambdas.sh --git-sha --out-dir --only <key>`, and `build_info.py` as the SHA marker. After each `update-function-code` settles, `CodeSha256` must equal the base64 SHA-256 of the local zip and `LastUpdateStatus` must be `Successful`. That is the live-state check for functions with no health URL.
**`.github/workflows/cd-hcp-spa.yaml`** — HCP SPA CD. Checkout at `ref`, Node 24, `npm ci` + `npm run build`, origin-path guard, hashed s3 sync then `index.html` last then prune, invalidate, verify. Build env comes from the GitHub Environment (`vars.VITE_*`, `secrets.SENTRY_AUTH_TOKEN`).
**`.github/workflows/cd-hcp-static.yaml`** — HCP static-site CD for unfingerprinted builds such as Eleventy. Checkout at `ref`, Node 24, `build-command` (default `npm ci --ignore-scripts && npm run build`), one-day cache on assets, `no-cache` on HTML/XML/text, prune, invalidate, verify the served hash at `verify-path` (default `/`). Reads `/<prefix>/bucket` and `/<prefix>/distribution-id`. `index-required: false` drops the `index.html` checks for trees that have no index page, such as an MTA-STS policy host; a matrix caller can deploy several prefixes from one workflow. Do not use this for a hashed SPA.
**`.github/workflows/cd-hcp-static.yaml`** — HCP static-site CD for unfingerprinted builds such as Eleventy. Checkout at `ref`, Node 24, `npm ci --ignore-scripts` + `npm run build`, one-day cache on assets, `no-cache` on HTML/XML/text, prune, invalidate, verify the served index hash. Reads `/<prefix>/bucket` and `/<prefix>/distribution-id`. Do not use this for a hashed SPA.
**`.github/workflows/cd-sam.yaml`** — Reusable CD workflow for SAM repos. Runs `sam build` + `sam deploy` with OIDC credentials and a CloudFormation execution role. Triggers via `workflow_call` from per-repo `deploy.yaml` on push to `main`.
@ -93,7 +89,7 @@ The formatter GitHub App is not on the main-branch bypass list.
**`.github/workflows/labeler.yaml`** — This repo's own thin caller of `callable-labeler.yaml`, so the labeler runs on `.github`'s own PRs.
**`.github/workflows/ci.yaml`** — Self-CI for this repo: parallel `isolation-tests` (the isolation-checker unit tests) and `actionlint` (checksum-verified install) portions, plus a `ci-complete` aggregator that emits the status context the CI complete ruleset requires. Its shellcheck integration is enabled, so `run:` bodies are shell-linted too; the two deploy steps that rely on intentional word-splitting (`sam deploy … $PARAMS`, `cdk deploy $STACKS`) carry a per-line, commented `# shellcheck disable=SC2086` rather than being quoted or globally exempted.
**`.github/workflows/ci.yaml`** — Self-CI for this repo: actionlint (checksum-verified install) over all workflow files, emitting the required `ci / ci` status context. Its shellcheck integration is enabled, so `run:` bodies are shell-linted too; the two deploy steps that rely on intentional word-splitting (`sam deploy … $PARAMS`, `cdk deploy $STACKS`) carry a per-line, commented `# shellcheck disable=SC2086` rather than being quoted or globally exempted.
### Workflow templates (`workflow-templates/`)

View file

@ -1,239 +0,0 @@
#!/usr/bin/env python3
"""Tests for check_app_terraform_isolation."""
from __future__ import annotations
import os
import re
import subprocess
import sys
import tempfile
import unittest
from pathlib import Path
ROOT = Path(__file__).resolve().parents[1]
ACTION = ROOT / ".github" / "actions" / "app-terraform-isolation"
WORKFLOW = ROOT / ".github" / "workflows" / "ci-terraform.yaml"
sys.path.insert(0, str(ACTION))
from check_app_terraform_isolation import ( # noqa: E402
first_isolation_violation,
isolation_violation,
)
APP_PATHS = "src/\npackage.json\npackage-lock.json\n"
class IsolationTests(unittest.TestCase):
def test_terraform_only(self) -> None:
self.assertIsNone(
isolation_violation(
["terraform/lambda.tf", "terraform/README.md"],
APP_PATHS,
)
)
def test_app_only(self) -> None:
self.assertIsNone(
isolation_violation(
["src/processPaymentCsv.js", "package.json", "package-lock.json"],
APP_PATHS,
)
)
def test_docs_and_workflows_with_terraform(self) -> None:
self.assertIsNone(
isolation_violation(
[
"terraform/lambda.tf",
".github/workflows/deploy.yaml",
"SETUP.md",
"scripts/check_app_terraform_isolation.py",
],
APP_PATHS,
)
)
def test_mixed_app_and_terraform_fails(self) -> None:
violation = isolation_violation(
["terraform/lambda.tf", "src/processPaymentCsv.js", "package.json"],
APP_PATHS,
)
self.assertIsNotNone(violation)
terraform_files, app_files = violation or ([], [])
self.assertEqual(terraform_files, ["terraform/lambda.tf"])
self.assertEqual(app_files, ["package.json", "src/processPaymentCsv.js"])
def test_empty_app_paths_skips(self) -> None:
self.assertIsNone(
isolation_violation(
["terraform/lambda.tf", "src/processPaymentCsv.js"],
"",
)
)
def test_separate_commits_pass_when_classified_alone(self) -> None:
self.assertIsNone(
first_isolation_violation(
[
["terraform/lambda.tf"],
["src/processPaymentCsv.js"],
],
APP_PATHS,
)
)
def test_union_of_separate_commits_fails(self) -> None:
violation = isolation_violation(
["terraform/lambda.tf", "src/processPaymentCsv.js"],
APP_PATHS,
)
self.assertIsNotNone(violation)
def test_non_default_terraform_dir_mixed_fails(self) -> None:
violation = isolation_violation(
["infra/main.tf", "src/app.js"],
"src/\n",
terraform_dir="infra",
)
self.assertIsNotNone(violation)
terraform_files, app_files = violation or ([], [])
self.assertEqual(terraform_files, ["infra/main.tf"])
self.assertEqual(app_files, ["src/app.js"])
def test_default_dir_leaves_other_prefixes_neutral(self) -> None:
self.assertIsNone(
isolation_violation(["infra/main.tf", "src/app.js"], "src/\n")
)
def test_terraform_prefix_does_not_match_a_longer_directory(self) -> None:
self.assertIsNone(
isolation_violation(
["infrastructure/main.tf", "src/app.js"],
"src/\n",
terraform_dir="infra",
)
)
def test_terraform_dir_trailing_slash(self) -> None:
violation = isolation_violation(
["infra/main.tf", "src/app.js"],
"src/\n",
terraform_dir="infra/",
)
self.assertIsNotNone(violation)
def test_blank_terraform_dir_defaults_to_terraform(self) -> None:
violation = isolation_violation(
["terraform/lambda.tf", "src/app.js"],
"src/\n",
terraform_dir=" ",
)
self.assertIsNotNone(violation)
class WorkflowDiffTests(unittest.TestCase):
def test_classified_diffs_include_deletions(self) -> None:
self.assertEqual(_workflow_diff_filters(), ["ACMRD", "ACMRD"])
def test_workflow_passes_working_directory(self) -> None:
self.assertIn(
"terraform-dir: ${{ inputs.working-directory }}",
WORKFLOW.read_text(),
)
def test_checker_is_this_repos_action_at_the_workflow_commit(self) -> None:
text = WORKFLOW.read_text()
self.assertIn("uses: $/.github/actions/app-terraform-isolation", text)
self.assertNotIn("github.workflow_sha", text)
self.assertNotIn("repository: Sea-Haven-Industries/.github", text)
def test_deleted_app_file_is_classified(self) -> None:
diff_filter = _workflow_diff_filters()[0]
with tempfile.TemporaryDirectory() as tmp:
repo = Path(tmp)
base = _commit_base(repo)
(repo / "terraform" / "lambda.tf").write_text("changed\n")
(repo / "src" / "processPaymentCsv.js").unlink()
_git(repo, "add", "-A")
_git(repo, "commit", "-m", "decommission handler")
omitted = _changed_paths(repo, base, "HEAD", "ACMR")
included = _changed_paths(repo, base, "HEAD", diff_filter)
self.assertNotIn("src/processPaymentCsv.js", omitted)
self.assertIn("src/processPaymentCsv.js", included)
self.assertIn("terraform/lambda.tf", included)
self.assertIsNotNone(isolation_violation(included, APP_PATHS))
def test_deleted_terraform_file_is_classified(self) -> None:
diff_filter = _workflow_diff_filters()[0]
with tempfile.TemporaryDirectory() as tmp:
repo = Path(tmp)
base = _commit_base(repo)
(repo / "terraform" / "lambda.tf").unlink()
(repo / "src" / "processPaymentCsv.js").write_text("changed\n")
_git(repo, "add", "-A")
_git(repo, "commit", "-m", "remove lambda")
omitted = _changed_paths(repo, base, "HEAD", "ACMR")
included = _changed_paths(repo, base, "HEAD", diff_filter)
self.assertNotIn("terraform/lambda.tf", omitted)
violation = isolation_violation(included, APP_PATHS)
self.assertIsNotNone(violation)
terraform_files, app_files = violation or ([], [])
self.assertEqual(terraform_files, ["terraform/lambda.tf"])
self.assertEqual(app_files, ["src/processPaymentCsv.js"])
def _workflow_diff_filters() -> list[str]:
action = (ACTION / "action.yml").read_text()
return re.findall(r"--diff-filter=([A-Z]+)", action)
def _git(repo: Path, *args: str) -> str:
env = os.environ.copy()
env["GIT_CONFIG_GLOBAL"] = os.devnull
env["GIT_CONFIG_NOSYSTEM"] = "1"
completed = subprocess.run(
[
"git",
"-c",
"commit.gpgsign=false",
"-c",
"user.name=test",
"-c",
"user.email=test@example.com",
*args,
],
cwd=repo,
check=True,
capture_output=True,
text=True,
env=env,
)
return completed.stdout
def _commit_base(repo: Path) -> str:
_git(repo, "init", "-b", "main")
(repo / "terraform").mkdir()
(repo / "src").mkdir()
(repo / "terraform" / "lambda.tf").write_text("resource\n")
(repo / "src" / "processPaymentCsv.js").write_text("export {}\n")
_git(repo, "add", ".")
_git(repo, "commit", "-m", "base")
return _git(repo, "rev-parse", "HEAD").strip()
def _changed_paths(repo: Path, base: str, head: str, diff_filter: str) -> list[str]:
output = _git(
repo,
"diff",
"--name-only",
f"--diff-filter={diff_filter}",
base,
head,
)
return [line for line in output.splitlines() if line]
if __name__ == "__main__":
unittest.main()

View file

@ -5,7 +5,7 @@ on:
jobs:
deploy:
uses: Sea-Haven-Industries/.github/.github/workflows/cd-cdk.yaml@47185fa602dffddb8297db5f3525d7c9bc05d7cd # v1.0.21
uses: Sea-Haven-Industries/.github/.github/workflows/cd-cdk.yaml@9781774f04b824b1182ff41638687f1c01c04361 # v1.0.11
with:
# Matches local dev (Node 24 / npm 11, lockfileVersion 3). This is also the
# reusable workflow's default — passed explicitly to pin against drift.

View file

@ -12,4 +12,4 @@ jobs:
# Every input is optional. Common overrides: `solution` (defaults to *.sln
# in the working directory), `working-directory`, and `dotnet-version`
# (defaults to 8.0.x). This reusable has no `node-version` input.
uses: Sea-Haven-Industries/.github/.github/workflows/ci-dotnet.yaml@47185fa602dffddb8297db5f3525d7c9bc05d7cd # v1.0.21
uses: Sea-Haven-Industries/.github/.github/workflows/ci-dotnet.yaml@9781774f04b824b1182ff41638687f1c01c04361 # v1.0.11

View file

@ -8,7 +8,7 @@ jobs:
ci:
# Job id MUST stay `ci`: the reusable's aggregator job is also `ci`, so the
# check context resolves to the required `ci / ci`.
uses: Sea-Haven-Industries/.github/.github/workflows/ci-mobile-ios.yaml@47185fa602dffddb8297db5f3525d7c9bc05d7cd # v1.0.21
uses: Sea-Haven-Industries/.github/.github/workflows/ci-mobile-ios.yaml@9781774f04b824b1182ff41638687f1c01c04361 # v1.0.11
with:
# Matches local dev (Node 24 / npm 11, lockfileVersion 3). This is also
# the reusable workflow's default — passed explicitly to pin against drift.

View file

@ -6,7 +6,7 @@ on:
jobs:
ci:
uses: Sea-Haven-Industries/.github/.github/workflows/ci-typescript-cdk.yaml@47185fa602dffddb8297db5f3525d7c9bc05d7cd # v1.0.21
uses: Sea-Haven-Industries/.github/.github/workflows/ci-typescript-cdk.yaml@9781774f04b824b1182ff41638687f1c01c04361 # v1.0.11
with:
# Matches local dev (Node 24 / npm 11, lockfileVersion 3). This is also the
# reusable workflow's default — passed explicitly to pin against drift.

View file

@ -10,4 +10,4 @@ jobs:
# check context resolves to the required `ci / ci`.
#
# Every input is optional. Common override: `source-dirs` (ruff targets).
uses: Sea-Haven-Industries/.github/.github/workflows/ci-python-app.yaml@47185fa602dffddb8297db5f3525d7c9bc05d7cd # v1.0.21
uses: Sea-Haven-Industries/.github/.github/workflows/ci-python-app.yaml@9781774f04b824b1182ff41638687f1c01c04361 # v1.0.11

View file

@ -6,7 +6,7 @@ on:
jobs:
ci:
uses: Sea-Haven-Industries/.github/.github/workflows/ci-python-sam.yaml@47185fa602dffddb8297db5f3525d7c9bc05d7cd # v1.0.21
uses: Sea-Haven-Industries/.github/.github/workflows/ci-python-sam.yaml@9781774f04b824b1182ff41638687f1c01c04361 # v1.0.11
with:
run-tests: true
# ci-python-sam.yaml declares a `node-version` input (default "24") that

View file

@ -8,7 +8,7 @@ jobs:
ci:
# Job id MUST stay `ci`: the reusable's job is also `ci`, so the check
# context resolves to the required `ci / ci`.
uses: Sea-Haven-Industries/.github/.github/workflows/ci-static.yaml@47185fa602dffddb8297db5f3525d7c9bc05d7cd # v1.0.21
uses: Sea-Haven-Industries/.github/.github/workflows/ci-static.yaml@9781774f04b824b1182ff41638687f1c01c04361 # v1.0.11
with:
# Pinned explicitly (handbook): local dev is Node 24 / npm 11, which
# generates lockfileVersion 3. Being explicit avoids lockfile drift.

View file

@ -8,7 +8,7 @@ jobs:
ci:
# Job id MUST stay `ci`: the reusable's job is also `ci`, so the check
# context resolves to the required `ci / ci`.
uses: Sea-Haven-Industries/.github/.github/workflows/ci-typescript-frontend.yaml@47185fa602dffddb8297db5f3525d7c9bc05d7cd # v1.0.21
uses: Sea-Haven-Industries/.github/.github/workflows/ci-typescript-frontend.yaml@9781774f04b824b1182ff41638687f1c01c04361 # v1.0.11
with:
# Pinned explicitly (handbook): local dev is Node 24 / npm 11, which
# generates lockfileVersion 3. Being explicit avoids lockfile drift.

View file

@ -8,4 +8,4 @@ permissions:
jobs:
dependency-review:
uses: Sea-Haven-Industries/.github/.github/workflows/callable-dependency-review.yaml@47185fa602dffddb8297db5f3525d7c9bc05d7cd # v1.0.21
uses: Sea-Haven-Industries/.github/.github/workflows/callable-dependency-review.yaml@9781774f04b824b1182ff41638687f1c01c04361 # v1.0.11

View file

@ -5,7 +5,7 @@ on:
jobs:
deploy:
uses: Sea-Haven-Industries/.github/.github/workflows/cd-dotnet-eb.yaml@47185fa602dffddb8297db5f3525d7c9bc05d7cd # v1.0.21
uses: Sea-Haven-Industries/.github/.github/workflows/cd-dotnet-eb.yaml@9781774f04b824b1182ff41638687f1c01c04361 # v1.0.11
with:
# Required: the project to publish, relative to the repo root.
project: REPLACE-ME-project-csproj

View file

@ -13,4 +13,4 @@ permissions:
jobs:
label:
uses: Sea-Haven-Industries/.github/.github/workflows/callable-labeler.yaml@47185fa602dffddb8297db5f3525d7c9bc05d7cd # v1.0.21
uses: Sea-Haven-Industries/.github/.github/workflows/callable-labeler.yaml@9781774f04b824b1182ff41638687f1c01c04361 # v1.0.11

View file

@ -5,7 +5,7 @@ on:
jobs:
deploy:
uses: Sea-Haven-Industries/.github/.github/workflows/cd-mobile-ios.yaml@47185fa602dffddb8297db5f3525d7c9bc05d7cd # v1.0.21
uses: Sea-Haven-Industries/.github/.github/workflows/cd-mobile-ios.yaml@9781774f04b824b1182ff41638687f1c01c04361 # v1.0.11
with:
# Pinned explicitly (handbook): local dev is Node 24 / npm 11, which
# generates lockfileVersion 3. Being explicit avoids lockfile drift.

View file

@ -13,7 +13,7 @@ permissions:
jobs:
release:
uses: Sea-Haven-Industries/.github/.github/workflows/release.yaml@47185fa602dffddb8297db5f3525d7c9bc05d7cd # v1.0.21
uses: Sea-Haven-Industries/.github/.github/workflows/release.yaml@9781774f04b824b1182ff41638687f1c01c04361 # v1.0.11
with:
version: ${{ inputs.version }}
# Tags in this org are v-prefixed MAJOR.MINOR.PATCH; "v" is the default

View file

@ -5,7 +5,7 @@ on:
jobs:
deploy:
uses: Sea-Haven-Industries/.github/.github/workflows/cd-sam.yaml@47185fa602dffddb8297db5f3525d7c9bc05d7cd # v1.0.21
uses: Sea-Haven-Industries/.github/.github/workflows/cd-sam.yaml@9781774f04b824b1182ff41638687f1c01c04361 # v1.0.11
with:
# Required: the CloudFormation stack name (kebab-case, matches repo name).
# NOTE: this is a literal placeholder on purpose — starter-workflow variables