mirror of
https://github.com/Sea-Haven-Industries/.github.git
synced 2026-10-05 21:12:02 +00:00
feat(cd): add build-command, index-required, and verify-path to cd-hcp-static (#160)
Optional inputs so a matrix caller can publish small static trees that have no index page. Defaults keep the current seahaven-site behavior: the build runs npm ci --ignore-scripts && npm run build, index.html is required, and the served hash of / is polled after invalidation. - build-command runs under bash -euo pipefail - index-required: false skips the local and bucket index.html checks - verify-path picks the local file and served URL used for the hash poll; a trailing slash means index.html, and .. is rejected - header documents a matrix caller keyed on ssm-prefix
This commit is contained in:
parent
47185fa602
commit
324951ca93
2 changed files with 88 additions and 13 deletions
99
.github/workflows/cd-hcp-static.yaml
vendored
99
.github/workflows/cd-hcp-static.yaml
vendored
|
|
@ -22,6 +22,34 @@ 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:
|
||||
|
|
@ -60,6 +88,21 @@ 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
|
||||
|
|
@ -183,15 +226,39 @@ 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
|
||||
npm ci --ignore-scripts
|
||||
npm run build
|
||||
bash -euo pipefail -c "${BUILD_COMMAND}"
|
||||
python3 - <<'PY'
|
||||
import os, sys
|
||||
output_dir = os.environ["OUTPUT_DIR"]
|
||||
|
|
@ -214,14 +281,18 @@ 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 not os.path.isfile(index):
|
||||
if os.environ["INDEX_REQUIRED"] == "true" and 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
|
||||
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}"
|
||||
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}"
|
||||
|
||||
- name: Configure AWS credentials using OIDC
|
||||
uses: aws-actions/configure-aws-credentials@cbe3b392738ccf3f987d68400dafcf4b0624a56c # v6.2.4
|
||||
|
|
@ -257,6 +328,7 @@ 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 \
|
||||
|
|
@ -266,7 +338,10 @@ 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 index.html
|
||||
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:
|
||||
|
|
@ -286,7 +361,7 @@ jobs:
|
|||
env:
|
||||
DISTRIBUTION_ID: ${{ steps.deploy.outputs.distribution_id }}
|
||||
SITE_URL: ${{ steps.deploy.outputs.site_url }}
|
||||
EXPECTED_INDEX_SHA256: ${{ env.INDEX_SHA256 }}
|
||||
EXPECTED_SHA256: ${{ env.VERIFY_SHA256 }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
SITE_URL="${SITE_URL%/}"
|
||||
|
|
@ -297,16 +372,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}/" | sha256_of)" && [ -n "${last_hash}" ]; then
|
||||
if last_hash="$(curl -fsS --max-time 30 "${SITE_URL}${VERIFY_URL_PATH}" | sha256_of)" && [ -n "${last_hash}" ]; then
|
||||
:
|
||||
else
|
||||
last_hash="unreachable"
|
||||
fi
|
||||
echo "poll ${attempt}/40: status=${last_status} served_sha256=${last_hash}"
|
||||
if [ "${last_status}" = "Deployed" ] && [ "${last_hash}" = "${EXPECTED_INDEX_SHA256}" ]; then
|
||||
echo "poll ${attempt}/40: status=${last_status} path=${VERIFY_URL_PATH} served_sha256=${last_hash}"
|
||||
if [ "${last_status}" = "Deployed" ] && [ "${last_hash}" = "${EXPECTED_SHA256}" ]; then
|
||||
exit 0
|
||||
fi
|
||||
sleep 15
|
||||
done
|
||||
echo "release did not converge: status=${last_status} served_sha256=${last_hash} expected=${EXPECTED_INDEX_SHA256}" >&2
|
||||
echo "release did not converge: status=${last_status} path=${VERIFY_URL_PATH} served_sha256=${last_hash} expected=${EXPECTED_SHA256}" >&2
|
||||
exit 1
|
||||
|
|
|
|||
|
|
@ -61,7 +61,7 @@ The formatter GitHub App is not on the main-branch bypass list.
|
|||
|
||||
**`.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, `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-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-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`.
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue