#!/usr/bin/env bash set -Eeuo pipefail umask 077 MODE="${1:---check}" BASE="/opt/router-ops" REPOSITORY_URL="https://github.com/DChuvelev/wg-paid-vm121.git" REPO_DIR="$BASE/state/source-repositories/wg-paid-vm121" STATE_FILE="$BASE/state/current-vm121-git-source.env" TUNNEL_ENV="$BASE/state/current-quick-tunnel.env" TOKEN_FILE="$BASE/public/.router-public-token" PUBLISH_TOOL="$BASE/bin/router-publish-report" LOCK_DIR="$BASE/state/locks" LOCK_FILE="$LOCK_DIR/vm121-git-source-publish.lock" TMPDIR_RUN="$(mktemp -d /tmp/vm121-git-source-publish.XXXXXX)" VM121_OUTPUT="$TMPDIR_RUN/vm121-publish.txt" cleanup() { rm -rf "$TMPDIR_RUN" } trap cleanup EXIT INT TERM log() { echo ">>> [vm121-source] $*" } stop() { echo "STOP=$*" exit 1 } case "$MODE" in --check|--publish) ;; *) echo "Usage: $0 --check" echo " $0 --publish" exit 2 ;; esac mkdir -p \ "$(dirname "$REPO_DIR")" \ "$LOCK_DIR" exec 9>"$LOCK_FILE" if ! flock -n 9; then stop "ANOTHER_VM121_SOURCE_PUBLISH_IS_RUNNING" fi log "precheck" command -v git >/dev/null 2>&1 command -v tar >/dev/null 2>&1 command -v gzip >/dev/null 2>&1 command -v sha256sum >/dev/null 2>&1 command -v python3 >/dev/null 2>&1 command -v flock >/dev/null 2>&1 test -x "$PUBLISH_TOOL" test -f "$TOKEN_FILE" test -f "$TUNNEL_ENV" echo "MODE=$MODE" echo "REPOSITORY_URL=$REPOSITORY_URL" echo "REPO_DIR=$REPO_DIR" log "run VM121 git publisher through approved direct SSH" if [ "$MODE" = "--publish" ]; then VM121_MODE="--publish" else VM121_MODE="--check" fi set +e ssh \ -o BatchMode=yes \ -o ConnectTimeout=15 \ vm121 \ "/usr/local/sbin/router-vm121-git-publish.sh \ '$VM121_MODE'" \ 2>&1 | tee "$VM121_OUTPUT" VM121_RC=${PIPESTATUS[0]} set -e if [ "$VM121_RC" -ne 0 ]; then stop "VM121_GIT_PUBLISHER_FAILED_RC_$VM121_RC" fi VM121_RESULT="$( awk -F= ' /^RESULT=/ { value=$2 } END { print value } ' "$VM121_OUTPUT" )" VM121_COMMIT="$( awk -F= ' /^GIT_COMMIT=/ { value=$2 } END { print value } ' "$VM121_OUTPUT" )" if [ -z "$VM121_COMMIT" ]; then VM121_COMMIT="$( awk -F= ' /^LOCAL_HEAD=/ { value=$2 } END { print value } ' "$VM121_OUTPUT" )" fi VM121_REMOTE_COMMIT="$( awk -F= ' /^REMOTE_COMMIT=/ { value=$2 } /^REMOTE_HEAD=/ { if (value == "") value=$2 } END { print value } ' "$VM121_OUTPUT" )" case "$VM121_COMMIT" in [0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f]*) ;; *) stop "INVALID_VM121_COMMIT_$VM121_COMMIT" ;; esac echo echo "VM121_RESULT=$VM121_RESULT" echo "VM121_COMMIT=$VM121_COMMIT" echo "VM121_REMOTE_COMMIT=$VM121_REMOTE_COMMIT" if [ -n "$VM121_REMOTE_COMMIT" ] && [ "$VM121_REMOTE_COMMIT" != "$VM121_COMMIT" ] then stop "VM121_LOCAL_REMOTE_COMMIT_MISMATCH" fi log "clone or refresh repository on VM130" if [ ! -d "$REPO_DIR/.git" ]; then rm -rf "$REPO_DIR" git clone \ --branch main \ "$REPOSITORY_URL" \ "$REPO_DIR" else CURRENT_ORIGIN="$( git -C "$REPO_DIR" remote get-url origin )" echo "CURRENT_ORIGIN=$CURRENT_ORIGIN" if [ "$CURRENT_ORIGIN" != "$REPOSITORY_URL" ]; then stop "UNEXPECTED_REPOSITORY_ORIGIN_$CURRENT_ORIGIN" fi git -C "$REPO_DIR" fetch \ --prune \ origin \ main git -C "$REPO_DIR" checkout \ -B main \ origin/main git -C "$REPO_DIR" reset \ --hard \ origin/main git -C "$REPO_DIR" clean \ -fdx fi VM130_COMMIT="$( git -C "$REPO_DIR" rev-parse HEAD )" VM130_TREE="$( git -C "$REPO_DIR" show -s --format=%T HEAD )" VM130_FILE_COUNT="$( git -C "$REPO_DIR" \ ls-tree \ -r \ --full-tree \ --name-only \ HEAD | awk 'END {print NR + 0}' )" VM130_STATUS="$( git -C "$REPO_DIR" status --porcelain )" echo echo "VM130_COMMIT=$VM130_COMMIT" echo "VM130_TREE=$VM130_TREE" echo "VM130_FILE_COUNT=$VM130_FILE_COUNT" echo "VM130_STATUS=${VM130_STATUS:-CLEAN}" test "$VM130_COMMIT" = "$VM121_COMMIT" test -z "$VM130_STATUS" log "verify required managed paths" for REQUIRED_PATH in \ etc/router-machine-git-source.conf \ usr/local/sbin/router-machine-git-publish.sh \ usr/local/sbin/router-vm121-git-publish.sh do if [ ! -f "$REPO_DIR/$REQUIRED_PATH" ]; then stop "REQUIRED_PATH_MISSING_$REQUIRED_PATH" fi echo "REQUIRED_PATH_OK=$REQUIRED_PATH" done if [ "$MODE" = "--check" ]; then echo echo "RESULT=PASS_VM130_VM121_GIT_SOURCE_CHECK" echo "VM121_COMMIT=$VM121_COMMIT" echo "VM130_COMMIT=$VM130_COMMIT" echo "VM130_TREE=$VM130_TREE" echo "VM130_FILE_COUNT=$VM130_FILE_COUNT" echo "VM121_PUSH_PERFORMED=false" echo "ARCHIVE_CREATED=false" echo "CLOUDFLARE_PUBLISHED=false" exit 0 fi log "create archive and metadata from exact Git commit" SHORT_COMMIT="${VM130_COMMIT:0:12}" GENERATED_AT_UTC="$(date -u +%Y%m%d-%H%M%S)" ARCHIVE_NAME="wg-paid-vm121-${SHORT_COMMIT}.tar.gz" ARCHIVE="$TMPDIR_RUN/$ARCHIVE_NAME" ARCHIVE_SHA_FILE="$TMPDIR_RUN/${ARCHIVE_NAME}.sha256" ARCHIVE_B64_NAME="${ARCHIVE_NAME}.b64.txt" ARCHIVE_B64="$TMPDIR_RUN/$ARCHIVE_B64_NAME" MANIFEST="$TMPDIR_RUN/git-tree-manifest.txt" PATHS="$TMPDIR_RUN/managed-paths.txt" COMMIT_INFO="$TMPDIR_RUN/git-commit.txt" FACTS="$TMPDIR_RUN/facts.json" git -C "$REPO_DIR" archive \ --format=tar.gz \ --prefix="wg-paid-vm121-${SHORT_COMMIT}/" \ "$VM130_COMMIT" \ > "$ARCHIVE" sha256sum "$ARCHIVE" \ > "$ARCHIVE_SHA_FILE" python3 - "$ARCHIVE" "$ARCHIVE_B64" <<'PY' import base64 import pathlib import sys import textwrap source = pathlib.Path(sys.argv[1]) output = pathlib.Path(sys.argv[2]) encoded = base64.b64encode(source.read_bytes()).decode("ascii") with output.open("w", encoding="ascii", newline="\n") as stream: for line in textwrap.wrap(encoded, 76): stream.write(line) stream.write("\n") PY git -C "$REPO_DIR" \ ls-tree \ -r \ --full-tree \ "$VM130_COMMIT" \ > "$MANIFEST" git -C "$REPO_DIR" \ ls-tree \ -r \ --full-tree \ --name-only \ "$VM130_COMMIT" \ > "$PATHS" git -C "$REPO_DIR" \ show \ -s \ --format=fuller \ "$VM130_COMMIT" \ > "$COMMIT_INFO" ARCHIVE_SHA256="$( awk '{print $1}' "$ARCHIVE_SHA_FILE" )" python3 - \ "$FACTS" \ "$GENERATED_AT_UTC" \ "$REPOSITORY_URL" \ "$VM130_COMMIT" \ "$VM130_TREE" \ "$VM130_FILE_COUNT" \ "$ARCHIVE_NAME" \ "$ARCHIVE_SHA256" <<'PY' import json import sys ( output, generated_at, repository, commit, tree, file_count, archive, archive_sha256, ) = sys.argv[1:] facts = { "result": "PASS_VM121_GIT_SOURCE_ARCHIVE", "generated_at_utc": generated_at, "repository": repository, "branch": "main", "commit": commit, "tree": tree, "file_count": int(file_count), "archive": archive, "archive_sha256": archive_sha256, "source_contract": "VM121 live worktree -> Git commit -> GitHub main -> VM130 checkout -> Git archive", } with open(output, "w", encoding="utf-8") as stream: json.dump(facts, stream, ensure_ascii=False, indent=2) stream.write("\n") PY python3 - "$FACTS" "$ARCHIVE_B64_NAME" <<'PY' import json import pathlib import sys facts_path = pathlib.Path(sys.argv[1]) archive_b64_name = sys.argv[2] facts = json.loads(facts_path.read_text(encoding="utf-8")) facts["archive_base64_transport"] = archive_b64_name facts["archive_base64_encoding"] = "base64" facts["archive_base64_decodes_to"] = facts["archive"] facts_path.write_text( json.dumps(facts, ensure_ascii=False, indent=2) + "\n", encoding="utf-8", ) PY echo "ARCHIVE=$ARCHIVE" echo "ARCHIVE_SHA256=$ARCHIVE_SHA256" echo "ARCHIVE_BASE64=$ARCHIVE_B64" echo "MANIFEST=$MANIFEST" echo "PATHS=$PATHS" echo "FACTS=$FACTS" log "publish atomically into latest/ and archive prior version into old/" LABEL="vm121_git_source_${SHORT_COMMIT}" PUBLISH_OUTPUT="$( "$PUBLISH_TOOL" \ "$LABEL" \ "$ARCHIVE" \ "$ARCHIVE_SHA_FILE" \ "$ARCHIVE_B64" \ "$MANIFEST" \ "$PATHS" \ "$COMMIT_INFO" \ "$FACTS" \ "$VM121_OUTPUT" )" printf '%s\n' "$PUBLISH_OUTPUT" PUBLISHED_DIR="$( printf '%s\n' "$PUBLISH_OUTPUT" | awk -F= ' /^PUBLISHED_DIR=/ { print $2 } ' )" test -n "$PUBLISHED_DIR" test -d "$PUBLISHED_DIR" PUBLISHED_RELATIVE_PATH="$( printf '%s\n' "$PUBLISH_OUTPUT" | awk -F= ' /^PUBLISHED_RELATIVE_PATH=/ { print substr($0, index($0, "=") + 1) } ' )" PUBLIC_TOKEN="$(cat "$TOKEN_FILE")" QUICK_TUNNEL_URL="$( awk -F= ' $1 == "QUICK_TUNNEL_URL" { print substr($0, index($0, "=") + 1) } ' "$TUNNEL_ENV" )" test -n "$PUBLISHED_RELATIVE_PATH" case "$PUBLISHED_RELATIVE_PATH" in latest/*) ;; *) stop "INVALID_PUBLISHED_RELATIVE_PATH_$PUBLISHED_RELATIVE_PATH" ;; esac test -n "$QUICK_TUNNEL_URL" SOURCE_URL="${QUICK_TUNNEL_URL%/}/r/${PUBLIC_TOKEN}/${PUBLISHED_RELATIVE_PATH%/}/" ARCHIVE_URL="${SOURCE_URL}${ARCHIVE_NAME}" ARCHIVE_B64_URL="${SOURCE_URL}${ARCHIVE_B64_NAME}" FACTS_URL="${SOURCE_URL}facts.json" MANIFEST_URL="${SOURCE_URL}git-tree-manifest.txt" log "save current source archive state" STATE_TMP="${STATE_FILE}.tmp.$$" { echo "VM121_GIT_SOURCE_UPDATED_AT_UTC=$GENERATED_AT_UTC" echo "VM121_GIT_SOURCE_REPOSITORY=$REPOSITORY_URL" echo "VM121_GIT_SOURCE_BRANCH=main" echo "VM121_GIT_SOURCE_COMMIT=$VM130_COMMIT" echo "VM121_GIT_SOURCE_TREE=$VM130_TREE" echo "VM121_GIT_SOURCE_FILE_COUNT=$VM130_FILE_COUNT" echo "VM121_GIT_SOURCE_PUBLIC_DIR=$PUBLISHED_DIR" echo "VM121_GIT_SOURCE_URL=$SOURCE_URL" echo "VM121_GIT_SOURCE_ARCHIVE_URL=$ARCHIVE_URL" echo "VM121_GIT_SOURCE_ARCHIVE_B64_URL=$ARCHIVE_B64_URL" echo "VM121_GIT_SOURCE_FACTS_URL=$FACTS_URL" echo "VM121_GIT_SOURCE_MANIFEST_URL=$MANIFEST_URL" echo "VM121_GIT_SOURCE_ARCHIVE_SHA256=$ARCHIVE_SHA256" } > "$STATE_TMP" chmod 600 "$STATE_TMP" mv "$STATE_TMP" "$STATE_FILE" echo echo "RESULT=PASS_VM130_VM121_GIT_SOURCE_PUBLISH" echo "VM121_GIT_SOURCE_COMMIT=$VM130_COMMIT" echo "VM121_GIT_SOURCE_TREE=$VM130_TREE" echo "VM121_GIT_SOURCE_FILE_COUNT=$VM130_FILE_COUNT" echo "VM121_GIT_SOURCE_URL=$SOURCE_URL" echo "VM121_GIT_SOURCE_ARCHIVE_URL=$ARCHIVE_URL" echo "VM121_GIT_SOURCE_ARCHIVE_B64_URL=$ARCHIVE_B64_URL" echo "VM121_GIT_SOURCE_FACTS_URL=$FACTS_URL" echo "VM121_GIT_SOURCE_MANIFEST_URL=$MANIFEST_URL" echo "VM121_GIT_SOURCE_ARCHIVE_SHA256=$ARCHIVE_SHA256" echo "VM121_PUSH_PERFORMED=true" echo "ARCHIVE_CREATED=true" echo "CLOUDFLARE_PUBLISHED=true"