refactor(release): single stable channel — cut internal release candidates (-rc), remove all beta wording

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
feat/v1-foundation
boban 2026-07-05 13:13:09 +02:00
parent e6f6e7f8a4
commit f804e25a66
24 changed files with 153 additions and 147 deletions

View File

@ -105,7 +105,7 @@ jobs:
# Gitea-free code tree; promote.sh's leak-guard is the last line of defence.
promote:
needs: [test, images]
# Stable = a clean vX.Y.Z with NO pre-release suffix. Excludes -rc AND any accidental -beta/-alpha.
# Stable = a clean vX.Y.Z with NO pre-release suffix. Excludes -rc AND any accidental pre-release suffix.
if: ${{ success() && startsWith(github.ref_name, 'v') && !contains(github.ref_name, '-') }}
runs-on: ubuntu-latest
environment: production

View File

@ -3,7 +3,7 @@ on:
workflow_dispatch:
inputs:
tag:
description: 'Tag to retract from the public repo (e.g. v0.10.0 or v0.10.0-beta1)'
description: 'Tag to retract from the public repo (e.g. v0.10.0)'
required: true
permissions:
@ -18,7 +18,7 @@ jobs:
env:
TAG: ${{ inputs.tag }}
run: |
echo "$TAG" | grep -qE '^v[0-9]+\.[0-9]+\.[0-9]+(-beta[0-9]+)?$' \
echo "$TAG" | grep -qE '^v[0-9]+\.[0-9]+\.[0-9]+$' \
|| { echo "refusing to yank an unrecognised tag: $TAG" >&2; exit 1; }
- name: Checkout public repo
uses: actions/checkout@v5

View File

@ -7,7 +7,7 @@ Alle nennenswerten Änderungen an Clusev. Format nach
**Release-Prozess:** Feature-Commits sammeln sich auf dem Branch. Ist ein Stand
reif, wird er nach `main` gemerged, als `vX.Y.Z` getaggt und hier unter einer
eigenen Versionsüberschrift dokumentiert. Nutzer beziehen ausschließlich
getaggte Releases (Kanal `stable`, optional `beta`) — niemals Entwicklungs-Builds.
getaggte Releases (Kanal `stable`) — niemals Entwicklungs-Builds.
**Was hier steht:** nur, was Nutzer der Installation merken — knapp, ein Eintrag
pro Änderung, gruppiert nach _Hinzugefügt / Geändert / Behoben / Sicherheit_. Rein
@ -18,7 +18,13 @@ sondern in die git-Historie.
_Keine offenen Änderungen — der nächste Stand wird hier gesammelt und als `vX.Y.Z` getaggt._
## [0.11.0-beta1] - 2026-06-26
## [0.11.0] - 2026-07-05
### Geändert
- **Ein einziger stabiler Release-Kanal.** Nutzer erhalten ausschließlich finalisierte `vX.Y.Z`-Releases;
die interne Release-Steuerung schneidet jetzt Release Candidates statt Vorab-Kanälen.
## [0.11.0-rc1] - 2026-06-26
### Hinzugefügt
- **Web-Terminal.** Vollwertiges SSH-Terminal direkt im Browser — eines pro Server und eines für den
@ -47,7 +53,7 @@ _Keine offenen Änderungen — der nächste Stand wird hier gesammelt und als `v
- **Proxy-Vertrauen gehärtet:** nur noch private Netze werden vertraut, damit ein gefälschter
`X-Forwarded-For` die IP-basierten Brute-Force-Limits nicht mehr umgehen kann.
## [0.10.0-beta1] - 2026-06-25
## [0.10.0-rc1] - 2026-06-25
### Hinzugefügt
- **Verlaufs-Graf auf der Server-Detailseite.** CPU, RAM und Disk über Zeit — Zeitraum wählbar
@ -64,7 +70,7 @@ _Keine offenen Änderungen — der nächste Stand wird hier gesammelt und als `v
## [0.9.68] - 2026-06-23
### Intern
- **Release-Tooling im Dev-Dashboard + CI-Pipeline.** Beta schneiden, Live-Pipeline-Status,
- **Release-Tooling im Dev-Dashboard + CI-Pipeline.** Release Candidate schneiden, Live-Pipeline-Status,
Veröffentlichen / Promote-to-Stable / Yank — für Nutzer der Installation nicht sichtbar.
Details in der git-Historie. (Sammelt die internen Versionen 0.9.590.9.68.)
@ -81,8 +87,8 @@ _Keine offenen Änderungen — der nächste Stand wird hier gesammelt und als `v
## [0.9.57] - 2026-06-22
### Hinzugefügt
- **Release-Kanäle `stable` und `beta`.** Nutzer beziehen stabile Releases (`vX.Y.Z`); wer früher
testen will, wählt den Beta-Kanal (`vX.Y.Z-betaN`) — niemals Entwicklungs-Builds.
- **Stabiler Release-Kanal `stable`.** Nutzer beziehen ausschließlich stabile Releases (`vX.Y.Z`) —
niemals Entwicklungs-Builds.
## [0.9.56] - 2026-06-22
@ -725,7 +731,7 @@ _Keine offenen Änderungen — der nächste Stand wird hier gesammelt und als `v
Version veröffentlicht war. Der neueste Tag im Kanal wird jetzt über die **öffentliche, lesende
Tag-API des Git-Hosts** abgefragt (anonym — kein Token verlässt das Panel), mit lokalem `.git` nur
noch als Dev-Fallback; das Ergebnis ist gecacht, sodass ein Seitenaufruf nie eine Netzwerkanfrage
auslöst (nur der Knopf selbst). Kanal-Logik: `stable` bietet nur `vX.Y.Z`, `beta` auch Vorab-Tags.
auslöst (nur der Knopf selbst). Kanal-Logik: `stable` bietet ausschließlich `vX.Y.Z` an.
Schlägt die Abfrage fehl (offline o. Ä.), degradiert sie still statt zu blockieren.
## [0.9.6] - 2026-06-19
@ -1295,7 +1301,7 @@ Erste geschnittene Version — die komplette v1-Basis inklusive Server- und Pane
- Panel-Selbst-Härtung: Security-Header + umgebungsbewusste CSP, 2FA-Brute-Force-
Drossel, Session-Cookie-Härtung.
- Release-/Versionierungsmodell: echte semantische Versionen + Git-Tags, Changelog
pro Version, Kanäle `stable`/`beta` (kein nutzer-seitiges `dev`).
pro Version, Kanal `stable` (kein nutzer-seitiges `dev`).
[Unreleased]: https://github.com/clusev/clusev/compare/v0.2.1...HEAD
[0.2.1]: https://github.com/clusev/clusev/compare/v0.2.0...v0.2.1

View File

@ -16,8 +16,8 @@ use Livewire\Attributes\On;
use Livewire\Component;
/**
* Dev-only Release control. "Deploy to Staging" cuts a beta of an allowed target version via the
* host release bridge (bump commit tag push to Gitea). The page is reachable only when
* Dev-only Release control. "Deploy to Staging" cuts a release candidate of an allowed target
* version via the host release bridge (bump commit tag push to Gitea). The page is reachable only when
* config('clusev.release_controls') is true; mount() re-guards as defence-in-depth.
*
* Deploy-to-Staging is irreversible (it pushes a tag the host CI acts on), so it is gated behind the
@ -37,7 +37,7 @@ class Index extends Component
/** The target X.Y.Z being staged (for display while it runs). */
public ?string $pendingTarget = null;
/** The tag of the last successfully-pushed staging beta (shown until the next deploy). */
/** The tag of the last successfully-pushed staging release candidate (shown until the next deploy). */
public ?string $lastTag = null;
public function mount(): void
@ -46,7 +46,7 @@ class Index extends Component
abort_unless(auth()->user()?->can('manage-panel'), 403);
}
/** Opens the wire-elements/modal confirm dialog (R5) before cutting a staging beta of $target. */
/** Opens the wire-elements/modal confirm dialog (R5) before cutting a staging release candidate of $target. */
public function confirmDeployStaging(string $target, ReleasePlanner $planner): void
{
abort_unless(auth()->user()?->can('manage-panel'), 403);
@ -80,7 +80,7 @@ class Index extends Component
$this->deployStaging($target, $planner, $bridge);
}
/** Cut + push a beta of $target via the host bridge. $target must be one of the proposed values. */
/** Cut + push a release candidate of $target via the host bridge. $target must be one of the proposed values. */
public function deployStaging(string $target, ReleasePlanner $planner, ReleaseBridge $bridge): void
{
abort_unless(auth()->user()?->can('manage-panel'), 403);
@ -160,12 +160,12 @@ class Index extends Component
$pipeline->refresh();
}
/** The current in-flight beta tag (vX.Y.Z-betaN), or null when the running version is stable. */
private function currentBetaTag(): ?string
/** The current in-flight release-candidate tag (vX.Y.Z-rcN), or null when the running version is stable. */
private function currentRcTag(): ?string
{
$v = (string) config('clusev.version');
return str_contains($v, '-beta') ? 'v'.ltrim($v, 'vV') : null;
return str_contains($v, '-rc') ? 'v'.ltrim($v, 'vV') : null;
}
/** Per-user throttle shared by all promotion dispatches (auto-expiring, never a lockout). */
@ -197,7 +197,7 @@ class Index extends Component
public function confirmDeployPublic(): void
{
abort_unless(auth()->user()?->can('manage-panel'), 403);
$tag = $this->currentBetaTag();
$tag = $this->currentRcTag();
if ($tag === null) {
return;
}
@ -216,7 +216,7 @@ class Index extends Component
return;
}
$tag = (string) ($payload['params']['tag'] ?? '');
if ($tag === '' || $this->currentBetaTag() !== $tag || ! $this->throttlePromote()) {
if ($tag === '' || $this->currentRcTag() !== $tag || ! $this->throttlePromote()) {
return;
}
$ok = $promotion->deployPublic($tag);
@ -288,7 +288,7 @@ class Index extends Component
return view('livewire.release.index', [
'current' => $current,
'targets' => app(ReleasePlanner::class)->proposedTargets($current),
'pipeline' => app(PipelineStatus::class)->forTrackedBeta(),
'pipeline' => app(PipelineStatus::class)->forTrackedRc(),
'publicTags' => app(PromotionService::class)->publicTags(),
])->title(__('release.title'));
}

View File

