Something went wrong. Try again.
Browser extension: detect and subscribe to standard.site publications on ATProto
Something went wrong. Try again.
6.6 kB · 137 lines
Shell
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138#!/usr/bin/env bash# Build the frontpage, upload it as an immutable release tree under# s3://substandard.blog/releases/<git sha>/, then run `tofu apply`, which# only flips CloudFront's origin_path to the new tree (the sha is handed to# tofu as release_sha in infra/terraform.tfvars.json, patched here). Roll# back by setting release_sha to a previously uploaded sha and re-applying.## Checks AWS credentials up front — the only credentials a deploy needs# (the Route53 zone, ACM cert, S3 bucket, and CloudFront distribution all# live in one AWS account, and tofu state is local in infra/).## Extra arguments are passed through to `tofu apply` (e.g. -auto-approve).set -euo pipefail
repo_root="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")/.." && pwd)"bucket="substandard.blog"
if ! identity="$(aws sts get-caller-identity --output text --query Arn 2>&1)"; then echo "deploy-site: AWS credential check failed:" >&2 echo " ${identity}" >&2 echo "deploy-site: configure credentials for the substandard.blog account, then re-run." >&2 exit 1fiecho "deploy-site: deploying as ${identity}"
# Same lockfile discipline as deploy-ext.sh: install exactly# web/package-lock.json so the uploaded release tree is a function of the# commit rather than of whatever is in web/node_modules right now.npm --prefix "${repo_root}/web" ci# The homepage gallery serves the popup captures out of web/public/, so a# capture regenerated without a re-sync would deploy a screenshot of a popup# the extension no longer has. Re-sync before the build rather than trusting# that someone ran `npm run shots`; node builtins only, so it needs no install# of its own, and verify:dist below fails the deploy if a copy is still stale.# A sync that changes files leaves the tree dirty, which the release naming# below already reports.node "${repo_root}/scripts/sync-shots.mjs"npm --prefix "${repo_root}/web" run build# A post page renders its document link tag from the atUri in its own# frontmatter, and publish.sh writes that back only after the deploy that put# the page online — so between publishing and the next commit the built page# has no tag. Inject re-reads sequoia's state and puts it back before anything# is uploaded: it leaves alone a tag the build already emitted, and touches no# page that is not a published post. The state file is gitignored, so a# checkout that never published finds nothing to do; the frontmatter is still# the committed source of the tag, and this is the second chance at it.(cd "${repo_root}/web" && ./node_modules/.bin/sequoia inject)# Verify what is about to be uploaded, injected tags included.npm --prefix "${repo_root}/web" run verify:dist
release="$(git -C "${repo_root}" rev-parse --short=12 HEAD)"# A dirty tree is not that commit's build: keep it out of the commit's# immutable release tree. The -dirty tree is scratch and gets overwritten.## `status`, not `diff-index`: diff-index trusts the stat info cached in the# index, so a file rewritten with identical content reads as modified — and# the sync above rewrites every capture on each run, which made this suffix# -dirty on a clean tree every time. status refreshes the index first and# compares contents. --untracked-files=no keeps the compared set the same:# tracked files only.if [[ -n "$(git -C "${repo_root}" status --porcelain --untracked-files=no)" ]]; then release="${release}-dirty" echo "deploy-site: uncommitted changes; deploying as ${release}"fiprefix="releases/${release}"echo "deploy-site: release ${release}"
# Record the release for tofu before uploading: on a first deploy the bucket# does not exist yet, and `tofu -chdir=infra apply` can create it from this# file before this script is re-run.node "${repo_root}/scripts/patch-tfvars.mjs" \ "${repo_root}/infra/terraform.tfvars.json" release_sha "${release}"
if ! aws s3api head-bucket --bucket "${bucket}" >/dev/null 2>&1; then echo "deploy-site: s3://${bucket} does not exist yet (first deploy?)." >&2 echo "deploy-site: run \`tofu -chdir=infra apply\` to create the infra" >&2 echo "deploy-site: (infra/terraform.tfvars.json is already written), then re-run this script." >&2 exit 1fi
content_type() { case "$1" in *.html) echo "text/html; charset=utf-8" ;; *.css) echo "text/css; charset=utf-8" ;; *.js) echo "text/javascript; charset=utf-8" ;; *.json) echo "application/json" ;; *.png) echo "image/png" ;; *.svg) echo "image/svg+xml" ;; *.ico) echo "image/x-icon" ;; *.txt) echo "text/plain; charset=utf-8" ;; *.woff) echo "font/woff" ;; *.woff2) echo "font/woff2" ;; *.webp) echo "image/webp" ;; *) echo "application/octet-stream" ;; esac}
upload() { local file="$1" key="$2" local args=(--content-type "$(content_type "${key}")") # Astro's _astro/ assets carry a content hash in the filename, so they can # be cached forever; everything else falls back to CloudFront defaults. if [[ "${key}" == _astro/* ]]; then args+=(--cache-control "public, max-age=31536000, immutable") fi aws s3 cp --no-progress "${file}" "s3://${bucket}/${prefix}/${key}" "${args[@]}"}
# Derive the key from the path so xargs can hand each worker a single# argument.upload_from_dist() { upload "$1" "${1#"${repo_root}/web/dist/"}"}
# One `aws` process per file costs ~0.5s of Python startup and a fresh TLS# handshake, which for a tree this small is nearly all of the upload time.# Run ten at a time. xargs exits non-zero if any single upload failed, and# `set -e` turns that into a failed deploy, so a partial release tree can# never reach the `tofu apply` below that points CloudFront at it.export -f upload upload_from_dist content_typeexport bucket prefix repo_root
find "${repo_root}/web/dist" -type f -print0 \ | xargs -0 -r -P 10 -I {} bash -c 'upload_from_dist "$@"' _ {}upload "${repo_root}/oauth/client-metadata.json" "client-metadata.json"echo "deploy-site: uploaded release to s3://${bucket}/${prefix}/"
tofu -chdir="${repo_root}/infra" apply "$@"
# The apply only updates the distribution config; edge caches still hold the# previous release. Wait for the config to reach every edge, then invalidate.dist_id="$(tofu -chdir="${repo_root}/infra" output -raw distribution_id)"echo "deploy-site: waiting for CloudFront distribution ${dist_id} to deploy"aws cloudfront wait distribution-deployed --id "${dist_id}"invalidation="$(aws cloudfront create-invalidation --distribution-id "${dist_id}" \ --paths "/*" --output text --query Invalidation.Id)"echo "deploy-site: created CloudFront invalidation ${invalidation}"