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:
+69
-69
@@ -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
@@ -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
|
||||||
|
|||||||
@@ -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.
|
||||||
|
|||||||
@@ -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
@@ -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
|
||||||
|
|||||||
@@ -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)"
|
||||||
|
|||||||
Reference in New Issue
Block a user