Release channels & release-feed (Self-Upgrade ADR 0013)¶
Scope status (post-Scope-Freeze 2026-05-10) — See ARCHITECTURE.md for the canonical 37 MÓDULOs + 7 Test Kinds + DOM/CPOS/PIE-PA safety architecture. ADRs 0014, 0019-0025 cover post-Freeze additions.
Operator-facing reference for the airgap-safe upgrade path that powers the dashboard's Self-Upgrade workflow. Companion to ADR 0013 Self-Upgrade Meraki-style and the upgrade orchestrator runbook (lands in PR-4).
Channels¶
Three channels are published. Operators pick exactly one in Settings → Self-Upgrade → Channel.
| Channel | Pointer file | Use for |
|---|---|---|
recommended |
channels/latest-recommended.json |
Production. Only stable semver tags (vMAJ.MIN.PATCH) ship here. |
rc |
channels/latest-rc.json |
Release candidates (-rc.N suffix). Use to validate before promotion. |
beta |
channels/latest-beta.json |
Early-adopter previews (-beta.N suffix). Expect breakage. |
A given tag is published to exactly one channel — v4.7.0-rc.2
goes only to rc, never to recommended. Only stable semver tags ever
become recommended. The operator never has to reason about which
channel a tag belongs to; the pointer file is the single source of truth.
How an upgrade flows¶
GitHub release.yml on tag push
│
├── build-and-publish (matrix per image)
│ • multi-arch build (amd64 + arm64)
│ • cosign keyless OIDC signature
│ • SBOM (full + slim) + attestation
│ • emits digest-{image} artifact
│
├── release (GitHub Release page + SBOM assets)
│
└── publish-release-feed
• assembles digests into one manifest
• validates against manifest-schema.json
• pushes to release-feed branch:
manifests/<version>.json
channels/latest-<channel>.json
▼
┌─ release-feed branch (machine-readable, no app code) ─┐
│ manifests/v4.6.0.json ← signed input │
│ channels/latest-recommended.json ← pointer │
└───────────────────────────────────────────────────────┘
▲
│ Cloner Function 7 polls every 5 min
│
Operator side
• Function 7 fetches the channel pointer (small JSON)
• Compares `version` to the running version
• If newer: fetches the referenced manifest
• Verifies cosign signature on EVERY containerImage
using the manifest's signature.* fields
• Surfaces the result in the dashboard
(Settings → Self-Upgrade → Available)
• Operator confirms → orchestrator runs preflight,
takes a Layer-2 backup snapshot, drains, applies,
health-checks. Auto-rollback if health fails.
Manifest format (v1)¶
Every manifest matches
tools/release-feed/manifest-schema.json.
Minimal example:
{
"schemaVersion": 1,
"version": "v4.6.0",
"channel": "recommended",
"releasedAt": "2026-05-08T14:00:00Z",
"containerImages": [
{
"name": "ghcr.io/nollagluiz/web-agent-dashboard",
"digest": "sha256:abcd...",
"platforms": ["linux/amd64", "linux/arm64"]
}
],
"releaseNotesUrl": "https://github.com/nollagluiz/AI_forSE/releases/tag/v4.6.0",
"estimatedUpgradeMinutes": { "min": 15, "max": 30 },
"signature": {
"scheme": "cosign-keyless-oidc",
"certificateIdentityRegexp": "https://github.com/nollagluiz/.+/.github/workflows/release.yml@refs/tags/v.+",
"certificateOidcIssuer": "https://token.actions.githubusercontent.com"
}
}
The containerImages[].digest is the canonical image identifier the
orchestrator pulls by — defeating tag-mutation attacks where someone
re-publishes :v4.6.0 to a different layer set.
rollbackMarker, databaseMigrations, releaseNotesByLang, and
downloadSizeBytes are optional and surfaced in the dashboard when
present. See the schema file for the full field reference.
Verifying a release manually¶
The same cosign verify invocation the operator-side orchestrator runs
under the hood:
TAG=v4.6.0
IMAGE=ghcr.io/nollagluiz/web-agent-dashboard
DIGEST=$(curl -s "https://raw.githubusercontent.com/nollagluiz/AI_forSE/release-feed/manifests/${TAG}.json" \
| jq -r ".containerImages[] | select(.name==\"${IMAGE}\") | .digest")
cosign verify "${IMAGE}@${DIGEST}" \
--certificate-identity-regexp "https://github.com/nollagluiz/.+/.github/workflows/release.yml@refs/tags/v.+" \
--certificate-oidc-issuer https://token.actions.githubusercontent.com
Exit 0 means the signature checks out and the image was built by this repo's release.yml. Anything else: do not deploy.
Publishing a release locally (dry-run)¶
For testing the manifest pipeline outside of a real tag push:
mkdir -p /tmp/digests
echo "ghcr.io/example/web-agent-dashboard:v4.6.0@sha256:$(printf '0%.0s' {1..64})" \
> /tmp/digests/dashboard.ref
node tools/release-feed/manifest-generator.mjs \
--version v4.6.0 \
--owner example \
--repo demo \
--digest-dir /tmp/digests \
--output /tmp/manifest.json
node tools/release-feed/manifest-generator.mjs --validate /tmp/manifest.json
Channel is inferred from the version string — v4.6.0 → recommended,
v4.6.0-rc.1 → rc, v4.7.0-beta.2 → beta.
What lives in the release-feed branch¶
release-feed/
├── README.md (operator-facing landing page)
├── channels/
│ ├── latest-recommended.json ← Function 7 polls this
│ ├── latest-rc.json
│ └── latest-beta.json
└── manifests/
├── v4.5.3.json
├── v4.6.0-rc.1.json
└── v4.6.0.json
The branch is append-only in practice (every tag adds one manifest file). Channel pointer files are the only mutable surface — they get overwritten on each new publish to the same channel. Manifest files are never deleted; old releases stay reachable for rollback.
Do not commit to this branch by hand. It is generated by
.github/workflows/release.yml and any manual edit will be overwritten
on the next tag push.
Pointer file format¶
{
"version": "v4.6.0",
"manifest": "manifests/v4.6.0.json",
"publishedAt": "2026-05-08T14:00:12Z"
}
This file is intentionally tiny so the operator-side polling loop costs ~200 bytes of bandwidth per check.
Roadmap¶
| PR | Lands in |
|---|---|
| PR-1 | Schema + this doc + workflow hookup (current) |
| PR-2 | This file (you are here) |
| PR-3 | Cloner Function 7 — operator-side poller |
| PR-4 | Upgrade orchestrator (preflight → drain → apply) |
| PR-5 | Dashboard "Self-Upgrade" UI |
| PR-6 | Help Center entries (en / pt-BR / es) |
| PR-7 | E2E test harness for the full upgrade loop |