chore(release): v0.3.0 #42

Merged
alex merged 1 commit from release-prep/rolling into main 2026-08-06 13:39:18 +02:00

Added

  • deploy: a failed post-deploy probe now restores the previous image AND the previous source tree (#32)

    docker compose up --wait replaces the container and waits second, so by the time the probe can refuse, the predecessor is already destroyed. The deploy now tags the running image before building and, on any failure after that point, restores both halves and verifies each. Restoring only the container would leave the tree at the new tag while the old image runs — the divergence the probe's first arm would then report as success.

Changed

None.

Fixed

  • release: cut to a draft so the publish click can fire the deploy

Removed

None.

Deprecated

None.

Upgrade

None.

### Added - **deploy**: a failed post-deploy probe now restores the previous image AND the previous source tree (#32) `docker compose up --wait` replaces the container and waits second, so by the time the probe can refuse, the predecessor is already destroyed. The deploy now tags the running image before building and, on any failure after that point, restores both halves and verifies each. Restoring only the container would leave the tree at the new tag while the old image runs — the divergence the probe's first arm would then report as success. ### Changed None. ### Fixed - **release**: cut to a draft so the publish click can fire the deploy ### Removed None. ### Deprecated None. ### Upgrade None.
Generated by release-toolkit rt prep.

Tracker: frankenbit/release-toolkit#1
Owner

Before merging this: what deploys, under each configuration

Merging this PR is the v0.3.0 cut. Recorded here rather than only on #39, because this is where the
decision gets made. Full analysis: #39.

AS CONFIGURED TODAY
  no RELEASE_TOOLKIT_TOKEN · publish_mode: immediate
  → cut publishes with the Actions token → no cascade → NO deploy
  → this is the current defect. Safe, and the pipeline stays unproven.

TOKEN SET, publish_mode still immediate
  → cut publishes with the PAT → cascades → DEPLOYS, unattended, no human gate
  → n=16 behind this path. It is the one configuration we have real evidence for,
    and it is the one nobody wants.

publish_mode: draft  (with or without the token)
  → cut creates a DRAFT · no publish event · no deploy
  → operator clicks Publish → that human action is what fires the deploy
  → ⚠️ THIS TRANSITION HAS NO SUPPORTING CELL. See below.

The one thing to know before choosing draft

The n=16 does not reach it. That evidence is PAT-created releases cascade — a creation
transition. Under draft, the deploy fires from a human publishing an existing draft, which is a
different transition with no observed instance anywhere on this instance.

And there is a specific plausible failure mode, not just missing evidence: GitHub distinguishes the
published and released release-event actions, and they do not fire identically for
draft-then-publish versus create-published. If Forgejo mirrors that split, deploy.yml's
types: [published] misses the draft transition and the fix silently does nothing.

Cheap way to settle it first — proposed, not run

Create a draft release on purser against a deliberately invalid tag (test-draft), then publish it.

deploy fires  → refuses at the tag-shape regex, step one, nothing mutates — cascade PROVEN
nothing runs  → the draft fix does not work, learned before changing any config

Safety: validate release tag shape is the first step, TAG arrives via env: not interpolation,
and the regex miss exit 1s under set -euo pipefail — checkout is at :90 and compose at :103,
both downstream. Verified independently by three chambers. Residual: a junk tag and release on
purser, both deletable. It mutates repo state rather than only producing a run, so it wants a ruling.

Standing caveat

No purser cut has ever been observed deploying. Five cells across two repos support the model;
purser's own success is predicted, not measured.

## Before merging this: what deploys, under each configuration Merging this PR is the v0.3.0 cut. Recorded here rather than only on #39, because this is where the decision gets made. Full analysis: #39. ``` AS CONFIGURED TODAY no RELEASE_TOOLKIT_TOKEN · publish_mode: immediate → cut publishes with the Actions token → no cascade → NO deploy → this is the current defect. Safe, and the pipeline stays unproven. TOKEN SET, publish_mode still immediate → cut publishes with the PAT → cascades → DEPLOYS, unattended, no human gate → n=16 behind this path. It is the one configuration we have real evidence for, and it is the one nobody wants. publish_mode: draft (with or without the token) → cut creates a DRAFT · no publish event · no deploy → operator clicks Publish → that human action is what fires the deploy → ⚠️ THIS TRANSITION HAS NO SUPPORTING CELL. See below. ``` ### The one thing to know before choosing `draft` **The n=16 does not reach it.** That evidence is *PAT-created releases cascade* — a **creation** transition. Under `draft`, the deploy fires from a human publishing an existing draft, which is a different transition with **no observed instance anywhere on this instance.** And there is a specific plausible failure mode, not just missing evidence: GitHub distinguishes the `published` and `released` release-event actions, and they do not fire identically for draft-then-publish versus create-published. **If Forgejo mirrors that split, `deploy.yml`'s `types: [published]` misses the draft transition and the fix silently does nothing.** ### Cheap way to settle it first — proposed, not run Create a draft release on purser against a deliberately invalid tag (`test-draft`), then publish it. ``` deploy fires → refuses at the tag-shape regex, step one, nothing mutates — cascade PROVEN nothing runs → the draft fix does not work, learned before changing any config ``` Safety: `validate release tag shape` is the first step, `TAG` arrives via `env:` not interpolation, and the regex miss `exit 1`s under `set -euo pipefail` — checkout is at `:90` and compose at `:103`, both downstream. Verified independently by three chambers. Residual: a junk tag and release on purser, both deletable. It mutates repo state rather than only producing a run, so it wants a ruling. ### Standing caveat **No purser cut has ever been observed deploying.** Five cells across two repos support the model; purser's own success is predicted, not measured.
Owner

Update — the deciding step is measured, and the rollback net is armed for this cut

Two things changed since the note above, both in the safe direction.

1. The unmeasured step is now measured

@quartermaster ran it in a disposable repo:

arm 0  push                        FIRED    ← liveness control
arm A  draft CREATED               NO RUN   ← negative control: a draft emits no `published`
arm B  draft PUBLISHED             FIRED    ← the cell. Run count 1 → 1 → 2.

A human publishing an existing draft does cascade, against a workflow filtering on exactly
types: [published] — so the failure mode I flagged (Forgejo emitting released instead, and the
filter missing it) is excluded, not merely unobserved.

Residual he named himself: he published via PATCH draft:false with a user token rather than
clicking Publish in the UI. Probably identical underneath; one step removed.

2. #38 merged, so this cut will carry the rollback

Verified on main at a0362f7:

scripts/deploy-rollback.sh          PRESENT on main
deploy.yml references it            5×
:113/:116   ROLLBACK_ARMED=1 / =0   armed only when the pre-deploy capture succeeded
:145–147    install -m 0755 scripts/deploy-rollback.sh "$RUNNER_TEMP/…"
                                    staged OUT of the tree before checkout, so a rollback
                                    cannot delete the script bash is mid-read of
:151        ::warning::             fires if the tag carries no rollback script
:390/:395   refuses if not armed, and prints the by-hand restore

v0.3.0 cuts from main, which now includes a0362f7. So the tag will contain the rollback
script and the net is armed
— unlike v0.1.0 and v0.2.0, neither of which carried it.

What that means for merging this

With purser#43 in (publish_mode: draft), merging this cut is:

cut lands → DRAFT release, no publish event, NO deploy
operator clicks Publish → deploy fires, attended, with the rollback net armed

That is the attended first deploy the crew has been arranging since 11:30 — and it doubles as the
last open cell, on purser itself rather than a synthetic repo, which retires the cross-repo residual
(@surveyor's point).

⚠️ #43 must merge before this cut, or the cut publishes immediately under the Actions token and
you get the current silent no-deploy.

## Update — the deciding step is measured, and the rollback net is armed for this cut Two things changed since the note above, both in the safe direction. ### 1. The unmeasured step is now measured @quartermaster ran it in a disposable repo: ``` arm 0 push FIRED ← liveness control arm A draft CREATED NO RUN ← negative control: a draft emits no `published` arm B draft PUBLISHED FIRED ← the cell. Run count 1 → 1 → 2. ``` **A human publishing an existing draft does cascade**, against a workflow filtering on exactly `types: [published]` — so the failure mode I flagged (Forgejo emitting `released` instead, and the filter missing it) is **excluded**, not merely unobserved. Residual he named himself: he published via `PATCH draft:false` with a user token rather than clicking Publish in the UI. Probably identical underneath; one step removed. ### 2. `#38` merged, so this cut will carry the rollback Verified on `main` at `a0362f7`: ``` scripts/deploy-rollback.sh PRESENT on main deploy.yml references it 5× :113/:116 ROLLBACK_ARMED=1 / =0 armed only when the pre-deploy capture succeeded :145–147 install -m 0755 scripts/deploy-rollback.sh "$RUNNER_TEMP/…" staged OUT of the tree before checkout, so a rollback cannot delete the script bash is mid-read of :151 ::warning:: fires if the tag carries no rollback script :390/:395 refuses if not armed, and prints the by-hand restore ``` **v0.3.0 cuts from `main`, which now includes `a0362f7`. So the tag will contain the rollback script and the net is armed** — unlike v0.1.0 and v0.2.0, neither of which carried it. ### What that means for merging this With `purser#43` in (`publish_mode: draft`), merging this cut is: ``` cut lands → DRAFT release, no publish event, NO deploy operator clicks Publish → deploy fires, attended, with the rollback net armed ``` That is the attended first deploy the crew has been arranging since 11:30 — and it doubles as the last open cell, on purser itself rather than a synthetic repo, which retires the cross-repo residual (@surveyor's point). ⚠️ **`#43` must merge before this cut**, or the cut publishes immediately under the Actions token and you get the current silent no-deploy.
forgejo-actions force-pushed release-prep/rolling from aea8fc1967 to 1d7da9e8e9
All checks were successful
go-ci / lint + build + test (push) Successful in 50s
release / decide + act (push) Successful in 5s
release / release (push) Successful in 0s
deploy / deploy purser to /srv/docker/purser (release) Successful in 25s
2026-08-06 12:50:41 +02:00
Compare
alex merged commit 1d7da9e8e9 into main 2026-08-06 13:39:18 +02:00
alex deleted branch release-prep/rolling 2026-08-06 13:39:18 +02:00
Sign in to join this conversation.
No description provided.