The Versions "Projekt" card linked config('clusev.repository') — the per-install
update source, which on the dev box is the private Gitea. A staging/stable install
must never surface a private source. Now only the dev/release-control box shows its
actual source; every other install links the public open-core repo only, built from
the public slug with a canonical fallback (a malformed or full-URL slug can never
produce a private/garbage link). The update SOURCE (ReleaseChecker) is unchanged —
this governs only the displayed link.
Codex review: hardened the slug to reject non-owner/repo values; the release_controls
gate is the dev-box invariant (it also gates the dev-only /release page, and a staging
box's CLUSEV_REPOSITORY is the public repo anyway).
Verified: 432 tests green (incl. dev / non-dev / malformed-slug cases), pint clean,
/versions loads 200 with zero console errors.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The self-update progress screen reset its elapsed timer to 0 and dropped the
phase checklist back to step one on every browser refresh — and the long image
build made it look like the update had restarted. Root causes: the timer and
phase state lived only in page JS (Date.now() at page load), and a
30s-before-page-load staleness guard rejected the real phase feed whenever a
stage had run longer than 30s (which the image build always does).
- DeploymentService: requestUpdate() now also writes a best-effort
run/update-started.json {at,from}. New updateStartedAt()/updatePhase() expose
the server-side start + current macro-stage; updatePhase() requires a positive
timestamp so a corrupt or stale 'done' can never drive a premature finish.
clearUpdateRequest() also removes the start marker.
- routes: /update-status.json delegates to updatePhase(); /update-progress
passes startedAt/initialPhase/hasFeed. A stage is trusted only when tied to
THIS update's start (startedAt) and written at/after it — a leftover stage from
a previous run can no longer resume or trigger a premature completion.
- update-progress.blade.php: the elapsed timer is anchored to the server-side
start (counts the real elapsed across a refresh); the checklist resumes at the
live stage; the live feed is authoritative (corrects any time-heuristic
overshoot) and the heuristic stays off once a real stage is known.
Verified: 28 UpdateProgressTest cases + full suite (426) green, pint clean, and a
headless browser load+reload mid-update keeps the timer counting and the phase in
place with zero console errors. Codex review: two premature-completion edge cases
found and fixed (timestamp validation + start-anchored freshness).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The changelog mixed user-facing changes with dev-only maintainer tooling
(the release pipeline, CI fixes), reading as overly detailed/unprofessional.
Establish the convention up front — user-facing changes only; internal work
lives in git history — and apply it:
- Collapse the dev-only release-tooling versions 0.9.59–0.9.68 into a single
terse 0.9.68 "Intern" entry (kept parseable so the Versions page still lists
the running version).
- Trim 0.9.58 to its user-facing parts (GitHub update-check, public address).
- Reduce 0.9.57 to the user-facing release-channels note.
Verified the Versions-page changelog parser still yields clean nodes
(0.9.68 -> Intern, 0.9.58, 0.9.57, ...) with no leaked translation keys.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- PromotionService::publicTags() is called on every Release render incl. the 5s
page poll; the anonymous GitHub tag API is rate-limited (60/h per IP). Cache it
120s so the panel never gets rate-limited to empty. (Test flushes cache in setUp.)
- yank.yml: replace the loose shell-glob tag guard with an anchored grep regex so
a garbage/space/metachar tag dispatched straight from the GitHub UI is rejected
before reaching git push --delete (the panel already validates tag ∈ publicTags).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
'Staging' is not a single server: any beta-channel server pulls a release itself,
so a per-server 'deployed' step in the dev dashboard was arbitrary + contradicted
that model. The pipeline now tracks only the global build facts of a beta —
Tag (Gitea) → Mirror (GitHub-private) → CI. Removes PipelineStatus::testStep, the
test-server Setting/input/saveTestServer, and the now-orphaned lang keys.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Full-page tests render @vite layouts; without public/build/manifest.json (gitignored,
absent on a fresh CI checkout) they 500. Move npm ci + npm run build ahead of the
test step so the manifest exists. Fixes the red CI on every v* tag.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- PipelineStatusTest: the private mirror slug 'boksbc/clusev-staging' was hardcoded
in this tracked test (tests/ ships to the public repo) — exactly the leak the
design forbids. Neutralise to an example slug 'acme/staging'. Add the missing
HTTP-error degrade tests (GitHub 5xx, GitHub throwing, test-server unreachable →
'unknown', never throws) — the core robustness guarantee was untested.
- Live rail: render the per-step description (the built $sub array was dead code)
and colour the live state by status (online/warning/offline).
- saveTestServer: refresh() the pipeline cache so a changed test-server URL is
reflected immediately, not after the 15s TTL.
- .env.example: document the dev-only release/pipeline keys (commented, no values).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The GitHub-private staging repo is a full push-mirror of Gitea, so export-ignore
(which only protects the public repo via git archive) does not strip these from
the mirror. Untrack the internal/AI-collaboration docs — CLAUDE.md, rules.md,
handoff.md, kickoff-prompt.md and docs/ (specs, plans, runbooks, handoffs) — and
gitignore them so they stay on the dev box but never reach Gitea, the mirror, or
any shipped image. The public repo was already clean via export-ignore.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Replace the bland version + four plain buttons with a composed layout: a current-
version hero (mono version, channel + source), tactile bump tiles showing the
current → target transition with a Beta-tag badge (continue-beta highlighted
accent), styled running/last-pushed states, and a vertical pipeline rail
(Gitea → GitHub-privat → CI → Staging). Tokens-only, responsive 375/768/1280,
German, no emoji; the R5 confirm flow + component contract are unchanged.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- serve-request.test.sh: the push-fail case pointed origin at a dead https remote,
so _precheck's fetch failed first and the rollback path in _do_stage was never
reached (assertions passed trivially). Use a fetch-OK / push-rejecting remote
(pre-receive exit 1) so the commit+tag are created and _rollback genuinely runs;
also assert the tree is restored clean.
- clusev-release.sh: |\| true on the version extraction (defensive under pipefail).
- install.sh: make the release-unit install best-effort (if/then/warn) like the WG
block so a systemctl failure cannot abort the installer.
- .gitignore: ignore runtime run/ contents, keep the dir via run/.gitkeep.
- docker/release/README.md: dev-watcher runbook (the manual end-to-end gate).
- ReleaseBridgeTest: assert a rejected target writes no request file.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The shipped Release UI references release.confirm_title, confirm_body,
confirm_action and staged_label, but neither lang/de/release.php nor
lang/en/release.php defined them, so the R5 confirm modal and the staged
success banner leaked raw keys (R9/R17). Add the four keys to both locales.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Register the /release route only when config('clusev.release_controls') is
true — the first of the spec's three gating layers (route + component mount +
sidebar). The route file is re-included per request when uncached, so the flag
is honoured; mount() abort_unless(...) still covers the cached-route case.
The gating test re-evaluates routes/web.php after toggling the flag (the file
is read once at boot, so a runtime config()->set() alone cannot register the
route), mirroring a fresh request.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Adds the dev-only Release page: a flag-gated route (config('clusev.release_controls'),
enforced by the component mount() guard) and a flag-gated sidebar item, plus the
Release Livewire component. Deploy-to-Staging is irreversible, so it goes through the
shared R5 wire-elements/modal confirm — confirmDeployStaging opens the modal and
applyDeployStaging consumes the sealed ConfirmToken and runs the deployStaging executor
(the host bridge writes the request, betaN is computed host-side). Registers the
releaseStaged confirm action in the ConfirmToken allow-list.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
First sub-project of Phase B: a dev-only Release page with a Deploy-to-Staging
button that cuts a beta (bump config version, commit, tag vX.Y.Z-betaN, push to
Gitea) via a host release bridge — same isolation as the WireGuard bridge, so the
container never holds git/token credentials. Gated behind CLUSEV_RELEASE_CONTROLS.
B2 (public promotion via GitHub API + vault token) is a separate later spec.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The earlier leak guard grepped the published tree for private URL strings, but
promote.sh / promote.test.sh contain those very strings (patterns + fixture), so
it tripped on its own source and would also ship them. Switch the guard to
internal-doc presence (CLAUDE.md/rules.md/handoff.md/kickoff-prompt.md/docs) — a
reliable pre-fix-tree signal that embeds no private identifier. Genericise the
dev VM IP in .env.example (APP_URL/REVERB_HOST) to localhost.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The actual file changes for the leak-safety hardening described in a94efb0
(that commit only captured the art/ asset move):
- promote.sh: leak guard refusing to publish a tree carrying a private identifier
(git.bave.dev / boban/clusev / clusev-staging) or an internal doc; negative test.
- .gitattributes: export-ignore kickoff-prompt.md (leaked the VM IP).
- README: reference the moved art/ SVGs (docs/ is export-ignored).
- .env.example: comment out CLUSEV_REPOSITORY so the public-repo config default
applies (a present-but-empty value would disable the update check).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- ReleaseChecker: mix a short hash of clusev.repository into the tag + changelog
cache keys so a cached value can never be reused across a host/repo change (the
promotion pipeline re-points CLUSEV_REPOSITORY between the private mirror and the
public repo).
- Strengthen the GitHub User-Agent test to assert the value (Clusev-Panel), not
mere presence — Guzzle always sets some UA, so the old check was a tautology.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- promote.sh: add a leak guard that refuses to publish a tree still carrying a
private identifier (git.bave.dev / boban/clusev / clusev-staging) or an internal
doc (CLAUDE.md, rules.md, handoff.md, kickoff-prompt.md, docs/). This makes
promotion safe even for a tag cut BEFORE the export-ignore fixes — such a tree is
refused instead of leaking the dev/staging repos. Negative test added.
- .gitattributes: export-ignore kickoff-prompt.md (internal bootstrap doc; leaked
the VM IP + infra topology).
- README: move the banner/architecture SVGs out of the export-ignored docs/ into
art/ so the public README images are actually published.
- .env.example: comment out CLUSEV_REPOSITORY — a present-but-empty value defeats
the config default (env() returns ''), which would silently disable the public
update check. Left unset, the public-repo default applies.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The release-pipeline + one-time-wiring section named the private staging runner
label and described internal setup — not public-facing. Move it to
docs/release-pipeline.md (export-ignore, never published) so the public README
stays clean and no private repo name leaks via the published tree.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Default update source, README (badge/clone/home), the console + source-view
branding, and the CHANGELOG compare links now reference the public repo. Private
URLs remain only in the gitignored .env (install.sh derives CLUSEV_REPOSITORY
from the clone origin). Bump 0.9.58.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
promote.sh copies a release tree with git archive, which honours export-ignore.
Mark CLAUDE.md, rules.md, handoff.md and docs/ as export-ignore so internal dev
notes (and the Gitea URL they contain) never reach the public repo; drop the
stock CHANGELOG.md export-ignore so the public Versions page can fetch it. Extend
promote.test.sh to assert internal files are excluded and CHANGELOG.md is kept.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
ReleaseChecker + the changelog fetch were hardwired to the Gitea API path, so a
GitHub-hosted public repo would never resolve a release. Centralise the repo-URL
parsing and branch by host: GitHub uses api.github.com for tags and
raw.githubusercontent.com for CHANGELOG.md (with the User-Agent GitHub requires);
Gitea keeps its /api/v1 paths. The changelog fetch moves into ReleaseChecker so
all host logic lives in one place.
Make the update-check tests hermetic: they now set clusev.repository themselves
(neutral host) instead of depending on the ambient .env — so they pass in CI with
an empty .env.example — and add GitHub-host coverage for tags + changelog.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>