#!/usr/bin/env bash # # Shared release helpers for install.sh and update.sh. # # Two deployment modes, and the difference matters: # # release — pinned to an immutable tag, checked out detached. What a # production server should be on: it moves only when someone # decides it moves. # branch — following the head of a line, usually main. The edge. CI tags a # commit AFTER it is pushed, so origin/main is briefly, and after a # failure permanently, code nobody has verified. # # The mode is persisted, because update.sh cannot infer it: a detached checkout # and a branch checkout need entirely different git commands, and guessing wrong # either fails loudly or silently drags a pinned server onto the edge. MODE_FILE="storage/app/deploy-mode" MANIFEST_FILE="storage/app/deployment.json" # The release number the checkout claims to be. release_version() { tr -d ' \n\r' < VERSION 2>/dev/null || echo '0.0.0'; } # release_mode — "release" or "branch"; defaults to branch for installs that # predate this file. release_mode() { cat "$MODE_FILE" 2>/dev/null || echo 'branch'; } release_source() { cat "${MODE_FILE}.source" 2>/dev/null || echo ''; } # The commit the manifest claims is deployed. Read with sed rather than a JSON # parser so this has no dependency the installer does not already guarantee. release_manifest_commit() { sed -n 's/.*"commit"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p' "$MANIFEST_FILE" 2>/dev/null | head -1 } release_remember() { # release_remember MODE SOURCE mkdir -p "$(dirname "$MODE_FILE")" printf '%s' "$1" > "$MODE_FILE" printf '%s' "$2" > "${MODE_FILE}.source" } # release_would_go_backwards BASE TARGET — true when TARGET is an ancestor of # BASE, i.e. this is a move back in history. # # Ancestry, not version strings: SemVer order and git history are different # things, and a tag can be cut from anywhere. Ancestry is what actually says # whether the code is going backwards. release_would_go_backwards() { local base="$1" target="$2" [[ "$base" == "$target" ]] && return 1 git merge-base --is-ancestor "$target" "$base" 2>/dev/null } # release_write_manifest COMMIT SOURCE MODE # # Written only once every step has succeeded, and written atomically: a # half-written manifest read by the console would report a version that never # ran. The console reads this rather than live git, because git says what the # files are, not whether the deployment finished. release_write_manifest() { local commit="$1" source="$2" mode="$3" tmp mkdir -p "$(dirname "$MANIFEST_FILE")" tmp="$(mktemp "${MANIFEST_FILE}.XXXXXX")" printf '{\n "version": "%s",\n "commit": "%s",\n "source": "%s",\n "mode": "%s",\n "deployed_at": "%s"\n}\n' \ "$(json_escape "$(release_version)")" \ "$(json_escape "$commit")" \ "$(json_escape "$source")" \ "$(json_escape "$mode")" \ "$(date -u +%Y-%m-%dT%H:%M:%SZ)" > "$tmp" chmod 644 "$tmp" mv -f "$tmp" "$MANIFEST_FILE" } # json_escape STRING — a branch name is chosen by whoever made the branch, and # a quote or a backslash in one would produce a document the console reads as # absent: it would lose the deployment metadata after a deployment that # otherwise went perfectly. No jq dependency, because this has to work on a # server installed before jq was ever on the package list. json_escape() { local s="$1" s="${s//\\/\\\\}" s="${s//\"/\\\"}" # Control characters cannot occur in a git ref and cannot be represented # raw; dropping them beats emitting a broken document. printf '%s' "$s" | tr -d '\000-\037' }