Follow the house shell conventions

Constants UPPER_SNAKE, mutable locals PascalCase, braced expansions
throughout, per Development/Coding conventions in the vault.
This commit is contained in:
fisher
2026-08-23 08:57:15 +00:00
parent 952de1ea8e
commit fc40facfc5
6 changed files with 131 additions and 108 deletions
+69 -69
View File
@@ -64,27 +64,27 @@ while [[ $# -gt 0 ]]; do
esac
done
[[ -n "$REPO" ]] || die "--repo is required"
[[ -n "$REF" ]] || die "--ref is required"
[[ -n "$SHA" ]] || die "--sha is required"
[[ -n "${REPO}" ]] || die "--repo is required"
[[ -n "${REF}" ]] || die "--ref is required"
[[ -n "${SHA}" ]] || die "--sha is required"
if [[ "$EVENT" != "push" ]]; then
if [[ "${EVENT}" != "push" ]]; then
log "ignoring event '${EVENT}' for ${REPO} (only 'push' deploys)"
exit 0
fi
if [[ "$REF" != refs/heads/* ]]; then
if [[ "${REF}" != refs/heads/* ]]; then
log "ignoring non-branch ref '${REF}' for ${REPO} (tags and deletes never deploy)"
exit 0
fi
BRANCH="${REF#refs/heads/}"
if [[ ! "$SHA" =~ ^[0-9a-f]{40}$ ]]; then
if [[ ! "${SHA}" =~ ^[0-9a-f]{40}$ ]]; then
die "--sha must be a full 40-character hex commit, got: ${SHA}"
fi
# The all-zero SHA is how git spells "this ref was deleted".
if [[ "$SHA" == "0000000000000000000000000000000000000000" ]]; then
if [[ "${SHA}" == "0000000000000000000000000000000000000000" ]]; then
log "ignoring branch deletion of ${REPO}@${BRANCH}"
exit 0
fi
@@ -92,43 +92,43 @@ fi
# ------------------------------------------------------ target resolution ---
set +e
resolved="$("$CD_TARGET" resolve --repo "$REPO" --branch "$BRANCH")"
resolve_rc=$?
Resolved="$("${CD_TARGET}" resolve --repo "${REPO}" --branch "${BRANCH}")"
ResolveRc=$?
set -e
if [[ $resolve_rc -eq $NO_MATCH ]]; then
if [[ ${ResolveRc} -eq ${NO_MATCH} ]]; then
log "no target on $(hostname) for ${REPO}@${BRANCH} -- nothing to do here"
exit 0
elif [[ $resolve_rc -ne 0 ]]; then
die "target lookup failed for ${REPO}@${BRANCH} (exit ${resolve_rc})"
elif [[ ${ResolveRc} -ne 0 ]]; then
die "target lookup failed for ${REPO}@${BRANCH} (exit ${ResolveRc})"
fi
eval "$resolved"
eval "${Resolved}"
mkdir -p "$STATE_DIR"
mkdir -p "${STATE_DIR}"
readonly LOG_FILE="${STATE_DIR}/${CD_NAME}.log"
readonly STATUS_FILE="${STATE_DIR}/${CD_NAME}.status"
exec > >(stdbuf -oL tee -a "$LOG_FILE") 2>&1
exec > >(stdbuf -oL tee -a "${LOG_FILE}") 2>&1
log "=============================================================="
log "target ${CD_NAME} (${CD_ENV} on ${CD_RESOLVED_HOST})"
log "repo ${REPO}@${BRANCH}"
log "commit ${SHA}"
log "stack ${CD_STACK_DIR}/${CD_COMPOSE_FILE} [project ${CD_COMPOSE_PROJECT}]"
[[ $DRY_RUN -eq 1 ]] && log "mode DRY RUN -- nothing will be changed"
[[ ${DRY_RUN} -eq 1 ]] && log "mode DRY RUN -- nothing will be changed"
[[ -d "$CD_STACK_DIR" ]] || die "stack directory missing: ${CD_STACK_DIR}"
[[ -d "${CD_STACK_DIR}" ]] || die "stack directory missing: ${CD_STACK_DIR}"
[[ -f "${CD_STACK_DIR}/${CD_COMPOSE_FILE}" ]] \
|| die "compose file missing: ${CD_STACK_DIR}/${CD_COMPOSE_FILE}"
# ----------------------------------------------------------------- notify ---
notify() {
local status="$1" message="$2" icon topic
case "$status" in
ok) icon="✅" ;;
fail) icon="❌" ;;
*) icon="️" ;;
local Status="$1" Message="$2" Icon Topic
case "${Status}" in
ok) Icon="✅" ;;
fail) Icon="❌" ;;
*) Icon="️" ;;
esac
# Every terminal outcome notifies, so this is also where the machine-readable
@@ -136,14 +136,14 @@ notify() {
# grepping the log's prose, which quietly reported "no completed deploy"
# for a healthy target whenever a log line was reworded.
printf '%s\t%s\t%s\t%s\n' \
"$(date -u '+%Y-%m-%dT%H:%M:%SZ')" "$status" "${SHA:0:12}" "$message" \
> "$STATUS_FILE"
"$(date -u '+%Y-%m-%dT%H:%M:%SZ')" "${Status}" "${SHA:0:12}" "${Message}" \
> "${STATUS_FILE}"
topic="${REPO//\//-}"
Topic="${REPO//\//-}"
curl -fsS --max-time 10 \
-H "Title: ${CD_NAME} deploy" \
-d "${icon} ${CD_NAME} (${CD_ENV}): ${message}" \
"${CD_NTFY_BASE_URL}/${topic}" >/dev/null 2>&1 \
-d "${Icon} ${CD_NAME} (${CD_ENV}): ${Message}" \
"${CD_NTFY_BASE_URL}/${Topic}" >/dev/null 2>&1 \
|| warn "ntfy notification failed (the deploy itself is unaffected)"
}
@@ -151,9 +151,9 @@ notify() {
# Serialise per target, not per host: a test deploy must not block a prod one.
readonly LOCK_FILE="${STATE_DIR}/${CD_NAME}.lock"
exec 9>"$LOCK_FILE"
exec 9>"${LOCK_FILE}"
log "waiting for deploy lock (${CD_LOCK_WAIT_SECONDS}s max)"
if ! flock -w "$CD_LOCK_WAIT_SECONDS" 9; then
if ! flock -w "${CD_LOCK_WAIT_SECONDS}" 9; then
notify fail "timed out waiting for the deploy lock after ${CD_LOCK_WAIT_SECONDS}s"
die "timed out waiting for deploy lock after ${CD_LOCK_WAIT_SECONDS}s"
fi
@@ -161,13 +161,13 @@ log "lock acquired"
# -------------------------------------------------------------- image tag ---
tag="$CD_IMAGE_TAG_TEMPLATE"
tag="${tag//\{branch\}/$BRANCH}"
tag="${tag//\{env\}/$CD_ENV}"
tag="${tag//\{sha\}/$SHA}"
tag="${tag//\{short7\}/${SHA:0:7}}"
tag="${tag//\{short12\}/${SHA:0:12}}"
readonly CANDIDATE="${CD_IMAGE_REPO}:${tag}"
Tag="${CD_IMAGE_TAG_TEMPLATE}"
Tag="${Tag//\{branch\}/$BRANCH}"
Tag="${Tag//\{env\}/$CD_ENV}"
Tag="${Tag//\{sha\}/$SHA}"
Tag="${Tag//\{short7\}/${SHA:0:7}}"
Tag="${Tag//\{short12\}/${SHA:0:12}}"
readonly CANDIDATE="${CD_IMAGE_REPO}:${Tag}"
log "candidate ${CANDIDATE}"
@@ -177,23 +177,23 @@ log "candidate ${CANDIDATE}"
# has finished building. Poll rather than fail: the running container keeps
# serving throughout.
wait_for_image() {
local elapsed=0
local Elapsed=0
while true; do
if docker pull "$CANDIDATE" >/dev/null 2>&1; then
log "image available after ${elapsed}s"
if docker pull "${CANDIDATE}" >/dev/null 2>&1; then
log "image available after ${Elapsed}s"
return 0
fi
if (( elapsed >= CD_IMAGE_WAIT_SECONDS )); then
if (( Elapsed >= CD_IMAGE_WAIT_SECONDS )); then
return 1
fi
log "image not published yet, retrying in ${CD_IMAGE_POLL_INTERVAL}s (${elapsed}s elapsed)"
sleep "$CD_IMAGE_POLL_INTERVAL"
elapsed=$(( elapsed + CD_IMAGE_POLL_INTERVAL ))
log "image not published yet, retrying in ${CD_IMAGE_POLL_INTERVAL}s (${Elapsed}s elapsed)"
sleep "${CD_IMAGE_POLL_INTERVAL}"
Elapsed=$(( Elapsed + CD_IMAGE_POLL_INTERVAL ))
done
}
log "waiting for CI to publish the image (up to ${CD_IMAGE_WAIT_SECONDS}s)"
if [[ $DRY_RUN -eq 1 ]]; then
if [[ ${DRY_RUN} -eq 1 ]]; then
log "dry run: skipping image wait"
elif ! wait_for_image; then
notify fail "image ${CANDIDATE} never appeared (waited ${CD_IMAGE_WAIT_SECONDS}s) -- did CI fail?"
@@ -207,27 +207,27 @@ image_digest_ref() {
| grep "^${CD_IMAGE_REPO}@" | head -n1
}
if [[ $DRY_RUN -eq 1 ]]; then
DEPLOY_IMAGE="$CANDIDATE"
if [[ ${DRY_RUN} -eq 1 ]]; then
DEPLOY_IMAGE="${CANDIDATE}"
log "dry run: would deploy ${DEPLOY_IMAGE}"
else
revision="$(docker image inspect \
Revision="$(docker image inspect \
--format '{{index .Config.Labels "org.opencontainers.image.revision"}}' \
"$CANDIDATE" 2>/dev/null || true)"
"${CANDIDATE}" 2>/dev/null || true)"
if [[ -z "$revision" || "$revision" == "<no value>" ]]; then
if [[ -z "${Revision}" || "${Revision}" == "<no value>" ]]; then
warn "image carries no org.opencontainers.image.revision label; cannot prove provenance"
elif [[ "$revision" != "$SHA" ]]; then
notify fail "image ${CANDIDATE} is built from ${revision:0:12}, not ${SHA:0:12} -- refusing to deploy"
die "provenance mismatch: ${CANDIDATE} declares revision ${revision}, expected ${SHA}"
elif [[ "${Revision}" != "${SHA}" ]]; then
notify fail "image ${CANDIDATE} is built from ${Revision:0:12}, not ${SHA:0:12} -- refusing to deploy"
die "provenance mismatch: ${CANDIDATE} declares revision ${Revision}, expected ${SHA}"
else
log "provenance revision label matches ${SHA:0:12}"
fi
DEPLOY_IMAGE="$(image_digest_ref "$CANDIDATE")"
if [[ -z "$DEPLOY_IMAGE" ]]; then
DEPLOY_IMAGE="$(image_digest_ref "${CANDIDATE}")"
if [[ -z "${DEPLOY_IMAGE}" ]]; then
warn "could not resolve a digest for ${CANDIDATE}; deploying by tag instead"
DEPLOY_IMAGE="$CANDIDATE"
DEPLOY_IMAGE="${CANDIDATE}"
else
log "pinned ${DEPLOY_IMAGE}"
fi
@@ -297,10 +297,10 @@ write_rollback_record() {
}
check_health() {
local attempt=1 body
while (( attempt <= CD_HEALTH_RETRIES )); do
body="$(curl -fsS --max-time 5 "$CD_HEALTH_URL" 2>/dev/null || true)"
if [[ -n "$body" ]]; then
local Attempt=1 Body
while (( Attempt <= CD_HEALTH_RETRIES )); do
Body="$(curl -fsS --max-time 5 "${CD_HEALTH_URL}" 2>/dev/null || true)"
if [[ -n "${Body}" ]]; then
if python3 -c '
import json, sys
key, want = sys.argv[1], sys.argv[2]
@@ -309,20 +309,20 @@ try:
except Exception:
sys.exit(1)
sys.exit(0 if str(data.get(key, "")).lower() == want.lower() else 1)
' "$CD_HEALTH_EXPECT_KEY" "$CD_HEALTH_EXPECT_VALUE" <<<"$body"; then
log "health ok after ${attempt} attempt(s): ${body}"
' "${CD_HEALTH_EXPECT_KEY}" "${CD_HEALTH_EXPECT_VALUE}" <<<"${Body}"; then
log "health ok after ${Attempt} attempt(s): ${Body}"
return 0
fi
fi
log "health not ready (attempt ${attempt}/${CD_HEALTH_RETRIES}), retrying in ${CD_HEALTH_INTERVAL}s"
sleep "$CD_HEALTH_INTERVAL"
(( attempt++ ))
log "health not ready (attempt ${Attempt}/${CD_HEALTH_RETRIES}), retrying in ${CD_HEALTH_INTERVAL}s"
sleep "${CD_HEALTH_INTERVAL}"
(( Attempt++ ))
done
warn "health check never passed: last response was '${body:-<no response>}'"
warn "health check never passed: last response was '${Body:-<no response>}'"
return 1
}
if [[ $DRY_RUN -eq 1 ]]; then
if [[ ${DRY_RUN} -eq 1 ]]; then
log "dry run: would recreate service ${CD_COMPOSE_SERVICE} with ${CD_IMAGE_ENV_VAR}=${DEPLOY_IMAGE}"
log "dry run: would health-check ${CD_HEALTH_URL}"
log "dry run complete -- no changes made"
@@ -352,18 +352,18 @@ fi
# ---------------------------------------------------------------- rollback ---
if [[ "$CD_ROLLBACK_ON_FAILURE" != "true" ]]; then
if [[ "${CD_ROLLBACK_ON_FAILURE}" != "true" ]]; then
notify fail "health check failed for ${SHA:0:12}; rollback disabled, stack left on the new image"
die "health check failed and rollback is disabled for this target"
fi
if [[ -z "$PREVIOUS_IMAGE" ]]; then
if [[ -z "${PREVIOUS_IMAGE}" ]]; then
notify fail "health check failed for ${SHA:0:12} and there is no previous image to roll back to"
die "health check failed; no previous image recorded, leaving the stack as it is"
fi
warn "health check failed -- rolling back to ${PREVIOUS_IMAGE}"
if compose_up "$PREVIOUS_IMAGE" && check_health; then
if compose_up "${PREVIOUS_IMAGE}" && check_health; then
notify fail "deploy of ${SHA:0:12} failed health check; rolled back to the previous image successfully"
die "deploy failed health check; rolled back to ${PREVIOUS_IMAGE}"
fi