Skip to main content
← Back to list
01Issue
FeatureShippedSwamp Club
Assigneesstack72

Relationships

#608 API: Add release channel support for extension versions (beta, rc, stable)

Opened by stack72 · 6/10/2026· Shipped 6/10/2026

Context

This is the API/platform side of issue #311 — "Publish release-candidate / unstable extension versions." The CLI side will be implemented separately in the swamp repo after these API changes are deployed.

Problem

Every extension push produces a stable, immediately-resolvable version. There is no way to publish candidate or preview releases that default consumers skip. This makes iterative CI/CD work and pre-release validation difficult — pushing a candidate contaminates the stable channel, and there is no way to promote a tested candidate to stable without re-pushing.

Design

Three fixed release channels with a strict promotion ladder:

beta → rc → stable
  └──────────┘  (can skip rc)
  • stable — the default. All existing versions are stable. Omitting the channel parameter means stable.
  • rc — release candidate. Opt-in only.
  • beta — early preview. Opt-in only.

Key invariants

  • A version string (CalVer: YYYY.MM.DD.MICRO) is globally unique per extension regardless of channel. You cannot push 2026.06.10.1 as both beta and stable. This enables promotion as a metadata-only operation.
  • Promotion is forward-only: beta→rc, beta→stable, rc→stable. Backward transitions (stable→rc, rc→beta, stable→beta) are rejected by the server.
  • Backward compatibility: all endpoints must continue to work exactly as they do today when no channel parameter is provided. Existing CLI versions that do not send a channel field must see no behavior change.

Required API Changes

1. Database: Add channel column to extension versions

Add a channel column (enum or string: beta, rc, stable) to the extension versions table. Default value: stable.

Migration: Backfill all existing version rows with channel = 'stable'.

The column should be indexed — it will be used in queries for "latest version in channel X."

2. Push endpoints: Accept optional channel field

POST /api/v1/extensions/push (initiate)

Accept an optional channel field in the request body alongside the existing PushMetadata fields:

{
  "name": "@collective/extension",
  "version": "2026.06.10.1",
  "description": "...",
  "channel": "beta"
}
  • channel is optional. If omitted, default to "stable".
  • Validate that channel is one of: "beta", "rc", "stable". Reject unknown values with 400.
  • Version uniqueness check remains global (not per-channel). If 2026.06.10.1 already exists in any channel, reject with 409 Conflict as today.

POST /api/v1/extensions/confirm

Same — accept and persist the channel field from the confirm request body. The channel value should match what was sent in initiate (or default to stable if not sent).

3. GET /latest: Accept optional ?channel= query param

GET /api/v1/extensions/{name}/latest

Current behavior: returns the latest published version (which is implicitly stable since all existing versions are stable).

New behavior:

  • GET /latest (no param) → returns latest stable version (unchanged behavior)
  • GET /latest?channel=stable → same as no param
  • GET /latest?channel=rc → returns latest version with channel=rc, or 404 if none
  • GET /latest?channel=beta → returns latest version with channel=beta, or 404 if none

"Latest" means the highest version by CalVer ordering within the specified channel.

Response should include the channel in the response body:

{
  "latestVersion": "2026.06.10.1",
  "latestVersionDetail": {
    "version": "2026.06.10.1",
    "publishedAt": "2026-06-10T12:00:00Z",
    "channel": "beta"
  }
}

4. GET /versions: New endpoint for version history

GET /api/v1/extensions/{name}/versions (new endpoint)

Returns a paginated list of all published versions for an extension, with channel metadata.

Query params:

  • channel[] — collect parameter, filters to specific channels. Multiple allowed: ?channel[]=rc&channel[]=beta. If omitted, returns all versions (all channels).
  • perPage — pagination (default 20, max 100)
  • page — page number (default 1)

Response:

{
  "versions": [
    {
      "version": "2026.06.10.3",
      "channel": "beta",
      "publishedAt": "2026-06-10T15:00:00Z"
    },
    {
      "version": "2026.06.10.2",
      "channel": "rc",
      "publishedAt": "2026-06-10T14:00:00Z"
    },
    {
      "version": "2026.06.10.1",
      "channel": "stable",
      "publishedAt": "2026-06-10T12:00:00Z"
    }
  ],
  "meta": {
    "total": 3,
    "page": 1,
    "perPage": 20
  }
}

