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 esac
done done
[[ -n "$REPO" ]] || die "--repo is required" [[ -n "${REPO}" ]] || die "--repo is required"
[[ -n "$REF" ]] || die "--ref is required" [[ -n "${REF}" ]] || die "--ref is required"
[[ -n "$SHA" ]] || die "--sha 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)" log "ignoring event '${EVENT}' for ${REPO} (only 'push' deploys)"
exit 0 exit 0
fi fi
if [[ "$REF" != refs/heads/* ]]; then if [[ "${REF}" != refs/heads/* ]]; then
log "ignoring non-branch ref '${REF}' for ${REPO} (tags and deletes never deploy)" log "ignoring non-branch ref '${REF}' for ${REPO} (tags and deletes never deploy)"
exit 0 exit 0
fi fi
BRANCH="${REF#refs/heads/}" 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}" die "--sha must be a full 40-character hex commit, got: ${SHA}"
fi fi
# The all-zero SHA is how git spells "this ref was deleted". # 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}" log "ignoring branch deletion of ${REPO}@${BRANCH}"
exit 0 exit 0
fi fi
@@ -92,43 +92,43 @@ fi
# ------------------------------------------------------ target resolution --- # ------------------------------------------------------ target resolution ---
set +e set +e
resolved="$("$CD_TARGET" resolve --repo "$REPO" --branch "$BRANCH")" Resolved="$("${CD_TARGET}" resolve --repo "${REPO}" --branch "${BRANCH}")"
resolve_rc=$? ResolveRc=$?
set -e 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" log "no target on $(hostname) for ${REPO}@${BRANCH} -- nothing to do here"
exit 0 exit 0
elif [[ $resolve_rc -ne 0 ]]; then elif [[ ${ResolveRc} -ne 0 ]]; then
die "target lookup failed for ${REPO}@${BRANCH} (exit ${resolve_rc})" die "target lookup failed for ${REPO}@${BRANCH} (exit ${ResolveRc})"
fi fi
eval "$resolved" eval "${Resolved}"
mkdir -p "$STATE_DIR" mkdir -p "${STATE_DIR}"
readonly LOG_FILE="${STATE_DIR}/${CD_NAME}.log" readonly LOG_FILE="${STATE_DIR}/${CD_NAME}.log"
readonly STATUS_FILE="${STATE_DIR}/${CD_NAME}.status" 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 "=============================================================="
log "target ${CD_NAME} (${CD_ENV} on ${CD_RESOLVED_HOST})" log "target ${CD_NAME} (${CD_ENV} on ${CD_RESOLVED_HOST})"
log "repo ${REPO}@${BRANCH}" log "repo ${REPO}@${BRANCH}"
log "commit ${SHA}" log "commit ${SHA}"
log "stack ${CD_STACK_DIR}/${CD_COMPOSE_FILE} [project ${CD_COMPOSE_PROJECT}]" 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}" ]] \ [[ -f "${CD_STACK_DIR}/${CD_COMPOSE_FILE}" ]] \
|| die "compose file missing: ${CD_STACK_DIR}/${CD_COMPOSE_FILE}" || die "compose file missing: ${CD_STACK_DIR}/${CD_COMPOSE_FILE}"
# ----------------------------------------------------------------- notify --- # ----------------------------------------------------------------- notify ---
notify() { notify() {
local status="$1" message="$2" icon topic local Status="$1" Message="$2" Icon Topic
case "$status" in case "${Status}" in
ok) icon="✅" ;; ok) Icon="✅" ;;
fail) icon="❌" ;; fail) Icon="❌" ;;
*) icon="️" ;; *) Icon="️" ;;
esac esac
# Every terminal outcome notifies, so this is also where the machine-readable # 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" # grepping the log's prose, which quietly reported "no completed deploy"
# for a healthy target whenever a log line was reworded. # for a healthy target whenever a log line was reworded.
printf '%s\t%s\t%s\t%s\n' \ printf '%s\t%s\t%s\t%s\n' \
"$(date -u '+%Y-%m-%dT%H:%M:%SZ')" "$status" "${SHA:0:12}" "$message" \ "$(date -u '+%Y-%m-%dT%H:%M:%SZ')" "${Status}" "${SHA:0:12}" "${Message}" \
> "$STATUS_FILE" > "${STATUS_FILE}"
topic="${REPO//\//-}" Topic="${REPO//\//-}"
curl -fsS --max-time 10 \ curl -fsS --max-time 10 \
-H "Title: ${CD_NAME} deploy" \ -H "Title: ${CD_NAME} deploy" \
-d "${icon} ${CD_NAME} (${CD_ENV}): ${message}" \ -d "${Icon} ${CD_NAME} (${CD_ENV}): ${Message}" \
"${CD_NTFY_BASE_URL}/${topic}" >/dev/null 2>&1 \ "${CD_NTFY_BASE_URL}/${Topic}" >/dev/null 2>&1 \
|| warn "ntfy notification failed (the deploy itself is unaffected)" || 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. # Serialise per target, not per host: a test deploy must not block a prod one.
readonly LOCK_FILE="${STATE_DIR}/${CD_NAME}.lock" 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)" 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" 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" die "timed out waiting for deploy lock after ${CD_LOCK_WAIT_SECONDS}s"
fi fi
@@ -161,13 +161,13 @@ log "lock acquired"
# -------------------------------------------------------------- image tag --- # -------------------------------------------------------------- image tag ---
tag="$CD_IMAGE_TAG_TEMPLATE" Tag="${CD_IMAGE_TAG_TEMPLATE}"
tag="${tag//\{branch\}/$BRANCH}" Tag="${Tag//\{branch\}/$BRANCH}"
tag="${tag//\{env\}/$CD_ENV}" Tag="${Tag//\{env\}/$CD_ENV}"
tag="${tag//\{sha\}/$SHA}" Tag="${Tag//\{sha\}/$SHA}"
tag="${tag//\{short7\}/${SHA:0:7}}" Tag="${Tag//\{short7\}/${SHA:0:7}}"
tag="${tag//\{short12\}/${SHA:0:12}}" Tag="${Tag//\{short12\}/${SHA:0:12}}"
readonly CANDIDATE="${CD_IMAGE_REPO}:${tag}" readonly CANDIDATE="${CD_IMAGE_REPO}:${Tag}"
log "candidate ${CANDIDATE}" log "candidate ${CANDIDATE}"
@@ -177,23 +177,23 @@ log "candidate ${CANDIDATE}"
# has finished building. Poll rather than fail: the running container keeps # has finished building. Poll rather than fail: the running container keeps
# serving throughout. # serving throughout.
wait_for_image() { wait_for_image() {
local elapsed=0 local Elapsed=0
while true; do while true; do
if docker pull "$CANDIDATE" >/dev/null 2>&1; then if docker pull "${CANDIDATE}" >/dev/null 2>&1; then
log "image available after ${elapsed}s" log "image available after ${Elapsed}s"
return 0 return 0
fi fi
if (( elapsed >= CD_IMAGE_WAIT_SECONDS )); then if (( Elapsed >= CD_IMAGE_WAIT_SECONDS )); then
return 1 return 1
fi fi
log "image not published yet, retrying in ${CD_IMAGE_POLL_INTERVAL}s (${elapsed}s elapsed)" log "image not published yet, retrying in ${CD_IMAGE_POLL_INTERVAL}s (${Elapsed}s elapsed)"
sleep "$CD_IMAGE_POLL_INTERVAL" sleep "${CD_IMAGE_POLL_INTERVAL}"
elapsed=$(( elapsed + CD_IMAGE_POLL_INTERVAL )) Elapsed=$(( Elapsed + CD_IMAGE_POLL_INTERVAL ))
done done
} }
log "waiting for CI to publish the image (up to ${CD_IMAGE_WAIT_SECONDS}s)" 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" log "dry run: skipping image wait"
elif ! wait_for_image; then elif ! wait_for_image; then
notify fail "image ${CANDIDATE} never appeared (waited ${CD_IMAGE_WAIT_SECONDS}s) -- did CI fail?" 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 | grep "^${CD_IMAGE_REPO}@" | head -n1
} }
if [[ $DRY_RUN -eq 1 ]]; then if [[ ${DRY_RUN} -eq 1 ]]; then
DEPLOY_IMAGE="$CANDIDATE" DEPLOY_IMAGE="${CANDIDATE}"
log "dry run: would deploy ${DEPLOY_IMAGE}" log "dry run: would deploy ${DEPLOY_IMAGE}"
else else
revision="$(docker image inspect \ Revision="$(docker image inspect \
--format '{{index .Config.Labels "org.opencontainers.image.revision"}}' \ --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" warn "image carries no org.opencontainers.image.revision label; cannot prove provenance"
elif [[ "$revision" != "$SHA" ]]; then elif [[ "${Revision}" != "${SHA}" ]]; then
notify fail "image ${CANDIDATE} is built from ${revision:0:12}, not ${SHA:0:12} -- refusing to deploy" 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}" die "provenance mismatch: ${CANDIDATE} declares revision ${Revision}, expected ${SHA}"
else else
log "provenance revision label matches ${SHA:0:12}" log "provenance revision label matches ${SHA:0:12}"
fi fi
DEPLOY_IMAGE="$(image_digest_ref "$CANDIDATE")" DEPLOY_IMAGE="$(image_digest_ref "${CANDIDATE}")"
if [[ -z "$DEPLOY_IMAGE" ]]; then if [[ -z "${DEPLOY_IMAGE}" ]]; then
warn "could not resolve a digest for ${CANDIDATE}; deploying by tag instead" warn "could not resolve a digest for ${CANDIDATE}; deploying by tag instead"
DEPLOY_IMAGE="$CANDIDATE" DEPLOY_IMAGE="${CANDIDATE}"
else else
log "pinned ${DEPLOY_IMAGE}" log "pinned ${DEPLOY_IMAGE}"
fi fi
@@ -297,10 +297,10 @@ write_rollback_record() {
} }
check_health() { check_health() {
local attempt=1 body local Attempt=1 Body
while (( attempt <= CD_HEALTH_RETRIES )); do while (( Attempt <= CD_HEALTH_RETRIES )); do
body="$(curl -fsS --max-time 5 "$CD_HEALTH_URL" 2>/dev/null || true)" Body="$(curl -fsS --max-time 5 "${CD_HEALTH_URL}" 2>/dev/null || true)"
if [[ -n "$body" ]]; then if [[ -n "${Body}" ]]; then
if python3 -c ' if python3 -c '
import json, sys import json, sys
key, want = sys.argv[1], sys.argv[2] key, want = sys.argv[1], sys.argv[2]
@@ -309,20 +309,20 @@ try:
except Exception: except Exception:
sys.exit(1) sys.exit(1)
sys.exit(0 if str(data.get(key, "")).lower() == want.lower() else 1) sys.exit(0 if str(data.get(key, "")).lower() == want.lower() else 1)
' "$CD_HEALTH_EXPECT_KEY" "$CD_HEALTH_EXPECT_VALUE" <<<"$body"; then ' "${CD_HEALTH_EXPECT_KEY}" "${CD_HEALTH_EXPECT_VALUE}" <<<"${Body}"; then
log "health ok after ${attempt} attempt(s): ${body}" log "health ok after ${Attempt} attempt(s): ${Body}"
return 0 return 0
fi fi
fi fi
log "health not ready (attempt ${attempt}/${CD_HEALTH_RETRIES}), retrying in ${CD_HEALTH_INTERVAL}s" log "health not ready (attempt ${Attempt}/${CD_HEALTH_RETRIES}), retrying in ${CD_HEALTH_INTERVAL}s"
sleep "$CD_HEALTH_INTERVAL" sleep "${CD_HEALTH_INTERVAL}"
(( attempt++ )) (( Attempt++ ))
done 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 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 recreate service ${CD_COMPOSE_SERVICE} with ${CD_IMAGE_ENV_VAR}=${DEPLOY_IMAGE}"
log "dry run: would health-check ${CD_HEALTH_URL}" log "dry run: would health-check ${CD_HEALTH_URL}"
log "dry run complete -- no changes made" log "dry run complete -- no changes made"
@@ -352,18 +352,18 @@ fi
# ---------------------------------------------------------------- rollback --- # ---------------------------------------------------------------- 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" 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" die "health check failed and rollback is disabled for this target"
fi 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" 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" die "health check failed; no previous image recorded, leaving the stack as it is"
fi fi
warn "health check failed -- rolling back to ${PREVIOUS_IMAGE}" 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" 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}" die "deploy failed health check; rolled back to ${PREVIOUS_IMAGE}"
fi fi
+18 -18
View File
@@ -19,25 +19,25 @@ bold() { printf '\n\033[1m%s\033[0m\n' "$*"; }
bold "Host" bold "Host"
# Capture first: `eval ""` succeeds, so evaluating the failed call inline made # Capture first: `eval ""` succeeds, so evaluating the failed call inline made
# the branch below unreachable and tripped `set -u` instead. # the branch below unreachable and tripped `set -u` instead.
if host_config="$("${REPO_ROOT}/bin/cd-target" host-config 2>/dev/null)"; then if HostConfig="$("${REPO_ROOT}/bin/cd-target" host-config 2>/dev/null)"; then
eval "$host_config" eval "${HostConfig}"
printf ' %s, receiver on %s:%s\n' "$CD_HOST" "$CD_BIND" "$CD_PORT" printf ' %s, receiver on %s:%s\n' "${CD_HOST}" "${CD_BIND}" "${CD_PORT}"
else else
printf ' %s is not configured in targets.json\n' "$(hostname)" printf ' %s is not configured in targets.json\n' "$(hostname)"
printf ' answers to: %s\n' "$("${REPO_ROOT}/bin/cd-target" hostnames)" printf ' answers to: %s\n' "$("${REPO_ROOT}/bin/cd-target" hostnames)"
fi fi
bold "Receiver" bold "Receiver"
state="$(systemctl --user is-active "$UNIT_NAME" 2>/dev/null || true)" State="$(systemctl --user is-active "${UNIT_NAME}" 2>/dev/null || true)"
printf ' %-12s %s\n' "state" "${state:-not installed}" printf ' %-12s %s\n' "state" "${State:-not installed}"
if [[ "$state" == "active" ]]; then if [[ "${State}" == "active" ]]; then
printf ' %-12s %s\n' "since" \ printf ' %-12s %s\n' "since" \
"$(systemctl --user show "$UNIT_NAME" -p ActiveEnterTimestamp --value)" "$(systemctl --user show "${UNIT_NAME}" -p ActiveEnterTimestamp --value)"
if command -v ss >/dev/null 2>&1 && [[ -n "${CD_PORT:-}" ]]; then if command -v ss >/dev/null 2>&1 && [[ -n "${CD_PORT:-}" ]]; then
if ss -ltn 2>/dev/null | grep -q ":${CD_PORT}\b"; then if ss -ltn 2>/dev/null | grep -q ":${CD_PORT}\b"; then
printf ' %-12s listening on port %s\n' "socket" "$CD_PORT" printf ' %-12s listening on port %s\n' "socket" "${CD_PORT}"
else else
printf ' %-12s \033[31mNOT listening on port %s\033[0m\n' "socket" "$CD_PORT" printf ' %-12s \033[31mNOT listening on port %s\033[0m\n' "socket" "${CD_PORT}"
fi fi
fi fi
fi fi
@@ -49,25 +49,25 @@ if [[ -f "${CONFIG_DIR}/hooks.json" ]]; then
fi fi
bold "Targets on this host" bold "Targets on this host"
mapfile -t targets < <("${REPO_ROOT}/bin/cd-target" list 2>/dev/null || true) mapfile -t Targets < <("${REPO_ROOT}/bin/cd-target" list 2>/dev/null || true)
if [[ ${#targets[@]} -eq 0 ]]; then if [[ ${#Targets[@]} -eq 0 ]]; then
printf ' none\n' printf ' none\n'
else else
for target in "${targets[@]}"; do for Target in "${Targets[@]}"; do
# cd-deploy writes this on every terminal outcome. Reading a record # cd-deploy writes this on every terminal outcome. Reading a record
# beats grepping the log for phrases the log is free to reword. # beats grepping the log for phrases the log is free to reword.
status_file="${STATE_DIR}/${target}.status" StatusFile="${STATE_DIR}/${Target}.status"
if [[ -f "$status_file" ]]; then if [[ -f "${StatusFile}" ]]; then
IFS=$'\t' read -r when result sha message < "$status_file" || true IFS=$'\t' read -r When Result Sha Message < "${StatusFile}" || true
printf ' %-22s %s %-4s %s %s\n' \ printf ' %-22s %s %-4s %s %s\n' \
"$target" "$when" "$result" "$sha" "$message" "${Target}" "${When}" "${Result}" "${Sha}" "${Message}"
else else
printf ' %-22s %s\n' "$target" "never deployed from this host" printf ' %-22s %s\n' "${Target}" "never deployed from this host"
fi fi
done done
fi fi
bold "Recent receiver log" bold "Recent receiver log"
journalctl --user -u "$UNIT_NAME" -n 15 --no-pager 2>/dev/null \ journalctl --user -u "${UNIT_NAME}" -n 15 --no-pager 2>/dev/null \
|| printf ' (no journal entries)\n' || printf ' (no journal entries)\n'
echo echo
+17
View File
@@ -15,3 +15,20 @@ update may re-add it; if it's still done, just delete it again.)
- Publishing public releases to a separate GitHub repo → `ai-context/infrastructure/gitea-to-github-release.md` - Publishing public releases to a separate GitHub repo → `ai-context/infrastructure/gitea-to-github-release.md`
- Uploading build artifacts to Cloudflare R2 from CI → `ai-context/services/cloudflare-r2.md` - Uploading build artifacts to Cloudflare R2 from CI → `ai-context/services/cloudflare-r2.md`
- CI success/failure notifications via ntfy → `ai-context/services/ntfy.md` - CI success/failure notifications via ntfy → `ai-context/services/ntfy.md`
## This project
- **What this repo is, how it is installed, troubleshooting** → `docs/OPERATIONS.md`
- **Non-obvious gotchas in this codebase** (health endpoint, `eval` pitfall,
importing `cd-target`) → `docs/hints.md`
- **Design, targets, networking, deployment status** → MCP vault,
`Home IT/Services/CD webhook (fleet deployment).md`
- **Why this exists, how to add a project, testing without a host** → MCP vault,
`Development/CD webhook development.md`
- **What a correct domaindingo deploy is** — this repo automates it, that note
defines it → MCP vault, `Development/DomainDingo Development.md`
- **Shell style for this repo** (constants `UPPER_SNAKE`, locals `PascalCase`,
braced expansions) → MCP vault, `Development/Coding conventions.md`
Before adding a deploy script to any application repository: don't. Add a target
to `targets.json` here instead.
+6
View File
@@ -55,3 +55,9 @@ Short notes on things that were not obvious. Prune stale ones.
holding `DD_ROLLBACK_IMAGE`, `DD_ROLLBACK_REVISION`, `DD_ROLLBACK_CAPTURED_AT`. holding `DD_ROLLBACK_IMAGE`, `DD_ROLLBACK_REVISION`, `DD_ROLLBACK_CAPTURED_AT`.
`cd-deploy` writes the same format in the same place, so an operator following `cd-deploy` writes the same format in the same place, so an operator following
the manual note can recover from an automated deploy. the manual note can recover from an automated deploy.
- **Shell naming follows the vault's `Development/Coding conventions`:**
constants `UPPER_SNAKE`, mutable locals `PascalCase`, every expansion braced
(`"${Name}"`). When bulk-renaming with a regex, exclude comments and message
strings — an earlier pass turned "cd-status" into "cd-Status" and
"revision label" into "Revision label" inside user-facing text.
+16 -16
View File
@@ -30,13 +30,13 @@ die() { printf '\n\033[31mERROR\033[0m %s\n' "$*" >&2; exit 1; }
info "Checking prerequisites" info "Checking prerequisites"
for tool in python3 docker curl flock stdbuf; do for Tool in python3 docker curl flock stdbuf; do
command -v "$tool" >/dev/null 2>&1 || die "required tool not found: ${tool}" command -v "${Tool}" >/dev/null 2>&1 || die "required tool not found: ${Tool}"
done done
ok "python3, docker, curl, flock, stdbuf" ok "python3, docker, curl, flock, stdbuf"
WEBHOOK_BIN="${WEBHOOK_BIN:-$(command -v webhook || true)}" WEBHOOK_BIN="${WEBHOOK_BIN:-$(command -v webhook || true)}"
[[ -n "$WEBHOOK_BIN" && -x "$WEBHOOK_BIN" ]] || die \ [[ -n "${WEBHOOK_BIN}" && -x "${WEBHOOK_BIN}" ]] || die \
"adnanh/webhook not found. Install it, or set WEBHOOK_BIN=/path/to/webhook. "adnanh/webhook not found. Install it, or set WEBHOOK_BIN=/path/to/webhook.
Debian/Ubuntu: sudo apt install webhook Debian/Ubuntu: sudo apt install webhook
Or a release binary from https://github.com/adnanh/webhook/releases" Or a release binary from https://github.com/adnanh/webhook/releases"
@@ -56,36 +56,36 @@ ok "host ${CD_HOST}, binding ${CD_BIND}:${CD_PORT}"
mapfile -t TARGETS < <("${REPO_ROOT}/bin/cd-target" list) mapfile -t TARGETS < <("${REPO_ROOT}/bin/cd-target" list)
[[ ${#TARGETS[@]} -gt 0 ]] || die "no enabled targets for ${CD_HOST} in targets.json" [[ ${#TARGETS[@]} -gt 0 ]] || die "no enabled targets for ${CD_HOST} in targets.json"
for target in "${TARGETS[@]}"; do for Target in "${TARGETS[@]}"; do
ok "target ${target}" ok "target ${Target}"
done done
# ----------------------------------------------------------------- secrets --- # ----------------------------------------------------------------- secrets ---
info "Checking the secrets file" info "Checking the secrets file"
mkdir -p "$CONFIG_DIR" mkdir -p "${CONFIG_DIR}"
chmod 700 "$CONFIG_DIR" chmod 700 "${CONFIG_DIR}"
if [[ ! -f "$SECRETS_FILE" ]]; then if [[ ! -f "${SECRETS_FILE}" ]]; then
install -m 0600 "${REPO_ROOT}/secrets.env.example" "$SECRETS_FILE" install -m 0600 "${REPO_ROOT}/secrets.env.example" "${SECRETS_FILE}"
die "created a template at ${SECRETS_FILE} die "created a template at ${SECRETS_FILE}
Fill in one secret per repository, then re-run this installer. Fill in one secret per repository, then re-run this installer.
The same value must be set as the Secret on the Gitea webhook." The same value must be set as the Secret on the Gitea webhook."
fi fi
chmod 600 "$SECRETS_FILE" chmod 600 "${SECRETS_FILE}"
ok "${SECRETS_FILE}" ok "${SECRETS_FILE}"
# ------------------------------------------------------------------- hooks --- # ------------------------------------------------------------------- hooks ---
info "Rendering hooks for ${CD_HOST}" info "Rendering hooks for ${CD_HOST}"
"${REPO_ROOT}/bin/cd-render-hooks" --output "$HOOKS_FILE" --secrets "$SECRETS_FILE" "${REPO_ROOT}/bin/cd-render-hooks" --output "${HOOKS_FILE}" --secrets "${SECRETS_FILE}"
# ------------------------------------------------------------------ systemd -- # ------------------------------------------------------------------ systemd --
info "Installing the user service" info "Installing the user service"
mkdir -p "$SYSTEMD_DIR" mkdir -p "${SYSTEMD_DIR}"
sed -e "s|@WEBHOOK_BIN@|${WEBHOOK_BIN}|g" \ sed -e "s|@WEBHOOK_BIN@|${WEBHOOK_BIN}|g" \
-e "s|@HOOKS_FILE@|${HOOKS_FILE}|g" \ -e "s|@HOOKS_FILE@|${HOOKS_FILE}|g" \
-e "s|@BIND@|${CD_BIND}|g" \ -e "s|@BIND@|${CD_BIND}|g" \
@@ -94,15 +94,15 @@ sed -e "s|@WEBHOOK_BIN@|${WEBHOOK_BIN}|g" \
ok "${SYSTEMD_DIR}/${UNIT_NAME}" ok "${SYSTEMD_DIR}/${UNIT_NAME}"
systemctl --user daemon-reload systemctl --user daemon-reload
systemctl --user enable "$UNIT_NAME" >/dev/null systemctl --user enable "${UNIT_NAME}" >/dev/null
systemctl --user restart "$UNIT_NAME" systemctl --user restart "${UNIT_NAME}"
ok "enabled and restarted" ok "enabled and restarted"
# Survive logout, same as the other user services in this fleet. # Survive logout, same as the other user services in this fleet.
loginctl enable-linger "$(whoami)" >/dev/null 2>&1 || true loginctl enable-linger "$(whoami)" >/dev/null 2>&1 || true
sleep 1 sleep 1
systemctl --user is-active --quiet "$UNIT_NAME" \ systemctl --user is-active --quiet "${UNIT_NAME}" \
|| die "the service did not stay running: || die "the service did not stay running:
systemctl --user status ${UNIT_NAME} systemctl --user status ${UNIT_NAME}
journalctl --user -u ${UNIT_NAME} -n 50 --no-pager" journalctl --user -u ${UNIT_NAME} -n 50 --no-pager"
@@ -120,7 +120,7 @@ printf ' %-28s %s\n' "Secret" "the matching value from ${SECRETS_FILE}"
echo echo
"${REPO_ROOT}/bin/cd-render-hooks" --list-urls \ "${REPO_ROOT}/bin/cd-render-hooks" --list-urls \
--output "$HOOKS_FILE" --base-url "http://${CD_BIND}:${CD_PORT}" \ --output "${HOOKS_FILE}" --base-url "http://${CD_BIND}:${CD_PORT}" \
| sed 's/^/ /' | sed 's/^/ /'
echo echo
+5 -5
View File
@@ -13,11 +13,11 @@ readonly STATE_DIR="${XDG_STATE_HOME:-${HOME}/.local/state}/cd-webhook"
readonly SYSTEMD_DIR="${HOME}/.config/systemd/user" readonly SYSTEMD_DIR="${HOME}/.config/systemd/user"
readonly UNIT_NAME="cd-webhook.service" readonly UNIT_NAME="cd-webhook.service"
PURGE=0 Purge=0
[[ "${1:-}" == "--purge" ]] && PURGE=1 [[ "${1:-}" == "--purge" ]] && Purge=1
echo "Stopping ${UNIT_NAME} ..." echo "Stopping ${UNIT_NAME} ..."
systemctl --user disable --now "$UNIT_NAME" 2>/dev/null || true systemctl --user disable --now "${UNIT_NAME}" 2>/dev/null || true
rm -f "${SYSTEMD_DIR}/${UNIT_NAME}" rm -f "${SYSTEMD_DIR}/${UNIT_NAME}"
systemctl --user daemon-reload systemctl --user daemon-reload
@@ -25,9 +25,9 @@ systemctl --user daemon-reload
# is regenerated from targets.json plus secrets.env on the next install. # is regenerated from targets.json plus secrets.env on the next install.
rm -f "${CONFIG_DIR}/hooks.json" rm -f "${CONFIG_DIR}/hooks.json"
if [[ $PURGE -eq 1 ]]; then if [[ ${Purge} -eq 1 ]]; then
echo "Purging secrets and deploy logs ..." echo "Purging secrets and deploy logs ..."
rm -rf "$CONFIG_DIR" "$STATE_DIR" rm -rf "${CONFIG_DIR}" "${STATE_DIR}"
else else
echo "Kept ${CONFIG_DIR}/secrets.env and deploy logs in ${STATE_DIR}" echo "Kept ${CONFIG_DIR}/secrets.env and deploy logs in ${STATE_DIR}"
echo " (re-run with --purge to remove them too)" echo " (re-run with --purge to remove them too)"