Something went wrong. Try again.
Infrastructure-as-code for running lance.blue
Something went wrong. Try again.
Shell
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277#!/usr/bin/env bash# Everything that decides what a deploy post says. Sourced by# scripts/deploy.sh, which owns the network and the account; nothing in here# talks to a PDS, so scripts/lib/post.test.sh can check the awkward parts -# the commit ranges and the chunking - without one.## A post is 300 graphemes and its record 3000 bytes, which is smaller than one# repo's commits routinely are. So the changelog rides in the first post when# it fits and becomes a thread when it does not.
# What build_post produces, read by whoever sourced this: the first post, and# the thread under it.# shellcheck disable=SC2034 # scripts/deploy.sh reads bothROOT_TEXT=""CHUNKS=()
# A post's limits. Graphemes are counted as characters: commit subjects here# are ASCII, where the two agree, and a locale that counts bytes instead only# makes this stricter.POST_GRAPHEMES=300POST_BYTES=3000
# How many posts a thread may run to before the tail is summarised. A range# of hundreds of commits is a mistake somewhere - a rebuilt branch, a first# deploy after a long gap - and turning it into fifty posts helps nobody.POST_MAX="${DEPLOY_MAX_POSTS:-25}"
fits() { [ "$(printf '%s' "$1" | wc -m)" -le "$POST_GRAPHEMES" ] && [ "$(printf '%s' "$1" | wc -c)" -le "$POST_BYTES" ]}
# Where the sibling checkouts are. --git-common-dir rather than --show-toplevel# so this is the real repository from inside a worktree too, and LANCE_REPOS# for a deploy host that keeps them somewhere else.repos_root() { local common if [ -n "${LANCE_REPOS:-}" ]; then printf '%s' "$LANCE_REPOS" return fi common="$(git rev-parse --path-format=absolute --git-common-dir 2>/dev/null || true)" [ -n "$common" ] || return 0 printf '%s' "$(dirname "$(dirname "$common")")"}
# Which checkout built a release. api and site are both headquarters: one# apply moving both is one range, and it is printed once.repo_for() { case "$1" in api | site) printf 'headquarters' ;; arena) printf 'arena' ;; helm) printf 'helm' ;; *) return 1 ;; esac}
# Every release id ends in the commit it was built from - "main-a7b1f59e8e3b",# "mm0.51.0-sur26.4.7-main-b812ae202e32". Anything else is not a commit and# gets no changelog rather than a wrong one.release_commit() { local tail="${1##*-}" case "$tail" in *[!0-9a-f]* | "") return 1 ;; esac [ "${#tail}" -ge 7 ] || return 1 printf '%s' "$tail"}
# One repo's range, as "<short sha> <subject>" lines. Silent when the checkout# is missing or has not fetched the commits: a deploy is not the moment to# find out somebody's clone is behind, and the post is better without the# changelog than held up for it.changelog_for() { local dir="$1" old="$2" new="$3" [ -d "$dir" ] || return 0 [ "$old" != "$new" ] || return 0 git -C "$dir" cat-file -e "$old^{commit}" 2>/dev/null || return 0 git -C "$dir" cat-file -e "$new^{commit}" 2>/dev/null || return 0 git -C "$dir" log --no-merges --format='%h %s' "$old..$new" 2>/dev/null || true}
# Pack lines into posts. Lines are never split across posts, so a post is# whole commits; a single line longer than a post is the one exception and is# cut. $chunk_cont, set by the caller, is repeated at the top of each post a# group spills into, so a reader landing mid-thread knows what they are# reading.chunk_reset() { CHUNKS=() chunk_current="" chunk_cont=""}
chunk_flush() { [ -n "$chunk_current" ] || return 0 CHUNKS+=("$chunk_current") chunk_current=""}
chunk_add() { local line="$1" candidate [ "$(printf '%s' "$line" | wc -m)" -le "$POST_GRAPHEMES" ] || line="${line:0:297}..."
if [ -z "$chunk_current" ]; then candidate="$line"; else candidate="$chunk_current"$'\n'"$line"; fi if fits "$candidate"; then chunk_current="$candidate" return 0 fi
chunk_flush if [ -n "$chunk_cont" ]; then candidate="$chunk_cont"$'\n'"$line" if fits "$candidate"; then chunk_current="$candidate" return 0 fi fi chunk_current="$line"}
# The release ids that moved, as tab-separated "key<TAB>old<TAB>new". A key# the previous state did not have has an empty old.release_changes() { jq -rn --argjson before "$1" --argjson after "$2" ' $after | to_entries[] | ($before[.key] // "") as $old | select($old != .value) | [.key, $old, .value] | @tsv'}
# The human line for each of those: "site: <old> -> <new>", or just the value# when there was nothing to move from.release_summary() { local key old new while IFS=$'\t' read -r key old new; do [ -n "$key" ] || continue if [ -z "$old" ]; then printf '%s: %s\n' "$key" "$new" else printf '%s: %s -> %s\n' "$key" "$old" "$new" fi done <<<"$1"}
# The changelog for a set of release changes, as lines: a header per repo and# then its commits. Ranges are deduplicated, so api and site moving together# print headquarters' commits once under both names.build_changelog() { local root="$1" changes="$2" local key old new repo old_sha new_sha range local -a order=() labels=() olds=() news=() repos=() local i found
while IFS=$'\t' read -r key old new; do [ -n "$key" ] || continue repo="$(repo_for "$key")" || continue old_sha="$(release_commit "$old")" || continue new_sha="$(release_commit "$new")" || continue range="$repo $old_sha $new_sha"
found="" for i in "${!order[@]}"; do if [ "${order[$i]}" = "$range" ]; then labels[i]="${labels[$i]}, $key" found=1 break fi done [ -n "$found" ] && continue
order+=("$range") labels+=("$key") repos+=("$repo") olds+=("$old_sha") news+=("$new_sha") done <<<"$changes"
local log count for i in "${!order[@]}"; do log="$(changelog_for "$root/${repos[$i]}" "${olds[$i]}" "${news[$i]}")" [ -n "$log" ] || continue count="$(printf '%s\n' "$log" | wc -l)" printf '%s: %s commit%s in %s\n' \ "${labels[$i]}" "$count" "$([ "$count" = 1 ] || printf s)" "${repos[$i]}" printf '%s\n' "$log" done}
# The changelog as posts. CHUNKS comes back holding one post per element, and# a header line starts a new group so a repo never begins at the bottom of a# post it has one line left in.build_thread() { local changelog="$1" line label chunk_reset while IFS= read -r line; do [ -n "$line" ] || continue case "$line" in *' commit'*' in '*) label="${line%%:*}" chunk_flush chunk_cont="$label (cont.)" ;; esac chunk_add "$line" done <<<"$changelog" chunk_flush
# A runaway range says the truth about itself in one line rather than in # thirty posts. if [ "${#CHUNKS[@]}" -gt "$POST_MAX" ]; then local dropped=$((${#CHUNKS[@]} - POST_MAX + 1)) CHUNKS=("${CHUNKS[@]:0:$((POST_MAX - 1))}") CHUNKS+=("... and $dropped more posts of commits, not shown.") fi}
# The deploy's own address. A distinct URL per deploy is a distinct link card# and a distinct row in whatever counts them; readers never see it, because a# card shows the domain and not the path.stamped_link() { local url="$1" stamp="$2" case "$url" in *\?*) printf '%s&d=%s' "$url" "$stamp" ;; *) printf '%s?d=%s' "$url" "$stamp" ;; esac}
# One og: value out of a page. The tags are written by headquarters'# web/src/layouts/Base.astro, property first, and are the same tags every# other card fetcher reads - so a page that unfurls anywhere unfurls here.og_meta() { printf '%s' "$1" | grep -o "property=\"$2\" content=\"[^\"]*\"" | head -1 | sed -e 's/.*content="//' -e 's/"$//' \ -e 's/'/'"'"'/g' -e 's/"/"/g' -e 's/</</g' -e 's/>/>/g' \ -e 's/&/\&/g'}
# What the deploy says, in full. ROOT_TEXT is the first post - the release ids# that moved, and the changelog too when it fits - and CHUNKS is the thread# under it, empty when there is nothing left over.## $link is empty when a link card carries the address, which is the ordinary# case; it is the URL when the card could not be built, so that a post which# announces a deploy still links to it.build_post() { local summary="$1" changelog="$2" link="$3" local tail="" head names candidate
[ -z "$link" ] || tail=$'\n\n'"$link" head="Deployed lance.blue"$'\n\n'"$summary"
# Release ids are long and four of them can move at once. When they do not # fit, the names of what moved do. if ! fits "$head$tail"; then names="$(printf '%s\n' "$summary" | cut -d: -f1 | paste -sd, - | sed 's/,/, /g')" head="Deployed lance.blue: $names" fi
CHUNKS=()
if [ -n "$changelog" ]; then candidate="$head"$'\n\n'"$changelog$tail" if fits "$candidate"; then ROOT_TEXT="$candidate" return 0 fi fi
ROOT_TEXT="$head$tail" [ -n "$changelog" ] || return 0 build_thread "$changelog"}