@ -7,9 +7,9 @@ use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\Http;
/**
* Live status of the build pipeline for the currently-cut beta, for the dev Release page: tag (Gitea),
* mirror (GitHub-private), CI (GitHub Actions). These are global build facts of one beta there is no
* "deployed" step because any beta-channel server pulls a release itself (no single staging target).
* Live status of the build pipeline for the currently-cut release candidate, for the dev Release page: tag (Gitea),
* mirror (GitHub-private), CI (GitHub Actions). These are global build facts of one release candidate there is no
* "deployed" step because any server tracking the candidate pulls a release itself (no single staging target).
* Each step is cached briefly; any failure degrades to 'unknown' (never throws). The read-only GitHub
* token and the private staging slug come from config (env) and are never rendered or put in a URL.
*/
@ -20,10 +20,10 @@ class PipelineStatus
/**
* @return array{tag:string, steps:array<int,array{key:string,state:string,detail:?string,url:?string}>, ciRunning:bool}|null
*/
public function forTrackedBeta(): ?array
public function forTrackedRc(): ?array
{
$version = (string) config('clusev.version');
if (! str_contains($version, '-beta')) {
if (! str_contains($version, '-rc')) {
return null;
}
$tag = 'v'.ltrim($version, 'vV');
@ -47,7 +47,7 @@ class PipelineStatus
public function refresh(): void
{
$version = (string) config('clusev.version');
if (! str_contains($version, '-beta')) {
if (! str_contains($version, '-rc')) {
return;
}
$tag = 'v'.ltrim($version, 'vV');

View File

@ -14,9 +14,9 @@ use Illuminate\Support\Facades\Http;
*/
class PromotionService
{
public function deployPublic(string $betaTag): bool
public function deployPublic(string $rcTag): bool
{
return $this->dispatch('promote-public.yml', ['tag' => $betaTag]);
return $this->dispatch('promote-public.yml', ['tag' => $rcTag]);
}
public function yank(string $tag): bool

View File

@ -3,16 +3,16 @@
namespace App\Services;
/**
* Pure semver arithmetic for the Release page: given the current version, what beta-target versions
* may the operator cut? The dashboard computes the target X.Y.Z; the host bridge computes -betaN.
* Pure semver arithmetic for the Release page: given the current version, what rc-target versions
* may the operator cut? The dashboard computes the target X.Y.Z; the host bridge computes -rcN.
*/
class ReleasePlanner
{
/**
* Proposed target versions (bare X.Y.Z) for the current version. A beta also offers continuing
* its own base (another beta of the same target).
* Proposed target versions (bare X.Y.Z) for the current version. A release candidate also offers
* continuing its own base (another release candidate of the same target).
*
* @return array{patch:string, minor:string, major:string, continueBeta?:string}
* @return array{patch:string, minor:string, major:string, continueRc?:string}
*/
public function proposedTargets(string $current): array
{
@ -26,8 +26,8 @@ class ReleasePlanner
'major' => ($maj + 1).'.0.0',
];
if (str_contains($current, '-beta')) {
$targets['continueBeta'] = "{$maj}.{$min}.{$pat}";
if (str_contains($current, '-rc')) {
$targets['continueRc'] = "{$maj}.{$min}.{$pat}";
}
return $targets;

View File

@ -2,7 +2,7 @@
return [
// First tagged release is v0.1.0 (semantic, not -dev).
'version' => '0.11.0-beta1',
'version' => '0.11.0',
// Deployed commit + branch. install.sh bakes these into .env from the host's .git (the prod
// image ships no .git); the Versions page prefers them and falls back to a live .git read in
@ -12,7 +12,7 @@ return [
'branch' => env('CLUSEV_BUILD_BRANCH') ?: null,
],
// Default user channel. Only 'stable' and 'beta' are ever offered to users.
// Single stable release channel — users only ever receive finalized vX.Y.Z releases.
'channel' => 'stable',
// Install-time panel domain (APP_DOMAIN). The dashboard Setting overrides it; this

View File

@ -1,10 +1,10 @@
# Release bridge (host watcher) — DEV ONLY
The dashboard "Release" page (Deploy to Staging) cuts a beta by asking the **host** to do the git
The dashboard "Release" page (Deploy to Staging) cuts a release candidate by asking the **host** to do the git
work, so the container never holds git credentials — the same isolation as the WireGuard bridge.
- `clusev-release.sh serve-request` — reads `run/release-request.json` (the dashboard wrote it),
re-validates, bumps `config/clusev.php`, commits, tags `vX.Y.Z-betaN`, pushes to Gitea (token read
re-validates, bumps `config/clusev.php`, commits, tags `vX.Y.Z-rcN`, pushes to Gitea (token read
host-side from `/home/nexxo/.env.gitea`), and writes `run/release-result-<id>.json` for the
dashboard to poll. Roll back leaves no half-release if the push fails.
- `clusev-release-request.path` / `.service` — a systemd `.path` watcher that runs the handler when a
@ -30,5 +30,5 @@ lands where the watcher looks.
## Verify (manual gate)
This is the end-to-end gate (like the WireGuard runbook): with the watcher enabled and the flag on,
open `/release`, click a bump button, confirm, and check that `vX.Y.Z-betaN` lands on Gitea (and, via
open `/release`, click a bump button, confirm, and check that `vX.Y.Z-rcN` lands on Gitea (and, via
the push-mirror, on GitHub-private). The handler never logs the Gitea token.

View File

@ -1,6 +1,6 @@
#!/usr/bin/env bash
# Host side of the dashboard release bridge. `serve-request` reads run/release-request.json (the
# dashboard wrote it), cuts a beta — bump config version, commit, tag vX.Y.Z-betaN, push to Gitea —
# dashboard wrote it), cuts a release candidate — bump config version, commit, tag vX.Y.Z-rcN, push to Gitea —
# and writes a result the dashboard polls. ALL git/token operations run here; the container never
# holds credentials. `stage <target>` is the same path without the request file (used by the test).
set -euo pipefail
@ -43,7 +43,7 @@ _precheck() {
[ "$(git -C "$PROJ" rev-parse HEAD)" = "$(git -C "$PROJ" rev-parse "origin/${BRANCH}")" ] || { echo "unpushed"; return 1; }
}
# Cut + push a beta of <target>. Echoes the new tag on success; reason on stderr + non-zero on failure.
# Cut + push a release candidate of <target>. Echoes the new tag on success; reason on stderr + non-zero on failure.
_do_stage() { # target
local target="$1" cur base
cur="$(grep -oE "'version' => '[^']*'" "${PROJ}/config/clusev.php" | head -n1 | sed -E "s/.*=> '//; s/'$//" || true)"
@ -51,11 +51,11 @@ _do_stage() { # target
_ver_ge "$target" "$base" || { echo "downgrade-rejected" >&2; return 1; }
local n=0 t k
for t in $(git -C "$PROJ" tag -l "v${target}-beta*"); do
k="${t##*-beta}"
for t in $(git -C "$PROJ" tag -l "v${target}-rc*"); do
k="${t##*-rc}"
case "$k" in ''|*[!0-9]*) ;; *) [ "$k" -gt "$n" ] && n="$k" ;; esac
done
local ver="${target}-beta$((n + 1))" newtag="v${target}-beta$((n + 1))"
local ver="${target}-rc$((n + 1))" newtag="v${target}-rc$((n + 1))"
sed -i -E "s/('version' => ')[^']*(')/\\1${ver}\\2/" "${PROJ}/config/clusev.php"
git -C "$PROJ" commit -q -am "chore(release): ${ver}" || { echo "commit-failed" >&2; git -C "$PROJ" checkout -- . 2>/dev/null || true; return 1; }

View File

@ -1,6 +1,6 @@
#!/usr/bin/env bash
# Test clusev-release.sh `stage`: bump config version, commit, tag vX.Y.Z-betaN, push to a (local)
# Gitea-stand-in remote; beta auto-increment; refusals (dirty / unpushed / downgrade); push-fail rollback.
# Test clusev-release.sh `stage`: bump config version, commit, tag vX.Y.Z-rcN, push to a (local)
# Gitea-stand-in remote; rc auto-increment; refusals (dirty / unpushed / downgrade); push-fail rollback.
set -euo pipefail
here="$(cd "$(dirname "$0")" && pwd)"
work="$(mktemp -d)"; trap 'rm -rf "$work"' EXIT
@ -17,15 +17,15 @@ git -C "$work/proj" add -A && git -C "$work/proj" commit -qm init && git -C "$wo
printf 'GIT_ACCESS_TOKEN=dummy\n' > "$work/.env.gitea"
run() { CLUSEV_DIR="$work/proj" CLUSEV_RELEASE_BRANCH=main CLUSEV_GITEA_ENV="$work/.env.gitea" bash "$here/clusev-release.sh" "$@"; }
# happy path: minor -> 0.10.0-beta1, config bumped, tag pushed to the remote
# happy path: minor -> 0.10.0-rc1, config bumped, tag pushed to the remote
tag="$(run stage 0.10.0)"
test "$tag" = "v0.10.0-beta1" || { echo "FAIL: expected v0.10.0-beta1, got '$tag'"; exit 1; }
grep -q "'version' => '0.10.0-beta1'" "$work/proj/config/clusev.php" || { echo "FAIL: config not bumped"; exit 1; }
git -C "$work/remote.git" tag | grep -qx 'v0.10.0-beta1' || { echo "FAIL: tag not pushed to remote"; exit 1; }
test "$tag" = "v0.10.0-rc1" || { echo "FAIL: expected v0.10.0-rc1, got '$tag'"; exit 1; }
grep -q "'version' => '0.10.0-rc1'" "$work/proj/config/clusev.php" || { echo "FAIL: config not bumped"; exit 1; }
git -C "$work/remote.git" tag | grep -qx 'v0.10.0-rc1' || { echo "FAIL: tag not pushed to remote"; exit 1; }
# second run, same target -> beta2
# second run, same target -> rc2
tag2="$(run stage 0.10.0)"
test "$tag2" = "v0.10.0-beta2" || { echo "FAIL: expected beta2, got '$tag2'"; exit 1; }
test "$tag2" = "v0.10.0-rc2" || { echo "FAIL: expected rc2, got '$tag2'"; exit 1; }
# downgrade rejected (target below the current base 0.10.0)
if run stage 0.9.0 2>/dev/null; then echo "FAIL: downgrade not rejected"; exit 1; fi
@ -49,7 +49,7 @@ chmod +x "$work/remote.git/hooks/pre-receive"
before="$(git -C "$work/proj" rev-parse HEAD)"
if run stage 0.11.0 2>/dev/null; then echo "FAIL: push to a rejecting remote should fail"; exit 1; fi
rm -f "$work/remote.git/hooks/pre-receive"
git -C "$work/proj" tag | grep -qx 'v0.11.0-beta1' && { echo "FAIL: tag not rolled back after push reject"; exit 1; }
git -C "$work/proj" tag | grep -qx 'v0.11.0-rc1' && { echo "FAIL: tag not rolled back after push reject"; exit 1; }
test "$(git -C "$work/proj" rev-parse HEAD)" = "$before" || { echo "FAIL: HEAD not rolled back after push reject"; exit 1; }
test -z "$(git -C "$work/proj" status --porcelain)" || { echo "FAIL: config not restored (dirty) after rollback"; exit 1; }

View File

@ -5,7 +5,7 @@ return [
'nav' => 'Release',
'eyebrow' => 'Release-Pipeline',
'heading' => 'Release-Steuerung',
'subtitle' => 'Beta auf Staging schneiden — nur auf der Dev-Instanz.',
'subtitle' => 'Release Candidate auf Staging schneiden — nur auf der Dev-Instanz.',
'dev_instance' => 'Dev-Instanz',
'current' => 'Aktuelle Version',
@ -13,28 +13,28 @@ return [
'source' => 'Quelle',
// option tiles
'choose' => 'Beta schneiden',
'kind_continue' => 'Nächste Beta',
'choose' => 'Release Candidate schneiden',
'kind_continue' => 'Nächster RC',
'kind_patch' => 'Patch',
'kind_minor' => 'Minor',
'kind_major' => 'Major',
'tile_beta' => 'Beta-Tag',
'hint' => 'Schneidet einen Beta-Tag und pusht ihn nach Gitea → Mirror → CI → Staging.',
'tile_rc' => 'RC-Tag',
'hint' => 'Schneidet einen RC-Tag und pusht ihn nach Gitea → Mirror → CI → Staging.',
// running / result
'running_title' => 'Release läuft',
'running' => 'Beta :target wird geschnitten und gepusht …',
'running' => 'Release Candidate :target wird geschnitten und gepusht …',
'in_progress' => 'läuft',
'staged' => 'Beta :tag gepusht → Mirror → CI → Staging.',
'staged' => 'Release Candidate :tag gepusht → Mirror → CI → Staging.',
'staged_label' => 'Zuletzt gepusht:',
'failed' => 'Release fehlgeschlagen: :reason',
// pipeline rail (Tag → Mirror → CI; build readiness of a beta)
// pipeline rail (Tag → Mirror → CI; build readiness of a release candidate)
'pipeline' => 'Pipeline',
'pipe_sub_gitea' => 'Quelle · Tag wird hier erzeugt',
'pipe_sub_mirror' => 'Automatischer Push-Mirror',
'pipe_sub_ci' => 'Tests laufen',
'no_beta_running' => 'Keine Beta in Arbeit.',
'no_rc_running' => 'Kein Release Candidate in Arbeit.',
'state_done' => 'fertig',
'state_running' => 'läuft …',
'state_failed' => 'fehlgeschlagen',
@ -42,9 +42,9 @@ return [
'state_unknown' => 'unbekannt',
// confirm modal (R5)
'confirm_title' => 'Beta auf Staging schneiden?',
'confirm_body' => 'Schneidet eine Beta von :target, taggt sie und pusht nach Gitea → Mirror → CI → Staging. Der Tag ist unwiderruflich.',
'confirm_action' => 'Beta schneiden',
'confirm_title' => 'Release Candidate auf Staging schneiden?',
'confirm_body' => 'Schneidet einen Release Candidate von :target, taggt ihn und pusht nach Gitea → Mirror → CI → Staging. Der Tag ist unwiderruflich.',
'confirm_action' => 'Release Candidate schneiden',
// errors
'invalid_target' => 'Ungültige Ziel-Version.',
@ -57,12 +57,12 @@ return [
'deploy_public' => 'Deploy to Public (:tag)',
'promote_stable' => 'Promote to Stable (:v)',
'publish_hint' => 'Löst einen GitHub-Workflow aus, der die Version ins öffentliche Repo befördert.',
'public_confirm_title' => 'Beta öffentlich machen?',
'public_confirm_body' => 'Befördert :tag in den öffentlichen Beta-Kanal (clusev/clusev).',
'public_confirm_title' => 'Release Candidate öffentlich machen?',
'public_confirm_body' => 'Befördert :tag ins öffentliche Repo (clusev/clusev).',
'public_action' => 'Veröffentlichen',
'public_dispatched' => 'Public-Promotion ausgelöst: :tag.',
'stable_confirm_title' => 'Als Stable freigeben?',
'stable_confirm_body' => 'Finalisiert die Beta als stabile :v im öffentlichen Stable-Kanal.',
'stable_confirm_body' => 'Finalisiert den Release Candidate als stabile :v im öffentlichen Stable-Kanal.',
'stable_action' => 'Stable freigeben',
'stable_dispatched' => 'Stable-Promotion ausgelöst: :v.',
'promote_failed' => 'Auslösen fehlgeschlagen — Token-Scope (Actions: Write) prüfen.',

View File

@ -5,7 +5,7 @@ return [
'nav' => 'Release',
'eyebrow' => 'Release pipeline',
'heading' => 'Release control',
'subtitle' => 'Cut a staging beta — dev instance only.',
'subtitle' => 'Cut a staging release candidate — dev instance only.',
'dev_instance' => 'Dev instance',
'current' => 'Current version',
@ -13,28 +13,28 @@ return [
'source' => 'Source',
// option tiles
'choose' => 'Cut a beta',
'kind_continue' => 'Next beta',
'choose' => 'Cut a release candidate',
'kind_continue' => 'Next RC',
'kind_patch' => 'Patch',
'kind_minor' => 'Minor',
'kind_major' => 'Major',
'tile_beta' => 'Beta tag',
'hint' => 'Cuts a beta tag and pushes it to Gitea → mirror → CI → staging.',
'tile_rc' => 'RC tag',
'hint' => 'Cuts an RC tag and pushes it to Gitea → mirror → CI → staging.',
// running / result
'running_title' => 'Release running',
'running' => 'Cutting and pushing beta :target …',
'running' => 'Cutting and pushing release candidate :target …',
'in_progress' => 'running',
'staged' => 'Beta :tag pushed → mirror → CI → staging.',
'staged' => 'Release candidate :tag pushed → mirror → CI → staging.',
'staged_label' => 'Last pushed:',
'failed' => 'Release failed: :reason',
// pipeline rail (Tag → Mirror → CI; build readiness of a beta)
// pipeline rail (Tag → Mirror → CI; build readiness of a release candidate)
'pipeline' => 'Pipeline',
'pipe_sub_gitea' => 'Source · tag is cut here',
'pipe_sub_mirror' => 'Automatic push mirror',
'pipe_sub_ci' => 'Tests run',
'no_beta_running' => 'No beta in flight.',
'no_rc_running' => 'No release candidate in flight.',
'state_done' => 'done',
'state_running' => 'running …',
'state_failed' => 'failed',
@ -42,9 +42,9 @@ return [
'state_unknown' => 'unknown',
// confirm modal (R5)
'confirm_title' => 'Cut a staging beta?',
'confirm_body' => 'Cuts a beta of :target, tags it and pushes to Gitea → mirror → CI → staging. The tag is irreversible.',
'confirm_action' => 'Cut beta',
'confirm_title' => 'Cut a staging release candidate?',
'confirm_body' => 'Cuts a release candidate of :target, tags it and pushes to Gitea → mirror → CI → staging. The tag is irreversible.',
'confirm_action' => 'Cut release candidate',
// errors
'invalid_target' => 'Invalid target version.',
@ -57,12 +57,12 @@ return [
'deploy_public' => 'Deploy to Public (:tag)',
'promote_stable' => 'Promote to Stable (:v)',
'publish_hint' => 'Triggers a GitHub workflow that promotes the version to the public repo.',
'public_confirm_title' => 'Make the beta public?',
'public_confirm_body' => 'Promotes :tag to the public beta channel (clusev/clusev).',
'public_confirm_title' => 'Make the release candidate public?',
'public_confirm_body' => 'Promotes :tag to the public repo (clusev/clusev).',
'public_action' => 'Publish',
'public_dispatched' => 'Public promotion triggered: :tag.',
'stable_confirm_title' => 'Release as stable?',
'stable_confirm_body' => 'Finalises the beta as stable :v in the public stable channel.',
'stable_confirm_body' => 'Finalises the release candidate as stable :v in the public stable channel.',
'stable_action' => 'Release stable',
'stable_dispatched' => 'Stable promotion triggered: :v.',
'promote_failed' => 'Dispatch failed — check the token scope (Actions: Write).',

View File

@ -1,9 +1,9 @@
@php
$isBeta = str_contains($current, '-beta');
$isRc = str_contains($current, '-rc');
$tiles = [];
if (isset($targets['continueBeta'])) {
$tiles[] = ['target' => $targets['continueBeta'], 'label' => __('release.kind_continue'), 'icon' => 'rotate', 'primary' => true];
if (isset($targets['continueRc'])) {
$tiles[] = ['target' => $targets['continueRc'], 'label' => __('release.kind_continue'), 'icon' => 'rotate', 'primary' => true];
}
$tiles[] = ['target' => $targets['patch'], 'label' => __('release.kind_patch'), 'icon' => 'tag', 'primary' => false];
$tiles[] = ['target' => $targets['minor'], 'label' => __('release.kind_minor'), 'icon' => 'tag', 'primary' => false];
@ -39,7 +39,7 @@
<dl class="flex gap-7 font-mono text-[11px]">
<div>
<dt class="uppercase tracking-wider text-ink-4">{{ __('release.channel') }}</dt>
<dd class="mt-1 {{ $isBeta ? 'text-warning' : 'text-online' }}">{{ $isBeta ? 'beta' : 'stable' }}</dd>
<dd class="mt-1 {{ $isRc ? 'text-warning' : 'text-online' }}">{{ $isRc ? 'rc' : 'stable' }}</dd>
</div>
<div>
<dt class="uppercase tracking-wider text-ink-4">{{ __('release.source') }}</dt>
@ -106,7 +106,7 @@
'text-accent-text' => $t['primary'],
'text-ink' => ! $t['primary'],
])>{{ $t['target'] }}</span>
<x-badge tone="{{ $t['primary'] ? 'accent' : 'neutral' }}" class="ml-auto">{{ __('release.tile_beta') }}</x-badge>
<x-badge tone="{{ $t['primary'] ? 'accent' : 'neutral' }}" class="ml-auto">{{ __('release.tile_rc') }}</x-badge>
</div>
</button>
@endforeach
@ -115,8 +115,8 @@
</div>
@endif
@if ($isBeta)
{{-- Publish the current beta --}}
@if ($isRc)
{{-- Publish the current release candidate --}}
<div>
<p class="mb-2.5 font-mono text-[11px] uppercase tracking-wider text-ink-3">{{ __('release.publish') }}</p>
<div class="grid gap-3">
@ -150,7 +150,7 @@
{{-- Right: live pipeline rail --}}
<x-panel :title="__('release.pipeline')">
@if ($pipeline === null)
<p class="font-mono text-[11px] text-ink-4">{{ __('release.no_beta_running') }}</p>
<p class="font-mono text-[11px] text-ink-4">{{ __('release.no_rc_running') }}</p>
@else
<ol>
@foreach ($pipeline['steps'] as $s)

View File

@ -55,7 +55,7 @@ while IFS= read -r pat; do
done <<< "$forbidden"
git -C "$pub" add -A
# --allow-empty: stable promotion re-tags the same source commit as the last beta, so the
# --allow-empty: stable promotion re-tags the same source commit as the last release candidate, so the
# extracted tree is byte-identical and there is nothing to stage — still produce a release commit.
git -C "$pub" commit -q --allow-empty -m "$msg"
git -C "$pub" tag "$tag"

View File

@ -35,7 +35,7 @@ test -d "$work/pub/.git" || { echo "FAIL: public .git clobbered"; exit
git -C "$work/pub" rev-parse v1.0.0 >/dev/null 2>&1 || { echo "FAIL: tag missing"; exit 1; }
git -C "$work/pub" log -1 --pretty=%s | grep -qx 'Release v1.0.0' || { echo "FAIL: commit message"; exit 1; }
# Beta -> stable: stable re-tags the SAME source commit, so the extracted tree is byte-identical
# RC -> stable: stable re-tags the SAME source commit, so the extracted tree is byte-identical
# to the last promotion. Promote a second tag (same tree) into the SAME pub repo and assert it
# still creates the tag — guards against `git commit` aborting on "nothing to commit".
git -C "$work/src" tag v1.0.1 v1.0.0

View File

@ -19,7 +19,7 @@ class PipelineStatusTest extends TestCase
{
parent::setUp();
Cache::flush();
config()->set('clusev.version', '0.9.61-beta1');
config()->set('clusev.version', '0.9.61-rc1');
config()->set('clusev.github_token', 'tok_secret');
config()->set('clusev.staging_slug', self::SLUG);
}
@ -46,27 +46,27 @@ class PipelineStatusTest extends TestCase
Http::swap(new HttpFactory(app('events')));
}
public function test_non_beta_version_has_no_pipeline(): void
public function test_non_rc_version_has_no_pipeline(): void
{
config()->set('clusev.version', '0.9.60');
$this->assertNull(app(PipelineStatus::class)->forTrackedBeta());
$this->assertNull(app(PipelineStatus::class)->forTrackedRc());
}
public function test_tag_step_is_done_for_the_running_beta(): void
public function test_tag_step_is_done_for_the_running_rc(): void
{
Http::fake(['api.github.com/*' => Http::response([], 404)]);
$p = app(PipelineStatus::class)->forTrackedBeta();
$this->assertSame('v0.9.61-beta1', $p['tag']);
$p = app(PipelineStatus::class)->forTrackedRc();
$this->assertSame('v0.9.61-rc1', $p['tag']);
$this->assertSame('done', $this->step($p, 'tag')['state']);
}
public function test_mirror_done_when_the_tag_exists_on_github(): void
{
Http::fake([
'api.github.com/repos/acme/staging/git/refs/tags/v0.9.61-beta1' => Http::response(['ref' => 'refs/tags/v0.9.61-beta1']),
'api.github.com/repos/acme/staging/git/refs/tags/v0.9.61-rc1' => Http::response(['ref' => 'refs/tags/v0.9.61-rc1']),
'api.github.com/repos/acme/staging/actions/runs*' => Http::response(['workflow_runs' => []]),
]);
$p = app(PipelineStatus::class)->forTrackedBeta();
$p = app(PipelineStatus::class)->forTrackedRc();
$this->assertSame('done', $this->step($p, 'mirror')['state']);
}
@ -76,14 +76,14 @@ class PipelineStatusTest extends TestCase
'api.github.com/repos/acme/staging/git/refs/tags/*' => Http::response([], 404),
'api.github.com/repos/acme/staging/actions/runs*' => Http::response(['workflow_runs' => []]),
]);
$p = app(PipelineStatus::class)->forTrackedBeta();
$p = app(PipelineStatus::class)->forTrackedRc();
$this->assertSame('pending', $this->step($p, 'mirror')['state']);
}
public function test_ci_running_then_done_then_failed(): void
{
$run = fn (string $status, ?string $conclusion) => Http::response(['workflow_runs' => [[
'head_branch' => 'v0.9.61-beta1', 'status' => $status, 'conclusion' => $conclusion,
'head_branch' => 'v0.9.61-rc1', 'status' => $status, 'conclusion' => $conclusion,
'html_url' => 'https://github.com/acme/staging/actions/runs/1',
]]]);
@ -91,7 +91,7 @@ class PipelineStatusTest extends TestCase
'api.github.com/repos/acme/staging/git/refs/tags/*' => Http::response(['ref' => 'x']),
'api.github.com/repos/acme/staging/actions/runs*' => $run('in_progress', null),
]);
$p = app(PipelineStatus::class)->forTrackedBeta();
$p = app(PipelineStatus::class)->forTrackedRc();
$this->assertSame('running', $this->step($p, 'ci')['state']);
$this->assertTrue($p['ciRunning']);
$this->assertStringContainsString('runs/1', $this->step($p, 'ci')['url']);
@ -101,14 +101,14 @@ class PipelineStatusTest extends TestCase
'api.github.com/repos/acme/staging/git/refs/tags/*' => Http::response(['ref' => 'x']),
'api.github.com/repos/acme/staging/actions/runs*' => $run('completed', 'success'),
]);
$this->assertSame('done', $this->step(app(PipelineStatus::class)->forTrackedBeta(), 'ci')['state']);
$this->assertSame('done', $this->step(app(PipelineStatus::class)->forTrackedRc(), 'ci')['state']);
$this->resetForNextPhase();
Http::fake([
'api.github.com/repos/acme/staging/git/refs/tags/*' => Http::response(['ref' => 'x']),
'api.github.com/repos/acme/staging/actions/runs*' => $run('completed', 'failure'),
]);
$this->assertSame('failed', $this->step(app(PipelineStatus::class)->forTrackedBeta(), 'ci')['state']);
$this->assertSame('failed', $this->step(app(PipelineStatus::class)->forTrackedRc(), 'ci')['state']);
}
public function test_ci_pending_when_no_run_matches_the_tag(): void
@ -117,14 +117,14 @@ class PipelineStatusTest extends TestCase
'api.github.com/repos/acme/staging/git/refs/tags/*' => Http::response(['ref' => 'x']),
'api.github.com/repos/acme/staging/actions/runs*' => Http::response(['workflow_runs' => [['head_branch' => 'v9.9.9', 'status' => 'completed', 'conclusion' => 'success']]]),
]);
$this->assertSame('pending', $this->step(app(PipelineStatus::class)->forTrackedBeta(), 'ci')['state']);
$this->assertSame('pending', $this->step(app(PipelineStatus::class)->forTrackedRc(), 'ci')['state']);
}
public function test_no_token_makes_mirror_and_ci_unknown_without_calling_github(): void
{
config()->set('clusev.github_token', '');
Http::fake();
$p = app(PipelineStatus::class)->forTrackedBeta();
$p = app(PipelineStatus::class)->forTrackedRc();
$this->assertSame('unknown', $this->step($p, 'mirror')['state']);
$this->assertSame('unknown', $this->step($p, 'ci')['state']);
Http::assertNothingSent();
@ -133,7 +133,7 @@ class PipelineStatusTest extends TestCase
public function test_token_is_sent_in_the_header_never_in_the_url(): void
{
Http::fake(['api.github.com/*' => Http::response(['ref' => 'x'])]);
app(PipelineStatus::class)->forTrackedBeta();
app(PipelineStatus::class)->forTrackedRc();
Http::assertSent(fn ($req) => $req->hasHeader('Authorization')
&& ! str_contains($req->url(), 'tok_secret'));
}
@ -141,7 +141,7 @@ class PipelineStatusTest extends TestCase
public function test_github_5xx_degrades_to_unknown(): void
{
Http::fake(['api.github.com/*' => Http::response('boom', 500)]);
$p = app(PipelineStatus::class)->forTrackedBeta();
$p = app(PipelineStatus::class)->forTrackedRc();
$this->assertSame('unknown', $this->step($p, 'mirror')['state']);
$this->assertSame('unknown', $this->step($p, 'ci')['state']);
}
@ -149,7 +149,7 @@ class PipelineStatusTest extends TestCase
public function test_github_throwing_degrades_to_unknown_never_throws(): void
{
Http::fake(['api.github.com/*' => fn () => throw new \RuntimeException('net down')]);
$p = app(PipelineStatus::class)->forTrackedBeta();
$p = app(PipelineStatus::class)->forTrackedRc();
$this->assertSame('unknown', $this->step($p, 'mirror')['state']);
$this->assertSame('unknown', $this->step($p, 'ci')['state']);
}

View File

@ -28,12 +28,12 @@ class PromotionServiceTest extends TestCase
{
Http::fake(['api.github.com/*' => Http::response('', 204)]);
$this->assertTrue(app(PromotionService::class)->deployPublic('v0.10.0-beta1'));
$this->assertTrue(app(PromotionService::class)->deployPublic('v0.10.0-rc1'));
Http::assertSent(fn ($req) => $req->method() === 'POST'
&& str_contains($req->url(), 'repos/acme/staging/actions/workflows/promote-public.yml/dispatches')
&& $req['ref'] === 'feat/v1-foundation'
&& $req['inputs']['tag'] === 'v0.10.0-beta1'
&& $req['inputs']['tag'] === 'v0.10.0-rc1'
&& $req->hasHeader('Authorization')
&& ! str_contains($req->url(), 'tok_secret'));
}
@ -51,7 +51,7 @@ class PromotionServiceTest extends TestCase
public function test_dispatch_returns_false_on_a_github_error(): void
{
Http::fake(['api.github.com/*' => Http::response('forbidden', 403)]);
$this->assertFalse(app(PromotionService::class)->deployPublic('v0.10.0-beta1'));
$this->assertFalse(app(PromotionService::class)->deployPublic('v0.10.0-rc1'));
}
public function test_dispatch_without_a_token_does_not_call_github(): void
@ -59,7 +59,7 @@ class PromotionServiceTest extends TestCase
config()->set('clusev.github_token', '');
Http::fake();
$this->assertFalse(app(PromotionService::class)->deployPublic('v0.10.0-beta1'));
$this->assertFalse(app(PromotionService::class)->deployPublic('v0.10.0-rc1'));
Http::assertNothingSent();
}

View File

@ -55,13 +55,13 @@ class ReleaseBridgeTest extends TestCase
$id = app(ReleaseBridge::class)->requestStaging('0.10.0');
file_put_contents(
$this->dir."/release-result-{$id}.json",
json_encode(['id' => $id, 'ok' => true, 'tag' => 'v0.10.0-beta1', 'message' => 'ok'])
json_encode(['id' => $id, 'ok' => true, 'tag' => 'v0.10.0-rc1', 'message' => 'ok'])
);
$res = app(ReleaseBridge::class)->result($id);
$this->assertTrue($res['ok']);
$this->assertSame('v0.10.0-beta1', $res['tag']);
$this->assertSame('v0.10.0-rc1', $res['tag']);
}
public function test_result_is_null_until_the_host_replies(): void

View File

@ -11,16 +11,16 @@ class ReleaseCheckerChannelTest extends TestCase
{
use RefreshDatabase;
public function test_channel_is_always_stable_even_if_a_beta_setting_lingers(): void
public function test_channel_is_always_stable_even_if_a_prerelease_setting_lingers(): void
{
Setting::put('release_channel', 'beta'); // a stale setting from before the drop
Setting::put('release_channel', 'rc'); // a stale setting from before the drop
$this->assertSame('stable', app(ReleaseChecker::class)->channel());
}
public function test_newest_version_ignores_rc_and_beta_prereleases(): void
public function test_newest_version_ignores_rc_prereleases(): void
{
$checker = app(ReleaseChecker::class);
$tags = ['v0.11.0', 'v0.12.0-rc1', 'v0.12.0-beta2', 'v0.10.0'];
$tags = ['v0.11.0', 'v0.12.0-rc1', 'v0.12.0-rc2', 'v0.10.0'];
$this->assertSame('0.11.0', $checker->newestVersion($tags, 'stable'));
}
}

View File

@ -76,10 +76,10 @@ class ReleasePageTest extends TestCase
$id = $c->get('pendingId');
file_put_contents(
$this->dir."/release-result-{$id}.json",
json_encode(['id' => $id, 'ok' => true, 'tag' => 'v0.10.0-beta1', 'message' => 'ok'])
json_encode(['id' => $id, 'ok' => true, 'tag' => 'v0.10.0-rc1', 'message' => 'ok'])
);
$c->call('pollResult')->assertSet('pendingId', null)->assertSee('v0.10.0-beta1');
$c->call('pollResult')->assertSet('pendingId', null)->assertSee('v0.10.0-rc1');
}
public function test_poll_result_audits_a_host_failure_as_an_error(): void
@ -101,14 +101,14 @@ class ReleasePageTest extends TestCase
Livewire::test(Index::class)->assertStatus(404);
}
public function test_render_includes_the_live_pipeline_for_a_beta(): void
public function test_render_includes_the_live_pipeline_for_an_rc(): void
{
config()->set('clusev.version', '0.9.61-beta1');
config()->set('clusev.version', '0.9.61-rc1');
config()->set('clusev.staging_slug', ''); // degrade: no GitHub calls
config()->set('clusev.github_token', '');
Livewire::test(Index::class)
->assertViewHas('pipeline', fn ($p) => is_array($p) && $p['tag'] === 'v0.9.61-beta1');
->assertViewHas('pipeline', fn ($p) => is_array($p) && $p['tag'] === 'v0.9.61-rc1');
}
public function test_render_pipeline_is_null_for_a_stable_version(): void
@ -117,15 +117,15 @@ class ReleasePageTest extends TestCase
Livewire::test(Index::class)->assertViewHas('pipeline', null);
}
public function test_deploy_public_dispatches_and_audits_for_a_beta(): void
public function test_deploy_public_dispatches_and_audits_for_an_rc(): void
{
config()->set('clusev.version', '0.10.0-beta1');
config()->set('clusev.version', '0.10.0-rc1');
config()->set('clusev.github_token', 'tok');
config()->set('clusev.staging_slug', 'acme/staging');
config()->set('clusev.release_branch', 'feat/v1-foundation');
Http::fake(['api.github.com/*' => Http::response('', 204)]);
$token = ConfirmToken::issue('releasePublic', ['tag' => 'v0.10.0-beta1']);
$token = ConfirmToken::issue('releasePublic', ['tag' => 'v0.10.0-rc1']);
ConfirmToken::confirm($token); // the modal confirmation step
Livewire::test(Index::class)->call('applyDeployPublic', $token);
@ -158,11 +158,11 @@ class ReleasePageTest extends TestCase
public function test_publish_actions_are_a_no_op_for_a_stable_version(): void
{
config()->set('clusev.version', '0.10.0'); // not a beta
config()->set('clusev.version', '0.10.0'); // not an rc
config()->set('clusev.public_slug', ''); // degrade: no public-tags read on render
Http::fake();
$token = ConfirmToken::issue('releasePublic', ['tag' => 'v0.10.0-beta1']);
$token = ConfirmToken::issue('releasePublic', ['tag' => 'v0.10.0-rc1']);
ConfirmToken::confirm($token); // the modal confirmation step
Livewire::test(Index::class)->call('applyDeployPublic', $token);

View File

@ -236,12 +236,12 @@ class TerminalTest extends TestCase
public function test_search_filters_the_server_rail(): void
{
Server::create(['name' => 'alpha-web', 'ip' => '10.0.0.1', 'status' => 'online']);
Server::create(['name' => 'beta-db', 'ip' => '10.0.0.2', 'status' => 'online']);
Server::create(['name' => 'delta-db', 'ip' => '10.0.0.2', 'status' => 'online']);
Livewire::test(Index::class)
->set('search', 'alpha')
->assertSee('alpha-web')
->assertDontSee('beta-db');
->assertDontSee('delta-db');
}
public function test_search_box_appears_once_there_is_more_than_one_server(): void

View File

@ -114,16 +114,16 @@ class VersionUpdateCheckTest extends TestCase
{
config()->set('clusev.version', '0.9.6');
Http::fake([self::TAGS_URL => Http::response([
['name' => 'v1.0.0-beta1'], ['name' => 'v0.9.6'],
['name' => 'v1.0.0-rc1'], ['name' => 'v0.9.6'],
])]);
// stable: 1.0.0-beta1 is not offered, 0.9.6 == installed → current
// stable: 1.0.0-rc1 is not offered, 0.9.6 == installed → current
Setting::put('release_channel', 'stable');
Livewire::test(Index::class)->call('checkUpdates')->assertSet('updateState', 'current');
// a stale beta setting from before the channel drop is ignored → still current
// a stale prerelease setting from before the channel drop is ignored → still current
Cache::flush();
Setting::put('release_channel', 'beta');
Setting::put('release_channel', 'rc');
Livewire::test(Index::class)
->call('checkUpdates')
->assertSet('updateState', 'current');

View File

@ -15,21 +15,21 @@ class ReleasePlannerTest extends TestCase
$this->planner = new ReleasePlanner;
}
public function test_stable_version_offers_patch_minor_major_without_continue_beta(): void
public function test_stable_version_offers_patch_minor_major_without_continue_rc(): void
{
$t = $this->planner->proposedTargets('0.9.58');
$this->assertSame('0.9.59', $t['patch']);
$this->assertSame('0.10.0', $t['minor']);
$this->assertSame('1.0.0', $t['major']);
$this->assertArrayNotHasKey('continueBeta', $t);
$this->assertArrayNotHasKey('continueRc', $t);
}
public function test_beta_version_also_offers_continue_beta_of_its_base(): void
public function test_rc_version_also_offers_continue_rc_of_its_base(): void
{
$t = $this->planner->proposedTargets('0.10.0-beta3');
$t = $this->planner->proposedTargets('0.10.0-rc3');
$this->assertSame('0.10.0', $t['continueBeta']);
$this->assertSame('0.10.0', $t['continueRc']);
$this->assertSame('0.10.1', $t['patch']);
$this->assertSame('0.11.0', $t['minor']);
$this->assertSame('1.0.0', $t['major']);
@ -42,9 +42,9 @@ class ReleasePlannerTest extends TestCase
public function test_allowed_targets_are_the_proposed_values(): void
{
$allowed = $this->planner->allowedTargets('0.10.0-beta1');
$allowed = $this->planner->allowedTargets('0.10.0-rc1');
$this->assertContains('0.10.0', $allowed); // continueBeta
$this->assertContains('0.10.0', $allowed); // continueRc
$this->assertContains('0.11.0', $allowed); // minor
$this->assertNotContains('0.9.0', $allowed);
}