#!/usr/bin/env bash # Publishes this agent's cross-compiled binaries as a Gitea release. # # The release is what makes the install command real: `msa-agent enroll` runs on the node, and the node # has no Go toolchain, so the binary has to come from somewhere. Before this the only way to get one was # to build it on another machine and scp it, which is a step the install command cannot express. # # The token is read from the git credential helper rather than an environment variable, so it is not in # the process table and not in a shell history. Override with MSA_GITEA_TOKEN when running somewhere # that has no helper. # # Usage: # ./scripts/release.sh # publish the version in the Makefile # ./scripts/release.sh --tag v1.0.0 # publish an explicit tag # ./scripts/release.sh --dry-run # build, checksum, print, upload nothing # # This repository must stay publicly readable: the install command fetches the binary from a node that # has no credentials, and Gitea answers 404 for a private repository, so the failure on the node looks # like a wrong URL. The script checks that and refuses rather than publishing something unreachable. set -euo pipefail ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" cd "$ROOT" REMOTE="$(git config --get remote.origin.url)" HOST="$(printf '%s' "$REMOTE" | sed -E 's#^https?://([^/]+)/.*#\1#')" SLUG="$(printf '%s' "$REMOTE" | sed -E 's#^https?://[^/]+/##; s#\.git$##')" if [[ -z "$HOST" || -z "$SLUG" ]]; then echo "release: cannot parse the origin remote ($REMOTE)" >&2 exit 1 fi VERSION="$(sed -n 's/^VERSION[[:space:]]*?*=[[:space:]]*//p' Makefile | head -1)" TAG="v${VERSION}" DRY_RUN=0 while [[ $# -gt 0 ]]; do case "$1" in --tag) TAG="$2"; shift 2 ;; --dry-run) DRY_RUN=1; shift ;; *) echo "release: unknown argument $1" >&2; exit 2 ;; esac done step() { printf '\n\033[1m== %s\033[0m\n' "$1"; } step "building ${TAG}" # VERSION is passed through so the binary reports the tag it was published under, which is what an # operator sees in `msa-agent version` and in the agent's startup log line. make build-arm64 build-armv7 VERSION="${VERSION}" step "checksums" ( cd dist shasum -a 256 msa-agent-linux-arm64 msa-agent-linux-armv7 > SHA256SUMS cat SHA256SUMS ) if [[ "$DRY_RUN" -eq 1 ]]; then step "dry run: nothing was uploaded" exit 0 fi TOKEN="${MSA_GITEA_TOKEN:-}" if [[ -z "$TOKEN" ]]; then # The credential helper is what `git push` already uses, so there is no second secret to keep. TOKEN="$(printf 'protocol=https\nhost=%s\n\n' "$HOST" | git credential fill 2>/dev/null | sed -n 's/^password=//p')" fi if [[ -z "$TOKEN" ]]; then echo "release: no API token. Set MSA_GITEA_TOKEN, or make sure git can authenticate to $HOST." >&2 exit 1 fi API="https://${HOST}/api/v1/repos/${SLUG}" AUTH=(-H "Authorization: token ${TOKEN}") step "target repository" PRIVATE="$(curl -sf "${AUTH[@]}" "${API}" | python3 -c 'import json,sys; print(json.load(sys.stdin).get("private"))' 2>/dev/null || echo unknown)" echo "${SLUG} (private: ${PRIVATE})" if [[ "$PRIVATE" != "False" ]]; then echo "release: ${SLUG} is not publicly readable. A node fetching the binary has no credentials and" >&2 echo " would get 404, which looks like a wrong URL. Make it public before publishing." >&2 exit 1 fi step "creating the release ${TAG}" # A release that already exists is reused rather than refused: re-running this after a fix should # replace the binaries, and failing here would mean the operator has to delete the release by hand. EXISTING="$(curl -sf "${AUTH[@]}" "${API}/releases/tags/${TAG}" 2>/dev/null || true)" if [[ -n "$EXISTING" ]]; then RELEASE_ID="$(printf '%s' "$EXISTING" | python3 -c 'import json,sys; print(json.load(sys.stdin)["id"])')" echo "reusing release ${TAG} (id ${RELEASE_ID})" else BODY="$(python3 - "$TAG" "$VERSION" <<'PY' import json, sys tag, version = sys.argv[1], sys.argv[2] print(json.dumps({ "tag_name": tag, "name": f"simadmin-relay agent {version}", "body": ( "Outbound-only node agent for SimAdmin nodes, cross-compiled for the ARM Debian boxes SimAdmin " "runs on. Static, no runtime dependencies, no third-party modules.\n\n" "Install on a node:\n\n" "```sh\n" f"curl -fsSL https://HOST/SLUG/releases/download/{tag}/msa-agent-linux-arm64 \\\n" " -o /opt/msa-agent/msa-agent && chmod 755 /opt/msa-agent/msa-agent\n" "/opt/msa-agent/msa-agent enroll --relay https://your-relay --code \\\n" " --write /etc/msa-agent/config.json\n" "/opt/msa-agent/msa-agent install\n" "```\n\n" "`armv7` is the same build for 32-bit ARM. `SHA256SUMS` covers both.\n\n" "Removing it again is `msa-agent uninstall` on the node; `--dry-run` shows the plan first.\n" ), "draft": False, "prerelease": False, }, ensure_ascii=False)) PY )" BODY="${BODY//HOST/$HOST}" BODY="${BODY//SLUG/$SLUG}" RELEASE_ID="$(curl -sf -X POST "${AUTH[@]}" -H 'Content-Type: application/json' \ "${API}/releases" -d "$BODY" | python3 -c 'import json,sys; print(json.load(sys.stdin)["id"])')" echo "created release ${TAG} (id ${RELEASE_ID})" fi step "uploading binaries" for asset in msa-agent-linux-arm64 msa-agent-linux-armv7 SHA256SUMS; do # An asset with the same name cannot be replaced, so an existing one is deleted first: otherwise a # re-run after a rebuild would silently leave the previous binary in place. EXISTING_ASSET="$(curl -sf "${AUTH[@]}" "${API}/releases/${RELEASE_ID}/assets" \ | python3 -c " import json, sys name = sys.argv[1] for asset in json.load(sys.stdin): if asset['name'] == name: print(asset['id']) break " "$asset" || true)" if [[ -n "$EXISTING_ASSET" ]]; then # Gitea's delete path carries the release id; GitHub's does not. Using GitHub's shape here returns # 404 and, under `set -e`, killed the script silently after the first asset — which is how a # re-publish left the old binary in place while looking like it had replaced it. curl -sf -X DELETE "${AUTH[@]}" "${API}/releases/${RELEASE_ID}/assets/${EXISTING_ASSET}" >/dev/null echo " replaced ${asset}" fi curl -sf -X POST "${AUTH[@]}" \ -F "attachment=@dist/${asset}" \ "${API}/releases/${RELEASE_ID}/assets?name=${asset}" >/dev/null echo " uploaded ${asset}" done step "published" printf 'release: https://%s/%s/releases/tag/%s\n' "$HOST" "$SLUG" "$TAG" printf 'download: https://%s/%s/releases/download/%s/msa-agent-linux-arm64\n' "$HOST" "$SLUG" "$TAG" printf '\n' # The app renders the install command, so the URL it carries has to be this one. Printing it in the # exact form DefaultContainer holds is what makes the two comparable by eye. printf 'DefaultContainer.agentDownload must be:\n' printf ' https://%s/%s/releases/download/%s/msa-agent-linux-arm64\n' "$HOST" "$SLUG" "$TAG" printf '\nVerify it is fetchable with no credentials:\n' printf ' curl -fsSL -o /dev/null -w "%%{http_code}\\n" %s\n' \ "https://${HOST}/${SLUG}/releases/download/${TAG}/msa-agent-linux-arm64"