Something went wrong. Try again.
atproto git client
Something went wrong. Try again.
Shell
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140#!/usr/bin/env bash# Build the site and put it online at https://atgc.codes.## www/scripts/deploy.sh [tofu apply arguments…]## The build is uploaded as an immutable tree under# s3://atgc.codes/releases/<git sha>/, and `tofu apply` then only flips# CloudFront's origin_path to it — so a deploy is one atomic pointer move# rather than a bucket being rewritten under a reader. Roll back with:## tofu -chdir=infra apply -var release_sha=<a sha already uploaded>## State is remote (infra/backend.tf), so the sha CloudFront is serving is# recorded there and not in a file anybody has to keep. `tofu -chdir=infra# output release_sha` is what is live.## Publishing the notes to atproto is the step *after* this one, never part of# it: a document record claims an address, so the address has to answer first.# See www/scripts/publish.sh.set -euo pipefail
repo_root="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")/../.." && pwd)"bucket="atgc.codes"# The atgc.codes account. Written out here for the same reason# infra/backend.tf writes it into the state bucket's name: this runs before# tofu does, so there is nothing yet to read it from.account="549303709100"
if ! identity="$(aws sts get-caller-identity --output text --query '[Account,Arn]' 2>&1)"; then echo "deploy: AWS credential check failed:" >&2 echo " ${identity}" >&2 echo "deploy: log in to the atgc.codes account (AWS_PROFILE=jmm-atgc-codes-admin), then re-run." >&2 exit 1firead -r caller_account caller_arn <<<"${identity}"
# Valid credentials for the wrong account are the failure worth catching. The# SSO sessions in ~/.aws/config share one start URL, so a login authenticates# whoever the browser is already signed in as and hands back a working token# under whatever profile was asked for. Unchecked, that reads as a confusing# permissions error much later — or, with enough rights somewhere else,# uploads this site into somebody else's bucket.if [[ "${caller_account}" != "${account}" ]]; then echo "deploy: wrong AWS account." >&2 echo " credentials are for ${caller_account} (${caller_arn})" >&2 echo " atgc.codes is ${account}" >&2 echo "deploy: log in to it (AWS_PROFILE=jmm-atgc-codes-admin), then re-run." >&2 echo "deploy: if that profile is what you used, check who it logged you in as:" >&2 echo "deploy: aws sts get-caller-identity" >&2 exit 1fiecho "deploy: deploying as ${caller_arn}"
# `npm ci`, not `npm install`: the uploaded tree should be a function of the# commit rather than of whatever is in www/node_modules right now. The build# regenerates src/generated/ from the binary and then runs verify:dist, which# is what actually decides the tree is deployable.npm --prefix "${repo_root}/www" cinpm --prefix "${repo_root}/www" run build
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.# --untracked-files=no keeps the compared set to tracked files.if [[ -n "$(git -C "${repo_root}" status --porcelain --untracked-files=no)" ]]; then release="${release}-dirty" echo "deploy: uncommitted changes; deploying as ${release}"fiprefix="releases/${release}"echo "deploy: release ${release}"
if ! aws s3api head-bucket --bucket "${bucket}" >/dev/null 2>&1; then echo "deploy: s3://${bucket} does not exist yet (first deploy?)." >&2 echo "deploy: run \`tofu -chdir=infra init\` and" >&2 echo "deploy: \`tofu -chdir=infra apply -var release_sha=${release}\` to create the" >&2 echo "deploy: infrastructure, 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" ;; *.xml) echo "application/xml; charset=utf-8" ;; *.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 assets/ carry a content hash in the filename, so they can be # cached forever; everything else falls back to CloudFront defaults. if [[ "${key}" == assets/* ]]; 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}/www/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}/www/dist" -type f -print0 \ | xargs -0 -r -P 10 -I {} bash -c 'upload_from_dist "$@"' _ {}echo "deploy: uploaded release to s3://${bucket}/${prefix}/"
tofu -chdir="${repo_root}/infra" apply -var "release_sha=${release}" "$@"
# 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: 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: created CloudFront invalidation ${invalidation}"