name: ship-rolling-candidate description: Ship a rolling release candidate vX.Y.Z-rN. Bumps AppVersion, builds Windows + triggers Mac/Linux CI, applies notes, marks prerelease, deletes previous candidate per rolling-rN policy. when: Code change closed (plan item done, build/tests green) — ship autonomously без ожидания команды. Triggered automatically после fix-cycle. User intervenes только если direction wrong ("стоп", "hold", "откати").
Ship a rolling release candidate
VPNRouter ships iterations as vX.Y.Z-r1, vX.Y.Z-r2, etc. Only one
prerelease visible at a time. Stable cut НЕ autonomous — по явной команде
user после verification gate (см. cut-stable skill, rule #6).
HARD PRECONDITION (added 2026-05-25 after r7..r18 red-CI streak)
Before doing anything else, verify previous commit's CI is GREEN:
powershell -ExecutionPolicy Bypass -File tools/verify-last-commit-ci.ps1
Exit 0 → safe to ship. Exit 1/2/3 → STOP, do not bump AppVersion, do not start the cycle. Investigate the red/in-progress check first.
Why this rule exists: night-shift 2026-05-25 shipped r7→r18 (12
candidates) in a row. Tag-level CI on each vX.Y.Z-rN tag was green
because Windows binaries built fine. But commit-level CI on every
push event was RED throughout — Linux/macOS Avalonia XAML
compilation failed on LblZapretCacheStatus binding (introduced in r10
inside #if PLATFORM_WINDOWS but referenced by cross-platform XAML).
The bug propagated through 8 follow-up commits because nobody checked
the commits page on main between ships. User caught it via screenshot
of red-X column with 2/6 or 4/6 passing.
The script enforces this check so the AI can't silently skip it. Even if you "remember to check" 95% of the time, the 5% miss is what caused the r7→r18 debt. Make the verification a hard gate, not a mental TODO.
HARD PRECONDITION 2 — independent review-agent over the diff (added 2026-06-25, audit P0 item 3)
METHODOLOGY §5/§7.1: an independent review-agent is the FIRST blocking layer — a green build/test gate is NOT "done". This ship path had NO review (self-review only, §18 self-preference bias), which is exactly how the auto-failover P0 reached stable. Before bumping AppVersion:
git diff --name-only HEAD~1..HEAD # (or the staged diff if not yet committed)
git diff HEAD~1..HEAD
Spawn an INDEPENDENT reviewer with docs/REVIEW_AGENT_PROMPT.md — paste the FULL
diff + invariants verbatim (brief like a new colleague, never "as discussed"):
Agent(subagent_type: "general-purpose",
prompt: <docs/REVIEW_AGENT_PROMPT.md block with the diff pasted in>)
critical/important→ FIX before shipping, then re-review.minor→ opt-in. Real-but-deferred survivors → append toplans/OPEN-DEFECTS.md.
Hotfix short-circuit (§5): skip ONLY for a true ≤5-line, single-surface change with no contract/behaviour drift — and say so in the ship report.
Pre-flight checks
Before bumping anything:
git status— clean tree.dotnet build VPNRouter.sln -c Release→ 0 errors.- Run regression tests:
Expected: all pass.dotnet test VPNRouter.Tests/VPNRouter.Tests.csproj -c Release --no-build \ --filter "FullyQualifiedName~VlessServersResolverTests|FullyQualifiedName~ConfigGeneratorEmptyServersGuardTests|FullyQualifiedName~FreeConfigAggregatorPreserveTests|FullyQualifiedName~MainWindowViewModelCharacterizationTests" - If MainWindowViewModelCharacterizationTests failed with Windows hash drift:
bump
PinnedHashWindowsinVPNRouter.Tests/MainWindowViewModelCharacterizationTests.csBEFORE Step 2 commit. The actual hash appears in the test output asActual: <hex>. - Visual-diff gate (Windows-only — CI can't run this).
VisualDiffTestshas anOperatingSystem.IsWindows()skip guard, so the Linux/macOS CI runners never execute the pixel-diff layer. Baseline drift therefore goes INVISIBLE in CI — it slipped from v2.37 → v2.38 (DpiBypass/Telegram/Tools redesign) and was only caught by a manual run on 2026-06-02. This dev-VM pre-flight is the only place the diff actually runs, so it must run here:
Expected: 3/3 pass (DpiBypass / Telegram / Tools). Run it as its OWN command (not folded into the step-3 filter) — it'sdotnet test VPNRouter.Tests/VPNRouter.Tests.csproj -c Release --no-build \ --filter "FullyQualifiedName~VisualDiffTests"[AvaloniaFact]and the dispatcher-thread shutdown is cleaner when isolated.- Green → proceed.
- Red AND this ship intentionally restyled those pages → the baseline is
stale, not the code. Refresh per
VPNRouter.Tests/CLAUDE.md"Refresh workflow когда страница интенционально меняется": runPageScreenshotTests, EYEBALL the 3 fresh PNGs inVPNRouter.Tests/screenshots/(confirm no clipped controls / overflow),copyeach overVPNRouter.Tests/screenshots/baseline/, re-run this gate until green, and stage the refreshed baseline PNGs in THIS ship's Step-2 commit. - Red AND you did NOT touch those pages → real visual regression (control removed, theme inverted, layout shifted >~2 px). STOP and fix the code before shipping; do NOT re-pin to hide it.
Post-push verification (MANDATORY — was the v2.36.0-r6→r9 lesson)
After Step 2 push lands the commit, the main-branch dotnet test job runs
on Linux/macOS with a DIFFERENT pinned hash (PinnedHashLinux). The
Windows-side bump in pre-flight does NOT cover Linux because MainWindowViewModel
has #if PLATFORM_WINDOWS blocks that change the conditional-stripped surface.
The recurring failure: ship rN → Windows hash bumped locally → commit pushed →
GitHub Releases page shows vX.Y.Z-rN with all assets green → declare success →
but the commits page on main shows a RED X because Linux test failed with
hash-drift, and that debt accumulates over r5→r6→r7→r8→r9 (real history).
Fix workflow — after git push:
# 1. Wait for the dotnet test workflow on the new commit
gh run watch $(gh run list --repo PavelLizunov/VPNRouter --workflow "dotnet test" --limit 1 --json databaseId --jq '.[0].databaseId') --repo PavelLizunov/VPNRouter --exit-status
# 2. If it fails with Linux hash drift, capture the Actual hash:
gh run view <RUN_ID> --repo PavelLizunov/VPNRouter --log-failed 2>&1 | grep "Actual:"
# 3. Bump PinnedHashLinux to that value, commit, push as a follow-up "chore(tests)"
# commit. THIS COMMIT MUST LAND BEFORE you finalize the release as success.
# 4. Re-verify after the follow-up push:
gh api repos/PavelLizunov/VPNRouter/commits/HEAD/check-runs --jq '.check_runs[] | "\(.conclusion // .status) \(.name)"' | sort -u
This is non-negotiable. The visible state on the commits page is what the user sees. Green tag assets + red commit-level test = bad ship UX even if functional.
A clean ship has BOTH:
- Tag release: 14 desktop assets (16 if Android is attached), prerelease=true, previous-stable Latest restored.
- Latest main commit: all check-runs green (build, grep, publish, test, test-update, verify).
Step 1 — bump AppVersion
VPNRouter.Core/AppVersion.cs:
public const string Version = "X.Y.Z-rN"; // CRITICAL: must include -rN suffix
Suffix MUST match release tag exactly. v2.25.0-r1→r2 lesson:
бамп без суффикса → update-check возвращает null. См. CLAUDE.local.md секция
"Урок от v2.25.0-r1 → r2".
Step 2 — commit + push to BOTH remotes
git add <changed files>
git commit -m "feat(vX.Y.Z-rN): <one-line summary>
<details>
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>"
git push github HEAD:main
git push origin HEAD:main
Если Forgejo (origin) недоступен (AmneziaWG VPN флапает) — push в github обязательно, retry origin позже.
Step 3 — Windows build + create release (in background)
powershell -ExecutionPolicy Bypass -File "C:\Project\VPNRouter\build.ps1" -Version "X.Y.Z-rN" -Upload
Это:
- Создаёт тэг
vX.Y.Z-rNна текущем commit - Создаёт GitHub release через
gh release create --latest(build.ps1 ставит--latestautoматически — мы потом снимем) - Уплоадит
VPNRouter-vX.Y.Z-rN-win.zip+ update + 2× sha256
Запускать в background через run_in_background: true — займёт ~90 секунд.
Step 4 — пока Windows билдится, write release notes
Создать plans/release-notes-vX.Y.Z-rN.md:
- Summary: что фикс'нуто (если -r2/r3, mention previous fixes too)
- Test flow для user'а
- Links к commits
- Никаких emoji в файле
Step 5 — Mac + Linux CI auto-triggered
Tag push (от build.ps1) триггерит:
Build macOS DMGworkflowBuild Linux AppImage + .debworkflowPublish APT Repositoryworkflow
Race condition: если Mac CI завершилась ДО того как build.ps1 создал release → "skipping upload" warning. Re-trigger вручную:
gh workflow run "Build macOS DMG" --repo PavelLizunov/VPNRouter --ref vX.Y.Z-rN
Step 6 — finalize release (после Windows build done)
# Mark as prerelease + apply notes
gh release edit vX.Y.Z-rN --repo PavelLizunov/VPNRouter \
--prerelease \
--title "VPNRouter vX.Y.Z-rN — <one-line summary>" \
--notes-file "plans/release-notes-vX.Y.Z-rN.md"
# Restore previous stable as Latest (build.ps1 took it)
gh release edit vX.Y.Z-PREV --repo PavelLizunov/VPNRouter --latest
Step 7 — delete previous -rN per rolling policy
Только один in-flight prerelease видим за раз. Если был -r1 и сейчас
шипим -r2:
gh release delete vX.Y.Z-r1 --yes --repo PavelLizunov/VPNRouter
Тэг НЕ удаляем — он остаётся в git history.
Step 8 — wait for Mac + Linux CI
Pass run_in_background: true:
gh run watch <mac-run-id> --repo PavelLizunov/VPNRouter --exit-status
gh run watch <linux-run-id> --repo PavelLizunov/VPNRouter --exit-status
Когда оба done → проверить desktop asset count = 14 (4 Win + 4 Mac + 6 Linux):
gh release view vX.Y.Z-rN --repo PavelLizunov/VPNRouter --json assets --jq '.assets | length'
Step 9 — report to user (notification only, не блокирующее)
Кратко:
- OK: tag, prerelease=true, Latest=PREV, 14 desktop assets
- Recovery shortcut + test flow checklist
- Указать "verification gate зелёная — следующее действие cut-stable когда нет regression reports за ~24h"
Known gotchas
- VPN флап во время push в origin → попробовать
git push origin HEAD:mainпосле notification "VPN включил". - PS 5.1 NumericUpDown null bug — если фикс касается UI input — обязательно
int?+?? fallback(см. v2.28.3-r4). - Mac CI race — если Mac success но 0 уплоада, re-trigger через workflow_dispatch.
- Homebrew Cask — на prerelease НЕ обновляется (correct behaviour). На stable cut — autobump через repository_dispatch.
- GitHub REST
/releaseslisting cache lag — послеgh release create+ серииgh release edit(prerelease flag, notes, --latest restore) запись в DB корректна (gh release viewработает, прямой URL открывается), но public listing endpoint (REST/releases?per_page=N— именно его дёргает in-app update check) может зависнуть на 30-60+ минут. Atom feed и HTML release page тоже лагают. Verify after step 6:
Если не виден через 5 минут после finalize → принудительно invalidate через delete+recreate (тег сохраняем):gh api "repos/PavelLizunov/VPNRouter/releases?per_page=10" --jq '.[].tag_name' | grep "vX.Y.Z-rN"
Свежая запись индексируется немедленно. См. v2.28.4-r1 incident 2026-04-27.# 1. Скачать все 14 desktop assets локально mkdir /tmp/r-assets && cd /tmp/r-assets gh release download vX.Y.Z-rN --repo PavelLizunov/VPNRouter # 2. Delete release (tag preserved!) gh release delete vX.Y.Z-rN --repo PavelLizunov/VPNRouter --yes --cleanup-tag=false # 3. Recreate fresh (single create call, no follow-up edits) gh release create vX.Y.Z-rN /tmp/r-assets/* \ --repo PavelLizunov/VPNRouter \ --target main \ --prerelease \ --title "VPNRouter vX.Y.Z-rN — ..." \ --notes-file plans/release-notes-vX.Y.Z-rN.md
NOT to do
- Force-push tag после shipping — поломаешь download links для уже скачавших.
- Skip suffix
-rNв AppVersion — update-check сломается. - Push с
--no-verifyили--no-gpg-signбез явной команды user'а.