installer v1.23.0 — publishing becomes an act, not a side-effect (R-110, R-183)
gates / gates (push) Successful in 8s
gates / gates (push) Successful in 8s
Two channels moved off main in the same change, because either one left behind makes the other cosmetic. Channel 1 — the served script. webpage.yaml git-synced /scripts/ from --branch=main every 30s and nginx served that tree, so pushing this file WAS publishing it: within half a minute it was what every new machine downloaded and ran as root, with no staging and no rollback but another push. The sync is now SPLIT: the website keeps tracking main at the same cadence (a copy edit must never need a release) and /scripts/ tracks the tag installer-v<SCRIPT_VERSION>. PROVEN before the manifest was touched: git-sync v4.4.0 follows a tag AND notices a MOVED one — measured on a throwaway sync against this repo, "update required ... local:<old> remote:<new>" -> "updated successfully", within one period. The moved-tag half is what the publish model rests on. Channel 2 — the sixteen files fetched at run time. fetch_raw pulled from $AGENT_REPO/raw/branch/main; it now pulls raw/tag/v$ART_AGENT_VER. That is a correctness fix, not only a channel one (R-183): a fresh install fetched the vouched agent BINARY while taking its unit file, sudoers and guarded wrappers from whatever main held. Two refs, one install, nothing compared them. Their correct ref was never SCRIPT_VERSION — they do not live in this repo. No fallback to a branch: a vouched version whose tag is missing fails loudly rather than quietly serving main. Channel 3 — the URL — needed no change, recorded rather than left silent: https://felhom.eu/scripts/felhom-host-install.sh never carried a ref, so both producers follow the tag with no edit. No hub change, no hub version bump. Gate 6 in hostinstall_gates.py pins all three structurally with no network, so it stays in --fast and runs in CI. It deliberately does NOT assert "a tag exists for the current SCRIPT_VERSION": that would go red on the very push that bumps the version, before publishing — and publishing being separate is the ruling.
This commit is contained in:
@@ -1,3 +1,51 @@
|
||||
## v1.23.0 — the installer is published, not pushed (2026-08-03, R-110 + R-183)
|
||||
|
||||
**Two channels moved off `main` in the same change, because either one left behind makes the other
|
||||
cosmetic.**
|
||||
|
||||
**Channel 1 — the served script.** `manifests/webpage.yaml` git-synced `/scripts/` from
|
||||
`--branch=main` on a 30 s period and nginx served that working tree, so **pushing this file WAS
|
||||
publishing it**: within half a minute it was what every new machine downloaded and ran as root, with
|
||||
no staging and no rollback but another push. The sync is now **split in two**: the website keeps
|
||||
tracking `main` at the same cadence (a copy edit must never need a release), and `/scripts/` tracks
|
||||
the tag **`installer-v<SCRIPT_VERSION>`**. Publishing is moving that tag; rolling back is moving it
|
||||
back.
|
||||
|
||||
**PROVEN, not assumed:** git-sync v4.4.0 follows a tag *and* notices a **moved** one — measured on a
|
||||
throwaway sync against this repo, `update required … local:<old> remote:<new>` → `updated
|
||||
successfully`, within one period (~20 s). The moved-tag half is what the whole publish model rests
|
||||
on, so it was measured before the manifest was touched.
|
||||
|
||||
**Channel 2 — the sixteen files the installer fetches while it runs.** `fetch_raw` pulled from
|
||||
`$AGENT_REPO/raw/branch/main`. It now pulls from **`raw/tag/v$ART_AGENT_VER`** — the agent version the
|
||||
hub has vouched and whose binary sha this script already verifies.
|
||||
|
||||
**That is a correctness fix, not only a publish-channel one (→ R-183).** These are the AGENT's
|
||||
configs — its systemd unit, its sudoers, its guarded wrappers — and a fresh install was fetching the
|
||||
**vouched binary** while taking its configs from **whatever `main` held**. Two refs, one install, and
|
||||
nothing compared them. The right ref for them was never this script's `SCRIPT_VERSION`: they do not
|
||||
live in this repo and have no relationship to its version line.
|
||||
|
||||
**No fallback to a branch.** A vouched version whose tag is missing fails loudly rather than quietly
|
||||
serving `main` — a silent fallback is the appearance of control with none of it. `felhom-agent`
|
||||
carries `v<version>` tags from now on, `release-agent.sh` creates them, and `agent_gates.py` fails if
|
||||
the vouched version is not downloadable.
|
||||
|
||||
**Channel 3 — the URL — needed no change, and that is worth recording rather than leaving as a
|
||||
silence.** `https://felhom.eu/scripts/felhom-host-install.sh` never carried a ref: the ref lives in
|
||||
the manifest. So both producers of that URL (`scripts/iso/felhom-bootstrap.sh`, the hub's day-0
|
||||
command) follow the tag with no edit — **and no hub change, so no hub version bump.**
|
||||
|
||||
**Gate 6 in `hostinstall_gates.py`** pins all three structurally, with no network so it stays in
|
||||
`--fast` and runs in CI on every push: no `raw/branch/` ref anywhere in the installer; `fetch_raw`
|
||||
still pins to `$ART_AGENT_VER`; the manifest still syncs `/scripts/` from an `installer-v…` tag and
|
||||
the website still from `main`.
|
||||
|
||||
**It deliberately does NOT assert "a tag exists for the current SCRIPT_VERSION".** That gate would go
|
||||
red on the very push that bumps the version, before publishing — and publishing being a separate
|
||||
deliberate act is the entire ruling. A gate that fails on the normal path is one people learn to
|
||||
ignore.
|
||||
|
||||
## docs — v1.22.0 exercised end to end on two real reinstalls (2026-08-03, R-178) — **no script change**
|
||||
|
||||
**Nothing shipped.** `felhom-host-install.sh` stayed at **v1.22.0**; the published copy at
|
||||
|
||||
@@ -184,7 +184,7 @@
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
SCRIPT_VERSION="1.22.0" # the SINGLE version source (F-1): -h and the run banners follow it.
|
||||
SCRIPT_VERSION="1.23.0" # the SINGLE version source (F-1): -h and the run banners follow it.
|
||||
# The hub used to carry a copy for its Setup tab; R-94 DELETED it
|
||||
# (2026-08-02) because the hub cannot know which version a box runs —
|
||||
# the Setup command fetches this script at run time. scripts/
|
||||
@@ -492,12 +492,31 @@ fetch_verify() {
|
||||
# exists, else anonymous). These are non-executable text (not the integrity-checked binary); the
|
||||
# sudoers is `visudo -cf`-validated before install, which catches corruption/tampering that would
|
||||
# matter. $1=repo-path $2=dest
|
||||
#
|
||||
# R-110 / R-183: PINNED TO THE AGENT VERSION BEING INSTALLED, never to a branch.
|
||||
#
|
||||
# These sixteen files are the AGENT's configs — its systemd unit, its sudoers, its guarded wrappers —
|
||||
# so the ref that is correct for them is the agent version this run is installing, which the hub has
|
||||
# vouched and whose binary sha this script verifies. It is NOT the installer's own SCRIPT_VERSION:
|
||||
# these files do not live in the installer's repo and have no relationship to its version line.
|
||||
#
|
||||
# Before this they came from `raw/branch/main`, which is a REAL SKEW and not only a publish-channel
|
||||
# defect (R-183): a fresh install fetched the vouched agent BINARY while taking its unit file and
|
||||
# sudoers from whatever `main` happened to hold — two refs, one install, and nothing compared them.
|
||||
#
|
||||
# NO FALLBACK TO A BRANCH. A vouched version whose tag is missing must fail loudly here rather than
|
||||
# quietly serving `main`, because a silent fallback is exactly the "appearance of control with none of
|
||||
# it" this change exists to remove. `agent_gates.py`'s published-version gate keeps the tag and the
|
||||
# vouched version in step, so this die is a backstop and not the primary control.
|
||||
fetch_raw() {
|
||||
local path="$1" dest="$2"
|
||||
# Late steps (mgmt-watchdog, OOB) can run without step 5 having resolved the manifest.
|
||||
[[ -n "$ART_AGENT_VER" ]] || resolve_artifacts
|
||||
[[ -n "$ART_AGENT_VER" ]] || die "cannot pin $path: no agent version resolved from the hub manifest"
|
||||
local -a _auth; _git_auth_args _auth
|
||||
curl -fsS "${_auth[@]}" -o "$dest" \
|
||||
"$GITEA_BASE/$GITEA_OWNER/$AGENT_REPO/raw/branch/main/$path" \
|
||||
|| die "raw fetch failed: $path"
|
||||
"$GITEA_BASE/$GITEA_OWNER/$AGENT_REPO/raw/tag/v$ART_AGENT_VER/$path" \
|
||||
|| die "raw fetch failed: $path (agent tag v$ART_AGENT_VER — is that version tagged in $AGENT_REPO?)"
|
||||
[[ -s "$dest" ]] || die "raw fetch empty: $path"
|
||||
}
|
||||
|
||||
|
||||
@@ -146,6 +146,62 @@ if re.search(r'^PVE_STORAGES=\([^)]*felhom-pbs[^)]*\)', src, re.M):
|
||||
else:
|
||||
fail("felhom-pbs missing from the default PVE_STORAGES — narrowing it 403s the PBS-DR apply-bridge")
|
||||
|
||||
# ── 6. the publish channel is pinned, not floating (R-110 / R-183) ──────────────
|
||||
#
|
||||
# WHAT THIS ASSERTS, AND WHAT IT DELIBERATELY DOES NOT.
|
||||
#
|
||||
# It does NOT assert "a tag exists for the current SCRIPT_VERSION". That gate would fail the very
|
||||
# push that bumps SCRIPT_VERSION, before publishing has happened — and publishing being a SEPARATE
|
||||
# deliberate act is the whole point of R-110's ruling. A gate that goes red on the normal path is a
|
||||
# gate people learn to ignore, which is the reasoning the task's own §8.4 applies to the agent-side
|
||||
# gate; it applies here identically. "Is the vouched version actually downloadable" is a real
|
||||
# invariant and it lives where a missing artifact genuinely breaks day-0 — `felhom-agent`'s
|
||||
# `agent_gates.py`, which has the network access to answer it.
|
||||
#
|
||||
# What it asserts instead are the two STRUCTURAL regressions that would silently return the
|
||||
# installer to a floating channel, both answerable by reading files (no network, so this stays in
|
||||
# `--fast` and therefore runs in CI on every push):
|
||||
#
|
||||
# 6a. no `raw/branch/` ref anywhere in the installer — one of the sixteen agent-config fetches
|
||||
# slipping back to `main` is exactly how a channel stays floating unnoticed, and it is
|
||||
# invisible in a diff that touches one line.
|
||||
# 6b. `fetch_raw` still pins to the resolved agent version — the positive form, so the mechanism
|
||||
# cannot be quietly deleted rather than regressed.
|
||||
# 6c. the website manifest still syncs `/scripts/` from a TAG ref and the website from `main` —
|
||||
# the split is the deploy-side half of the same channel, and reverting it is one word.
|
||||
branch_refs = [l for l in lines if "raw/branch/" in l and not l.lstrip().startswith("#")]
|
||||
if branch_refs:
|
||||
fail("installer still fetches from a BRANCH ref — the run-time channel is floating again "
|
||||
"(R-110/R-183). Offending line(s): %s" % "; ".join(l.strip()[:90] for l in branch_refs))
|
||||
else:
|
||||
ok("no raw/branch/ ref in the installer — every run-time fetch is pinned")
|
||||
|
||||
if re.search(r'raw/tag/v\$ART_AGENT_VER/', src):
|
||||
ok("fetch_raw pins the agent configs to the vouched agent version")
|
||||
else:
|
||||
fail("fetch_raw no longer pins to $ART_AGENT_VER — the agent's configs and its binary can "
|
||||
"again come from different refs in one install (R-183)")
|
||||
|
||||
WEBPAGE = os.path.join(ROOT, "manifests", "webpage.yaml")
|
||||
try:
|
||||
with io.open(WEBPAGE, "r", encoding="utf-8") as f:
|
||||
wp = f.read()
|
||||
except IOError as e:
|
||||
fail("cannot read manifests/webpage.yaml to check the publish channel: %s" % e)
|
||||
wp = None
|
||||
if wp is not None:
|
||||
# The scripts sync must name a tag ref; the website sync must still track main.
|
||||
if re.search(r'--ref=installer-v', wp):
|
||||
ok("manifest: /scripts/ syncs from an installer tag")
|
||||
else:
|
||||
fail("manifests/webpage.yaml has no `--ref=installer-v…` sync — /scripts/ is not served "
|
||||
"from a tag, so pushing the installer publishes it again (R-110)")
|
||||
if re.search(r'--(branch|ref)=main', wp):
|
||||
ok("manifest: the website still tracks main (a copy edit must not need a release)")
|
||||
else:
|
||||
fail("manifests/webpage.yaml no longer tracks main for the website — pinning the SITE to "
|
||||
"the installer tag turns every copy edit into a release")
|
||||
|
||||
print()
|
||||
if fails:
|
||||
print("hostinstall gates: %d FAILURE(S)" % len(fails))
|
||||
|
||||
Reference in New Issue
Block a user