Versions should be ordered by CalVer descending (newest first).

5. Search: Accept optional ?channel[]= filter

GET /api/v1/extensions/search

Current behavior: returns extensions with their latestVersion field.

New behavior:

  • No channel param → returns extensions showing only stable latest version (unchanged)
  • ?channel[]=rc → returns extensions that have at least one version in the rc channel. The latestVersion in results should reflect the latest version in the queried channel(s).
  • ?channel[]=rc&channel[]=beta → returns extensions with versions in either rc or beta channels

Validate channel values — reject unknown values with 400.

6. GET extension info: Add per-channel latest versions

GET /api/v1/extensions/{name}

Add optional fields to the response alongside the existing latestVersion:

{
  "latestVersion": "2026.06.10.1",
  "latestRc": "2026.06.10.2",
  "latestBeta": "2026.06.10.3"
}
  • latestVersion — latest stable version (unchanged, backward compatible)
  • latestRc — latest rc version, or omitted/null if no rc versions exist
  • latestBeta — latest beta version, or omitted/null if no beta versions exist

7. POST /promote: New endpoint

POST /api/v1/extensions/{name}/promote

Request body:

{
  "version": "2026.06.10.1",
  "toChannel": "rc"
}

Validation:

  1. Extension must exist → 404 if not
  2. Version must exist → 404 if not
  3. Caller must own the extension (same auth as push) → 403 if not
  4. toChannel must be one of "rc", "stable" → 400 if not
  5. Promotion ladder: the version's current channel must be strictly lower than toChannel:
    • beta → rc ✅
    • beta → stable ✅
    • rc → stable ✅
    • stable → rc ❌ (400: "Cannot demote a stable version to rc")
    • stable → beta ❌ (400: "Cannot demote a stable version to beta")
    • rc → beta ❌ (400: "Cannot demote an rc version to beta")
    • same → same ❌ (400: "Version is already in channel X")

On success:

  • Update the version's channel to toChannel
  • Recalculate per-channel latest: if the promoted version is newer (by CalVer) than the current latest in the target channel, update accordingly
  • Return 200 with the updated version info:
{
  "name": "@collective/extension",
  "version": "2026.06.10.1",
  "previousChannel": "beta",
  "channel": "rc",
  "message": "Version 2026.06.10.1 promoted from beta to rc"
}

8. Download and checksum endpoints (no changes needed)

GET /api/v1/extensions/{name}@{version}/download and GET /api/v1/extensions/{name}@{version}/checksum need no changes — these are version-specific and versions are globally unique regardless of channel.


Testing

  • Push with channel field → version stored with correct channel
  • Push without channel field → version stored as stable (backward compat)
  • Push same version twice in different channels → 409 Conflict (version globally unique)
  • GET /latest with no param → returns stable latest
  • GET /latest?channel=rc → returns rc latest or 404
  • GET /versions with no filter → returns all versions
  • GET /versions?channel[]=rc&channel[]=beta → returns only rc and beta versions
  • Search with no channel → returns stable-only results
  • Search with channel filter → returns matching results
  • GET extension info → includes latestRc and latestBeta when they exist
  • Promote beta→rc → succeeds, updates channel
  • Promote beta→stable → succeeds (skip rc)
  • Promote rc→stable → succeeds
  • Promote stable→rc → 400 rejection
  • Promote rc→beta → 400 rejection
  • Promote to same channel → 400 rejection
  • Promote recalculates per-channel latest correctly
  • Promote requires auth (same as push)
  • Backward compat: existing clients that don't send channel work unchanged

Deployment Notes

These API changes must be deployed before the CLI changes in swamp#311 ship, since the CLI will depend on the new endpoints and query params. The changes are designed to be fully backward compatible — deploying the API first introduces no breaking changes for existing CLI versions.

02Bog Flow
OPENTRIAGEDIN PROGRESSSHIPPED+ 1 MOREASSIGNED+ 5 MOREREVIEW+ 3 MOREPR_MERGED+ 1 MORENOTIFICATION_SKIPPED

Shipped

6/10/2026, 7:12:43 PM

Click a lifecycle step above to view its details.

03Sludge Pulse
stack72 assigned stack726/10/2026, 1:14:18 PM

Sign in to post a ripple.