4d82591052
gates / gates (push) Successful in 7s
R-115's whole point is that publishing cannot be forgotten because it rides the release script. On the first real release through it (v0.121.0, R-86) it died at exactly that leg: scripts/release-agent.sh: line 101: .../scripts/publish-agent.sh: Permission denied publish-agent.sh has been mode 0644 since it was created on 2026-06-28 — every earlier caller ran it as `bash scripts/publish-agent.sh`, so nothing ever noticed, and release-agent.sh (written the same day it was needed) called it directly. Two fixes, both small and both wanted: restore the executable bit, and invoke it through `bash` so the release no longer depends on a file mode — the kind of thing a checkout, an archive or a copy loses again. The v0.121.0 tag created by the failed run is withdrawn and recreated on this commit; nothing was published under it (verified 404 on the package endpoint), so one version name still means one binary.
140 lines
8.3 KiB
Bash
Executable File
140 lines
8.3 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
# release-agent.sh — THE way to release a felhom-agent version. One act: build → tag → publish →
|
|
# verify by independent download.
|
|
#
|
|
# WHY THIS EXISTS (R-115). Publishing used to be a step someone had to remember, and it was
|
|
# forgotten THREE TIMES IN FIVE DAYS:
|
|
#
|
|
# * R-111 (2026-07-29) 17 releases v0.97.0-v0.113.0 built and never published, so a new customer
|
|
# would have installed without the whole R-82 tiered-backup arc, F-CRIT-2 and F-REBOOT.
|
|
# * 0.114.0 (same afternoon) built, deployed to felhom-pve, never published.
|
|
# * 0.120.0 (2026-08-03) built, committed and deployed to BOTH demo hosts, never published. A
|
|
# documented-path reinstall would have silently DOWNGRADED both boxes to the pre-merge
|
|
# agent — and would have *succeeded* while doing it, because the current `step_grows`
|
|
# sets SYSDATA_GROW=0 so the older agent's fatal mp1 resize never fires.
|
|
#
|
|
# R-111's own closing line said publishing should join the release train rather than stay a
|
|
# remembered step. It closed SHIPPED without that leg, and the leg recurred the same afternoon —
|
|
# which is the evidence that a note is not a mechanism. This file is the mechanism. The
|
|
# documentation now points here instead of at a raw `go build` line, so there is ONE documented way
|
|
# to release and it cannot complete without publishing.
|
|
#
|
|
# WHY IT TAGS (R-183). Since felhom-host-install.sh pins its sixteen agent-config fetches to
|
|
# `raw/tag/v<version>`, a released version without a git tag 404s a box mid-install, as root, on a
|
|
# virgin machine. The tag and the package are two halves of one release and are created together.
|
|
#
|
|
# WHY IT DOES NOT VOUCH. Vouching is what points machines at a version, and it stays the operator's
|
|
# deliberate act — the same prove-then-vouch principle that governed the golden two sessions ago.
|
|
# This script prints the version and sha to vouch; a human decides when.
|
|
#
|
|
# Usage:
|
|
# GITEA_USER=admin GITEA_TOKEN=<token> ./scripts/release-agent.sh <version>
|
|
#
|
|
# Env: GITEA_USER/GITEA_TOKEN (package write) — same credentials publish-agent.sh already takes.
|
|
# GITEA_BASE / GITEA_OWNER override the defaults.
|
|
# RELEASE_ALLOW_DIRTY=1 skips the clean-tree gate (for a rehearsal; never for a real release).
|
|
set -euo pipefail
|
|
|
|
GITEA_BASE="${GITEA_BASE:-https://gitea.dooplex.hu}"
|
|
GITEA_OWNER="${GITEA_OWNER:-admin}"
|
|
REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
|
|
|
die() { echo "[release-agent] ERROR: $*" >&2; exit 1; }
|
|
log() { echo "[release-agent] $*" >&2; }
|
|
|
|
VERSION="${1:-}"
|
|
[[ -n "$VERSION" ]] || die "version required (usage: GITEA_USER=.. GITEA_TOKEN=.. $0 <version>)"
|
|
[[ "$VERSION" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]] || die "version must be bare semver X.Y.Z (got '$VERSION')"
|
|
TAG="v$VERSION"
|
|
|
|
cd "$REPO_ROOT"
|
|
|
|
# ── 1. Clean-tree gate ──────────────────────────────────────────────────────────────────────────
|
|
# An unpushed change does not exist. Releasing a dirty tree publishes a binary whose source nobody
|
|
# else can obtain, and tags a commit that does not contain what was built.
|
|
if [[ "${RELEASE_ALLOW_DIRTY:-0}" != "1" ]]; then
|
|
[[ -z "$(git status --porcelain)" ]] || die "working tree is dirty — commit and push first"
|
|
local_head="$(git rev-parse HEAD)"
|
|
git fetch -q origin main
|
|
[[ "$local_head" == "$(git rev-parse origin/main)" ]] \
|
|
|| die "HEAD != origin/main — push first (an unpushed change does not exist)"
|
|
fi
|
|
|
|
# ── 2. Refuse to re-release a version that already exists ───────────────────────────────────────
|
|
# Silently overwriting a published artifact is how "the same version" comes to mean two different
|
|
# binaries on two different boxes.
|
|
if git rev-parse -q --verify "refs/tags/$TAG" >/dev/null; then
|
|
die "tag $TAG already exists — releasing over it would make one version name two binaries"
|
|
fi
|
|
existing="$(curl -fsS -o /dev/null -w '%{http_code}' \
|
|
"$GITEA_BASE/api/packages/$GITEA_OWNER/generic/felhom-agent/$VERSION/felhom-agent" 2>/dev/null || true)"
|
|
[[ "$existing" != "200" ]] || die "version $VERSION is ALREADY PUBLISHED — bump the version instead"
|
|
|
|
# ── 3. Build ────────────────────────────────────────────────────────────────────────────────────
|
|
BIN="$(mktemp -t felhom-agent-XXXXXX)"
|
|
trap 'rm -f "$BIN"' EXIT
|
|
log "building $VERSION …"
|
|
go build -ldflags "-X main.version=$VERSION" -o "$BIN" ./cmd/felhom-agent \
|
|
|| die "go build failed"
|
|
built_ver="$("$BIN" --version 2>/dev/null | awk '{print $2}')"
|
|
[[ "$built_ver" == "$VERSION" ]] \
|
|
|| die "the built binary reports '$built_ver', not '$VERSION' — the ldflag did not take"
|
|
BUILT_SHA="$(sha256sum "$BIN" | awk '{print $1}')"
|
|
log "built ok: sha256 $BUILT_SHA"
|
|
|
|
# ── 4. Tag (before publishing, so a published version always has a tag) ─────────────────────────
|
|
# Order matters in this direction only: a tag with no package is caught by
|
|
# scripts/check-published-versions.py on the next CI run; a package with no tag is invisible to it,
|
|
# because the Gitea package LISTING api needs a token the gate does not have.
|
|
log "tagging $TAG at $(git rev-parse --short HEAD) …"
|
|
git tag -a "$TAG" -m "agent $TAG
|
|
|
|
Released by scripts/release-agent.sh.
|
|
sha256 of the published binary: $BUILT_SHA
|
|
|
|
felhom-host-install.sh fetches this version's config files from raw/tag/$TAG/configs/,
|
|
so this tag is part of the released artifact, not a bookmark (R-183)."
|
|
git push origin "$TAG" || die "tag push failed — refusing to publish an untagged version"
|
|
|
|
# ── 5. Publish (the existing script; deliberately not reimplemented) ────────────────────────────
|
|
log "publishing …"
|
|
# Invoked through `bash` DELIBERATELY, not as an executable. On 2026-08-03 the first real release
|
|
# through this script died here — `publish-agent.sh` has been mode 0644 since it was created on
|
|
# 2026-06-28, because every earlier caller ran it as `bash scripts/publish-agent.sh`. So the one leg
|
|
# R-115 exists to make unforgettable was, on its first use, unrunnable. The mode bit is restored in
|
|
# the same commit; this line makes the release independent of it, because a file mode is exactly the
|
|
# kind of thing that is lost again by a checkout, an archive, or a copy.
|
|
bash "$REPO_ROOT/scripts/publish-agent.sh" "$VERSION" "$BIN" || die "publish failed"
|
|
|
|
# ── 6. Verify by an INDEPENDENT download ────────────────────────────────────────────────────────
|
|
# The publish step's own success is not proof: it reports on its own write. What matters is that a
|
|
# box can now GET the bytes and that they are the bytes that were built. This is the same
|
|
# presence-is-not-success rule the project earned twice — a step that says "done" and a fetch that
|
|
# returns the right sha are different claims.
|
|
log "verifying by independent download …"
|
|
DL="$(mktemp -t felhom-agent-dl-XXXXXX)"
|
|
trap 'rm -f "$BIN" "$DL"' EXIT
|
|
curl -fsS -o "$DL" "$GITEA_BASE/api/packages/$GITEA_OWNER/generic/felhom-agent/$VERSION/felhom-agent" \
|
|
|| die "round-trip GET failed — the version is NOT installable"
|
|
DL_SHA="$(sha256sum "$DL" | awk '{print $1}')"
|
|
[[ "$DL_SHA" == "$BUILT_SHA" ]] \
|
|
|| die "published sha $DL_SHA != built sha $BUILT_SHA — the artifact is not what was built"
|
|
|
|
# The tag must also serve the configs the installer will fetch from it.
|
|
cfg_code="$(curl -fsS -o /dev/null -w '%{http_code}' \
|
|
"$GITEA_BASE/$GITEA_OWNER/felhom-agent/raw/tag/$TAG/configs/felhom-agent.service" 2>/dev/null || true)"
|
|
[[ "$cfg_code" == "200" ]] \
|
|
|| die "tag $TAG does not serve configs/felhom-agent.service (HTTP $cfg_code) — a box would 404 mid-install"
|
|
|
|
cat <<EOF
|
|
|
|
RELEASED — and installable, verified by download, not by this script's own say-so.
|
|
|
|
version : $VERSION
|
|
tag : $TAG
|
|
sha256 : $BUILT_SHA
|
|
|
|
NOT VOUCHED. Vouching is what points machines at this version and stays your deliberate act:
|
|
hub operator UI → Configs → Day-0 artifacts. Until then boxes keep installing the previous one.
|
|
EOF
|