docs(prd): finalize complete release flow
This commit is contained in:
@@ -71,7 +71,7 @@ jobs:
|
|||||||
BOT_BOTTLE_REF: ${{ needs.validate.outputs.sha }}
|
BOT_BOTTLE_REF: ${{ needs.validate.outputs.sha }}
|
||||||
run: |
|
run: |
|
||||||
sh install.sh
|
sh install.sh
|
||||||
bot-bottle --help
|
"$HOME/.local/bin/bot-bottle" --help
|
||||||
- name: Publish release qualification and advance channel
|
- name: Publish release qualification and advance channel
|
||||||
env:
|
env:
|
||||||
BOT_BOTTLE_RELEASE_TOKEN: ${{ secrets.BOT_BOTTLE_RELEASE_TOKEN }}
|
BOT_BOTTLE_RELEASE_TOKEN: ${{ secrets.BOT_BOTTLE_RELEASE_TOKEN }}
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import urllib.error
|
|||||||
import urllib.request
|
import urllib.request
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
|
from .release_bundle import parse_bundle_index
|
||||||
from .release_manifest import ReleaseManifestError
|
from .release_manifest import ReleaseManifestError
|
||||||
from .release_publish import (
|
from .release_publish import (
|
||||||
HTTP_TIMEOUT_SECONDS,
|
HTTP_TIMEOUT_SECONDS,
|
||||||
@@ -37,11 +38,20 @@ def release_version(tag: str) -> tuple[int, int, int, int]:
|
|||||||
"""Return a monotonically comparable version tuple."""
|
"""Return a monotonically comparable version tuple."""
|
||||||
stable = _STABLE_TAG_RE.fullmatch(tag)
|
stable = _STABLE_TAG_RE.fullmatch(tag)
|
||||||
if stable:
|
if stable:
|
||||||
return *(int(value) for value in stable.groups()), 1 << 30
|
return (
|
||||||
|
int(stable.group(1)),
|
||||||
|
int(stable.group(2)),
|
||||||
|
int(stable.group(3)),
|
||||||
|
1 << 30,
|
||||||
|
)
|
||||||
staging = _STAGING_TAG_RE.fullmatch(tag)
|
staging = _STAGING_TAG_RE.fullmatch(tag)
|
||||||
if staging:
|
if staging:
|
||||||
return *(int(value) for value in staging.groups()[:3]), int(
|
return (
|
||||||
staging.group(4))
|
int(staging.group(1)),
|
||||||
|
int(staging.group(2)),
|
||||||
|
int(staging.group(3)),
|
||||||
|
int(staging.group(4)),
|
||||||
|
)
|
||||||
raise ReleaseManifestError(
|
raise ReleaseManifestError(
|
||||||
"release tag must be vX.Y.Z or vX.Y.Z-rc.N")
|
"release tag must be vX.Y.Z or vX.Y.Z-rc.N")
|
||||||
|
|
||||||
@@ -138,9 +148,17 @@ def publish_qualification(
|
|||||||
channel_url = pointer_url(
|
channel_url = pointer_url(
|
||||||
"bot-bottle-channels", channel, "channel.json")
|
"bot-bottle-channels", channel, "channel.json")
|
||||||
|
|
||||||
if remote_bytes(str(pointer["bundle_index_url"])) is None:
|
bundle = remote_bytes(str(pointer["bundle_index_url"]))
|
||||||
|
if bundle is None:
|
||||||
raise ReleaseManifestError(
|
raise ReleaseManifestError(
|
||||||
f"commit bundle does not exist for {pointer['source_commit']}")
|
f"commit bundle does not exist for {pointer['source_commit']}")
|
||||||
|
try:
|
||||||
|
bundle_data = parse_bundle_index(json.loads(bundle))
|
||||||
|
except (json.JSONDecodeError, UnicodeDecodeError) as exc:
|
||||||
|
raise ReleaseManifestError("commit bundle index is malformed") from exc
|
||||||
|
if bundle_data["source_commit"] != pointer["source_commit"]:
|
||||||
|
raise ReleaseManifestError(
|
||||||
|
"commit bundle index does not match the qualified source commit")
|
||||||
existing_release = remote_bytes(release_url)
|
existing_release = remote_bytes(release_url)
|
||||||
if existing_release is not None and existing_release != wanted:
|
if existing_release is not None and existing_release != wanted:
|
||||||
raise ReleaseManifestError(
|
raise ReleaseManifestError(
|
||||||
|
|||||||
+17
@@ -45,6 +45,23 @@ report; macOS reports advisory coverage in its own job. The macOS infra
|
|||||||
container is a singleton, so its job uses a concurrency group and always tears
|
container is a singleton, so its job uses a concurrency group and always tears
|
||||||
the service down.
|
the service down.
|
||||||
|
|
||||||
|
## Artifact publication and promotion
|
||||||
|
|
||||||
|
[`publish-artifacts.yml`](../.gitea/workflows/publish-artifacts.yml) is the
|
||||||
|
manual deployment-test boundary. Dispatch it with any branch, tag, or commit;
|
||||||
|
it resolves that input once, publishes the orchestrator, gateway, Claude,
|
||||||
|
Codex, Pi, and Firecracker artifacts, then publishes only the wheel verified
|
||||||
|
against those identities. Install the reported commit with
|
||||||
|
`BOT_BOTTLE_REF=<sha> sh install.sh`; the installer warns that snapshots may
|
||||||
|
not have completed release qualification.
|
||||||
|
|
||||||
|
[`release.yml`](../.gitea/workflows/release.yml) handles promotion tags.
|
||||||
|
`vX.Y.Z-rc.N` must be reachable from `staging`; `vX.Y.Z` must be reachable
|
||||||
|
from `production`. It runs the complete pre-release workflow, ensures that the
|
||||||
|
same commit bundle is published, verifies a clean install, creates the
|
||||||
|
qualified release pointer, and advances the matching channel. Promotion
|
||||||
|
reuses immutable bundle bytes and never rebuilds them.
|
||||||
|
|
||||||
## Scheduled canary
|
## Scheduled canary
|
||||||
|
|
||||||
[`.gitea/workflows/canaries.yml`](../.gitea/workflows/canaries.yml) runs weekly
|
[`.gitea/workflows/canaries.yml`](../.gitea/workflows/canaries.yml) runs weekly
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
# PRD 0083: Packaged infrastructure artifacts
|
# PRD 0083: Packaged infrastructure artifacts
|
||||||
|
|
||||||
- **Status:** Draft
|
- **Status:** Active
|
||||||
- **Author:** Codex
|
- **Author:** Codex
|
||||||
- **Created:** 2026-07-27
|
- **Created:** 2026-07-27
|
||||||
- **Issue:** #536
|
- **Issue:** #536
|
||||||
@@ -167,7 +167,8 @@ immutable external bundle index under that SHA. The index contains:
|
|||||||
- Claude, Codex, and Pi agent OCI digest references;
|
- Claude, Codex, and Pi agent OCI digest references;
|
||||||
- Firecracker package versions and compressed-file SHA-256 values;
|
- Firecracker package versions and compressed-file SHA-256 values;
|
||||||
- producing workflow/run identity and publication timestamp; and
|
- producing workflow/run identity and publication timestamp; and
|
||||||
- qualification records, if any, naming the test workflow and release tag.
|
- producing workflow provenance. Qualification is recorded separately in an
|
||||||
|
immutable tag pointer so the commit bundle itself never changes.
|
||||||
|
|
||||||
The embedded wheel manifest contains the runtime subset of the same identities.
|
The embedded wheel manifest contains the runtime subset of the same identities.
|
||||||
Publication fails if the embedded manifest and external index differ.
|
Publication fails if the embedded manifest and external index differ.
|
||||||
@@ -234,7 +235,7 @@ The release workflow is triggered by an eligible staging or production tag:
|
|||||||
then if absent; otherwise verify every existing identity and checksum.
|
then if absent; otherwise verify every existing identity and checksum.
|
||||||
4. Install using the published installer and bundle index on clean supported
|
4. Install using the published installer and bundle index on clean supported
|
||||||
hosts, and run the post-install smoke test against the acquired artifacts.
|
hosts, and run the post-install smoke test against the acquired artifacts.
|
||||||
5. Attach a qualification record to the bundle, create the Gitea release, and
|
5. Publish an immutable qualified tag pointer, create the Gitea release, and
|
||||||
advance the matching channel pointer.
|
advance the matching channel pointer.
|
||||||
|
|
||||||
No release artifact or channel update occurs before qualification succeeds.
|
No release artifact or channel update occurs before qualification succeeds.
|
||||||
@@ -256,9 +257,9 @@ and publish them first, then install the reported SHA.
|
|||||||
|
|
||||||
An exact-commit install prints a prominent warning that the snapshot may not
|
An exact-commit install prints a prominent warning that the snapshot may not
|
||||||
have passed release qualification, even if the same SHA is later associated
|
have passed release qualification, even if the same SHA is later associated
|
||||||
with a tag. A tag or channel install suppresses that warning only when the
|
with a tag. A tag or channel install suppresses that warning only when its
|
||||||
external index contains a successful qualification record for that tag and
|
validated pointer is marked qualified and selects the same immutable bundle
|
||||||
the tag selects the same bundle SHA.
|
SHA.
|
||||||
|
|
||||||
The installer downloads the wheel named by the external index, verifies its
|
The installer downloads the wheel named by the external index, verifies its
|
||||||
SHA-256 before invoking pipx or pip, and reports the selected channel/tag,
|
SHA-256 before invoking pipx or pip, and reports the selected channel/tag,
|
||||||
@@ -361,9 +362,10 @@ Firecracker, and Apple Container.
|
|||||||
|
|
||||||
Create and protect `staging` and `production`, then promote one commit from
|
Create and protect `staging` and `production`, then promote one commit from
|
||||||
`main` through both branches. Publish its commit bundle, qualify an RC tag,
|
`main` through both branches. Publish its commit bundle, qualify an RC tag,
|
||||||
perform clean installs through the staging channel, promote the same SHA,
|
perform clean installs through the staging channel, promote the same SHA, and
|
||||||
qualify a stable tag, and only then make the production channel the installer's
|
qualify a stable tag. Until that first stable qualification exists, the
|
||||||
default. The stable promotion must demonstrate that every production artifact
|
installer's production default fails closed because there is no production
|
||||||
|
pointer. The stable promotion must demonstrate that every production artifact
|
||||||
identity matches the staging-qualified bundle.
|
identity matches the staging-qualified bundle.
|
||||||
|
|
||||||
Existing source checkouts use explicit local mode. Existing mutable local
|
Existing source checkouts use explicit local mode. Existing mutable local
|
||||||
|
|||||||
@@ -16,6 +16,37 @@ from bot_bottle.release_qualification import (
|
|||||||
COMMIT = "a" * 40
|
COMMIT = "a" * 40
|
||||||
|
|
||||||
|
|
||||||
|
def bundle() -> bytes:
|
||||||
|
return json.dumps({
|
||||||
|
"schema": 1,
|
||||||
|
"source_commit": COMMIT,
|
||||||
|
"wheel": {
|
||||||
|
"filename": "bot_bottle.whl",
|
||||||
|
"url": (
|
||||||
|
"https://gitea.dideric.is/api/packages/didericis/generic/"
|
||||||
|
f"bot-bottle-builds/{COMMIT}/bot_bottle.whl"
|
||||||
|
),
|
||||||
|
"sha256": "b" * 64,
|
||||||
|
},
|
||||||
|
"oci": {
|
||||||
|
role: f"registry.example/{role}@sha256:{'c' * 64}"
|
||||||
|
for role in (
|
||||||
|
"orchestrator",
|
||||||
|
"gateway",
|
||||||
|
"agent_claude",
|
||||||
|
"agent_codex",
|
||||||
|
"agent_pi",
|
||||||
|
)
|
||||||
|
},
|
||||||
|
"firecracker": {
|
||||||
|
role: {"version": "v1", "sha256": "d" * 64}
|
||||||
|
for role in ("orchestrator", "gateway")
|
||||||
|
},
|
||||||
|
"provenance": {"workflow_run": "run", "published_at": "now"},
|
||||||
|
"qualifications": [],
|
||||||
|
}).encode()
|
||||||
|
|
||||||
|
|
||||||
class ReleaseQualificationTest(unittest.TestCase):
|
class ReleaseQualificationTest(unittest.TestCase):
|
||||||
def test_tag_selects_channel(self) -> None:
|
def test_tag_selects_channel(self) -> None:
|
||||||
self.assertEqual(release_channel("v1.2.3"), "production")
|
self.assertEqual(release_channel("v1.2.3"), "production")
|
||||||
@@ -38,7 +69,7 @@ class ReleaseQualificationTest(unittest.TestCase):
|
|||||||
def test_publishes_bundle_release_and_channel(
|
def test_publishes_bundle_release_and_channel(
|
||||||
self, remote: mock.Mock, upload: mock.Mock,
|
self, remote: mock.Mock, upload: mock.Mock,
|
||||||
) -> None:
|
) -> None:
|
||||||
remote.side_effect = [b"bundle", None, None]
|
remote.side_effect = [bundle(), None, None]
|
||||||
pointer = build_release_pointer(
|
pointer = build_release_pointer(
|
||||||
tag="v1.2.3",
|
tag="v1.2.3",
|
||||||
source_commit=COMMIT,
|
source_commit=COMMIT,
|
||||||
@@ -59,7 +90,7 @@ class ReleaseQualificationTest(unittest.TestCase):
|
|||||||
workflow_run="old-run",
|
workflow_run="old-run",
|
||||||
qualified_at="old-time",
|
qualified_at="old-time",
|
||||||
)
|
)
|
||||||
remote.side_effect = [b"bundle", None, json.dumps(old).encode()]
|
remote.side_effect = [bundle(), None, json.dumps(old).encode()]
|
||||||
pointer = build_release_pointer(
|
pointer = build_release_pointer(
|
||||||
tag="v1.2.3",
|
tag="v1.2.3",
|
||||||
source_commit=COMMIT,
|
source_commit=COMMIT,
|
||||||
|
|||||||
Reference in New Issue
Block a user