hub v0.29.0: Day-0 artifact manifest — version dropdowns + auto-derived sha

Operator picks a version from a Gitea-populated dropdown; the hub reads that
version's sha256 from Gitea itself (files-metadata API, no artifact download) and
vouches it — no hand-copied checksums. New internal/gitea read-only client
(ListVersions + FileSHA256, unit-tested). Configuration UI: version <select>s +
read-only sha display; handleSetArtifacts derives the sha authoritatively and
refuses the save on a Gitea lookup failure. Degrades to manual text entry without
registry creds. go build/vet/test clean.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-07-01 09:02:13 +02:00
parent ce26c9d646
commit 079a2cdd08
7 changed files with 375 additions and 12 deletions
+36 -7
View File
@@ -43,7 +43,7 @@
<div class="flash flash-error">Invalid artifact version — use X.Y.Z (or blank to clear).</div>
{{end}}
{{if eq .Flash "artifact_sha_invalid"}}
<div class="flash flash-error">Invalid sha256 — use 64 hex chars (or blank to clear).</div>
<div class="flash flash-error">Couldn't set the checksum — the Gitea sha lookup failed (version missing / Gitea unreachable) or the manually-entered sha is invalid. Manifest unchanged.</div>
{{end}}
<!-- Phase 2 managed updates: global controller-version floor (moved from the Customers page —
@@ -71,20 +71,49 @@
<p class="text-muted" style="margin: 0 0 0.75rem; font-size: 0.85em;">
The current agent binary + golden archive the host-bootstrap script fetches from Gitea and
verifies (sha256) before installing. The hub vouches for these checksums (a different trust
root than Gitea). Paste the version + sha256 printed by <code>publish-agent.sh</code> /
<code>build-golden.sh</code>. Blank a field to clear it.
root than Gitea). Pick a version — the sha256 is read from Gitea automatically (no manual
copy). Choose <em>— none —</em> to clear an artifact.
</p>
<form method="POST" action="/configuration/artifacts" style="display: grid; grid-template-columns: auto 8em 1fr; gap: 0.5rem; align-items: center; max-width: 56em;">
<form method="POST" action="/configuration/artifacts" style="display: grid; grid-template-columns: auto 12em 1fr; gap: 0.5rem; align-items: center; max-width: 56em;">
{{.CSRFField}}
<label style="font-size: 0.9em; color: #cbd5e1;">Agent</label>
<input type="text" name="agent_version" value="{{.Artifacts.AgentVersion}}" placeholder="0.43.0" style="padding: 0.3em 0.5em;">
<input type="text" name="agent_sha256" value="{{.Artifacts.AgentSHA256}}" placeholder="64-hex sha256 (blank = none)" style="padding: 0.3em 0.5em; font-family: monospace;">
{{if .AgentChoices}}
<select name="agent_version" id="agent_version" onchange="syncArtifactSha('agent')" style="padding: 0.3em 0.5em;">
<option value="" data-sha="">— none —</option>
{{range .AgentChoices}}
<option value="{{.Version}}" data-sha="{{.SHA256}}" {{if eq .Version $.Artifacts.AgentVersion}}selected{{end}}>{{.Version}}</option>
{{end}}
</select>
{{else}}
<input type="text" name="agent_version" value="{{.Artifacts.AgentVersion}}" placeholder="0.52.0" style="padding: 0.3em 0.5em;">
{{end}}
<input type="text" name="agent_sha256" id="agent_sha256" value="{{.Artifacts.AgentSHA256}}" {{if .AgentChoices}}readonly{{end}} placeholder="64-hex sha256 (blank = none)" style="padding: 0.3em 0.5em; font-family: monospace; {{if .AgentChoices}}opacity: 0.7;{{end}}">
<label style="font-size: 0.9em; color: #cbd5e1;">Golden</label>
{{if .GoldenChoices}}
<select name="golden_version" id="golden_version" onchange="syncArtifactSha('golden')" style="padding: 0.3em 0.5em;">
<option value="" data-sha="">— none —</option>
{{range .GoldenChoices}}
<option value="{{.Version}}" data-sha="{{.SHA256}}" {{if eq .Version $.Artifacts.GoldenVersion}}selected{{end}}>{{.Version}}</option>
{{end}}
</select>
{{else}}
<input type="text" name="golden_version" value="{{.Artifacts.GoldenVersion}}" placeholder="0.85.1" style="padding: 0.3em 0.5em;">
<input type="text" name="golden_sha256" value="{{.Artifacts.GoldenSHA256}}" placeholder="64-hex sha256 (blank = none)" style="padding: 0.3em 0.5em; font-family: monospace;">
{{end}}
<input type="text" name="golden_sha256" id="golden_sha256" value="{{.Artifacts.GoldenSHA256}}" {{if .GoldenChoices}}readonly{{end}} placeholder="64-hex sha256 (blank = none)" style="padding: 0.3em 0.5em; font-family: monospace; {{if .GoldenChoices}}opacity: 0.7;{{end}}">
<span></span><span></span>
<button class="btn btn-sm" type="submit" style="justify-self: start;">Save artifact manifest</button>
</form>
<script>
// When a version is picked, mirror that option's Gitea-resolved sha256 into the read-only
// display field. The hub re-derives the sha authoritatively on save regardless of this value.
function syncArtifactSha(kind) {
var sel = document.getElementById(kind + '_version');
var sha = document.getElementById(kind + '_sha256');
if (!sel || !sha) return;
var opt = sel.options[sel.selectedIndex];
sha.value = (opt && opt.getAttribute('data-sha')) || '';
}
</script>
</section>
<!-- Assets section -->