Skip to content

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.0recommended, v4.6.0-rc.1rc, v4.7.0-beta.2beta.

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