#!/usr/bin/env bash # Host side of the dashboard release bridge. `serve-request` reads run/release-request.json (the # dashboard wrote it), cuts a release candidate — bump config version, commit, tag vX.Y.Z-rcN, push to Gitea — # and writes a result the dashboard polls. ALL git/token operations run here; the container never # holds credentials. `stage ` is the same path without the request file (used by the test). set -euo pipefail SELF_DIR="$(cd "$(dirname "$0")" && pwd)" PROJ="${CLUSEV_DIR:-$(cd "${SELF_DIR}/../.." && pwd)}" RUN_DIR="${PROJ}/run" BRANCH="${CLUSEV_RELEASE_BRANCH:-feat/v1-foundation}" GITEA_ENV="${CLUSEV_GITEA_ENV:-/home/nexxo/.env.gitea}" _json_str() { local s="$1"; s="${s//\\/\\\\}"; s="${s//\"/\\\"}"; s="${s//$'\r'/}"; s="${s//$'\t'/\\t}"; s="${s//$'\n'/\\n}" printf '"%s"' "$s" } _write_result() { # id ok tag message local id="$1" ok="$2" tag="$3" msg="$4" f="${RUN_DIR}/release-result-$1.json" mkdir -p "$RUN_DIR" 2>/dev/null || true printf '{"id":"%s","ok":%s,"tag":%s,"message":%s,"at":%s}\n' \ "$id" "$ok" "$(_json_str "$tag")" "$(_json_str "$msg")" "$(date +%s 2>/dev/null || echo 0)" > "${f}.tmp" mv -f "${f}.tmp" "$f" 2>/dev/null || { rm -f "${f}.tmp"; return 0; } chown "$(stat -c %u:%g "$RUN_DIR" 2>/dev/null || echo 0:0)" "$f" 2>/dev/null || true chmod 600 "$f" 2>/dev/null || true } # numeric semver "$1 >= $2" _ver_ge() { [ "$(printf '%s\n%s\n' "$1" "$2" | sort -V | head -n1)" = "$2" ]; } _rollback() { # tag git -C "$PROJ" tag -d "$1" >/dev/null 2>&1 || true git -C "$PROJ" reset --hard "origin/${BRANCH}" >/dev/null 2>&1 || true } # Preconditions. Echoes a one-word reason + returns non-zero on failure; silent + 0 on success. _precheck() { git -C "$PROJ" rev-parse --is-inside-work-tree >/dev/null 2>&1 || { echo "no-repo"; return 1; } [ "$(git -C "$PROJ" rev-parse --abbrev-ref HEAD)" = "$BRANCH" ] || { echo "wrong-branch"; return 1; } [ -z "$(git -C "$PROJ" status --porcelain)" ] || { echo "dirty-tree"; return 1; } git -C "$PROJ" fetch origin "$BRANCH" >/dev/null 2>&1 || { echo "fetch-failed"; return 1; } [ "$(git -C "$PROJ" rev-parse HEAD)" = "$(git -C "$PROJ" rev-parse "origin/${BRANCH}")" ] || { echo "unpushed"; return 1; } } # Cut + push a release candidate of . Echoes the new tag on success; reason on stderr + non-zero on failure. _do_stage() { # target local target="$1" cur base cur="$(grep -oE "'version' => '[^']*'" "${PROJ}/config/clusev.php" | head -n1 | sed -E "s/.*=> '//; s/'$//" || true)" base="${cur%%-*}" _ver_ge "$target" "$base" || { echo "downgrade-rejected" >&2; return 1; } local n=0 t k for t in $(git -C "$PROJ" tag -l "v${target}-rc*"); do k="${t##*-rc}" case "$k" in ''|*[!0-9]*) ;; *) [ "$k" -gt "$n" ] && n="$k" ;; esac done local ver="${target}-rc$((n + 1))" newtag="v${target}-rc$((n + 1))" sed -i -E "s/('version' => ')[^']*(')/\\1${ver}\\2/" "${PROJ}/config/clusev.php" git -C "$PROJ" commit -q -am "chore(release): ${ver}" || { echo "commit-failed" >&2; git -C "$PROJ" checkout -- . 2>/dev/null || true; return 1; } git -C "$PROJ" tag "$newtag" || { echo "tag-failed" >&2; _rollback "$newtag"; return 1; } local token origin host url # shellcheck source=/dev/null token="$( . "$GITEA_ENV" 2>/dev/null; printf '%s' "${GIT_ACCESS_TOKEN:-${GITEA_TOKEN:-}}" )" origin="$(git -C "$PROJ" remote get-url origin)" case "$origin" in https://*) [ -n "$token" ] || { echo "no-token" >&2; _rollback "$newtag"; return 1; } host="$(printf '%s' "$origin" | sed -E 's#^https://([^/]*@)?##')" url="https://oauth2:${token}@${host}" ;; *) url="$origin" ;; # local/file remote (tests): push directly esac if ! git -C "$PROJ" push "$url" "$BRANCH" >/dev/null 2>&1 || ! git -C "$PROJ" push "$url" "$newtag" >/dev/null 2>&1; then echo "push-failed" >&2; _rollback "$newtag"; return 1 fi printf '%s' "$newtag" } cmd_serve_request() { local req="${RUN_DIR}/release-request.json" wf="${RUN_DIR}/release-request.processing" [ -f "$req" ] || return 0 mv -f "$req" "$wf" 2>/dev/null || return 0 local json id action target json="$(cat "$wf" 2>/dev/null || true)"; rm -f "$wf" id="$(printf '%s' "$json" | grep -oE '"id":"[A-Za-z0-9]{16,64}"' | head -n1 | sed -E 's/.*:"//; s/"$//' || true)" [ -n "$id" ] || return 0 action="$(printf '%s' "$json" | grep -oE '"action":"[a-z-]{1,16}"' | head -n1 | sed -E 's/.*:"//; s/"$//' || true)" target="$(printf '%s' "$json" | grep -oE '"target":"[0-9]{1,4}\.[0-9]{1,4}\.[0-9]{1,4}"' | head -n1 | sed -E 's/.*:"//; s/"$//' || true)" local ok=false tag='' msg='ok' reason errf errf="$(mktemp)" case "$action" in stage) if [ -z "$target" ]; then msg='invalid-target' elif ! reason="$(_precheck 2>&1)"; then msg="$reason" elif tag="$(_do_stage "$target" 2>"$errf")"; then ok=true; msg='ok' else msg="$(cat "$errf" 2>/dev/null || echo 'release-failed')"; tag='' ; fi ;; *) msg='unknown-action' ;; esac rm -f "$errf" _write_result "$id" "$ok" "$tag" "$msg" } case "${1:-}" in serve-request) cmd_serve_request ;; stage) target="${2:?target required}" if reason="$(_precheck 2>&1)"; then _do_stage "$target"; else printf '%s\n' "$reason" >&2; exit 1; fi ;; *) echo "usage: $0 serve-request|stage " >&2; exit 2 ;; esac