From 382e765451a3613452d3f493af8613c21e9bfb5f Mon Sep 17 00:00:00 2001 From: guibeira Date: Thu, 30 Jul 2026 14:59:49 -0300 Subject: [PATCH 1/7] Revert "fix(release): trigger alpha tag publish (#633)" This reverts commit 45b212faedfb7c66c60f35b77183fe33994271b7. --- .github/workflows/alpha-release.yml | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/.github/workflows/alpha-release.yml b/.github/workflows/alpha-release.yml index 444478725..23c14ce02 100644 --- a/.github/workflows/alpha-release.yml +++ b/.github/workflows/alpha-release.yml @@ -182,10 +182,7 @@ jobs: git config user.name "workers-ci[bot]" git config user.email "workers-ci[bot]@users.noreply.github.com" git add -A - # This commit is the target of the tag push. Do not add `[skip ci]`: - # GitHub would then suppress the Release workflow that publishes the - # new alpha tag to the registry. - git commit -m "chore(${WORKER}): alpha v${VERSION}" + git commit -m "chore(${WORKER}): alpha v${VERSION} [skip ci]" git tag -a "$TAG" -m "Alpha release $TAG worker: $WORKER From c117cebcee892c4a7d82adf873b44df3b3a1d128 Mon Sep 17 00:00:00 2001 From: guibeira Date: Thu, 30 Jul 2026 14:59:49 -0300 Subject: [PATCH 2/7] Revert "fix(release): trigger alpha publish pipeline (#632)" This reverts commit 4f1b339b625353b998321ba615e94b7159456f8f. --- .github/workflows/alpha-release.yml | 11 ----------- 1 file changed, 11 deletions(-) diff --git a/.github/workflows/alpha-release.yml b/.github/workflows/alpha-release.yml index 23c14ce02..5dbec9e9d 100644 --- a/.github/workflows/alpha-release.yml +++ b/.github/workflows/alpha-release.yml @@ -80,21 +80,10 @@ jobs: timeout-minutes: 10 steps: - # Tags pushed with GITHUB_TOKEN do not trigger the Release workflow. - # Use the CI GitHub App token so the annotated alpha tag starts the - # build-and-publish pipeline after this workflow completes. - - name: Generate token - id: generate_token - uses: actions/create-github-app-token@v3 - with: - app-id: ${{ secrets.III_CI_APP_ID }} - private-key: ${{ secrets.III_CI_APP_PRIVATE_KEY }} - - name: Checkout selected branch uses: actions/checkout@v5 with: fetch-depth: 0 - token: ${{ steps.generate_token.outputs.token }} - name: Refuse to release main env: From e1316fab8b401c54e1e1cdc69c10f339a187de5d Mon Sep 17 00:00:00 2001 From: guibeira Date: Thu, 30 Jul 2026 14:59:49 -0300 Subject: [PATCH 3/7] Revert "fix(ci): simplify alpha release inputs (#628)" This reverts commit 72a6595440dd0338be301de4eb555a076dede8ef. --- .github/workflows/alpha-release.yml | 33 +++++++++++++++++++---------- docs/sops/release.md | 11 +++++----- 2 files changed, 28 insertions(+), 16 deletions(-) diff --git a/.github/workflows/alpha-release.yml b/.github/workflows/alpha-release.yml index 5dbec9e9d..6f61fa864 100644 --- a/.github/workflows/alpha-release.yml +++ b/.github/workflows/alpha-release.yml @@ -6,12 +6,16 @@ name: Alpha Release # # The regular release workflow handles this tag normally, producing a GitHub # prerelease and publishing the worker to the isolated `experimental` channel. -# Run this workflow from the feature branch to release. GitHub Actions' branch -# selector chooses the source; the only workflow input is the worker to publish. +# Run this workflow from main and provide the feature branch or pull-request +# ref to release. That lets it test PRs created before this workflow existed. on: workflow_dispatch: inputs: + source_ref: + description: 'Feature branch or pull-request ref (e.g. feat/my-worker or refs/pull/123/head)' + required: true + type: string worker: description: 'Worker module to publish from this branch' required: true @@ -65,6 +69,16 @@ on: - scrapling - web - worktree + bump: + description: 'Base version for the alpha (none = use the manifest version as its base)' + required: true + type: choice + options: + - patch + - minor + - major + - none + default: patch permissions: contents: write @@ -80,21 +94,17 @@ jobs: timeout-minutes: 10 steps: - - name: Checkout selected branch + - name: Checkout source ref uses: actions/checkout@v5 with: fetch-depth: 0 + ref: ${{ inputs.source_ref }} - name: Refuse to release main env: - REF_TYPE: ${{ github.ref_type }} - SOURCE_BRANCH: ${{ github.ref_name }} + SOURCE_REF: ${{ inputs.source_ref }} run: | set -euo pipefail - if [[ "$REF_TYPE" != "branch" ]]; then - echo "::error::Alpha Release must run from a feature branch" - exit 1 - fi git fetch origin main source_sha=$(git rev-parse HEAD) main_sha=$(git rev-parse origin/main) @@ -102,7 +112,7 @@ jobs: echo "::error::Alpha Release is for feature branches; use Create Tag for main releases" exit 1 fi - echo "::notice::Preparing alpha release from $SOURCE_BRANCH at $source_sha" + echo "::notice::Preparing alpha release from $SOURCE_REF at $source_sha" - name: Discover manifest id: meta @@ -125,11 +135,12 @@ jobs: id: version env: WORKER: ${{ inputs.worker }} + BUMP: ${{ inputs.bump }} MANIFEST: ${{ steps.meta.outputs.manifest }} run: | set -euo pipefail version=$(python3 .github/scripts/manifest_version.py bump "$WORKER/$MANIFEST" \ - --kind none --suffix alpha --worker "$WORKER") + --kind "$BUMP" --suffix alpha --worker "$WORKER") echo "version=$version" >> "$GITHUB_OUTPUT" echo "tag=${WORKER}/v${version}" >> "$GITHUB_OUTPUT" echo "::notice::${WORKER}: alpha ${version}@experimental" diff --git a/docs/sops/release.md b/docs/sops/release.md index 09a3816c9..cc45f3dd2 100644 --- a/docs/sops/release.md +++ b/docs/sops/release.md @@ -178,9 +178,10 @@ counter — `parse_release_tag.py` detects prereleases as `-.`. ### Alpha release from a pull request branch -To publish a worker from an unmerged pull request for integration testing, open -**Actions → Alpha Release**, select the pull request branch in **Use workflow -from**, then choose the worker. The workflow creates an ephemeral commit with an +To publish a worker from an unmerged pull request for integration testing, use +**Actions → Alpha Release** from `main`. Set **Source ref** to the pull request +branch (or `refs/pull//head`), then choose the worker and the intended +base-version bump. The workflow creates an ephemeral commit with an `-alpha.N` manifest version, then pushes only its annotated tag, for example `browser/v1.4.0-alpha.1`. @@ -189,8 +190,8 @@ the `experimental` registry channel (`browser@experimental`). Neither the selected branch nor `main` is pushed or changed. The channel is shared: a new alpha release for the same worker moves `experimental` to that version. -The selected branch must not be `main`; use **Create Tag** for a release that -should move `latest` or `next`. +**Source ref** must not resolve to `main`; use **Create Tag** for a release +that should move `latest` or `next`. ### Dry run From c4234d9658b8f6b3efc5047bd064ab202fdd5345 Mon Sep 17 00:00:00 2001 From: guibeira Date: Thu, 30 Jul 2026 14:59:49 -0300 Subject: [PATCH 4/7] Revert "feat(release): add alpha releases from pull request branches (#626)" This reverts commit ac0f75076b81071fbc0a81f161e6d101d633a2fb. --- .github/workflows/alpha-release.yml | 195 ---------------------------- docs/sops/release.md | 17 --- 2 files changed, 212 deletions(-) delete mode 100644 .github/workflows/alpha-release.yml diff --git a/.github/workflows/alpha-release.yml b/.github/workflows/alpha-release.yml deleted file mode 100644 index 6f61fa864..000000000 --- a/.github/workflows/alpha-release.yml +++ /dev/null @@ -1,195 +0,0 @@ -name: Alpha Release - -# Publishes a worker from a feature branch for integration testing without -# touching the branch or main. The tag points at an ephemeral commit whose -# manifest is bumped to an alpha version; only the tag is pushed. -# -# The regular release workflow handles this tag normally, producing a GitHub -# prerelease and publishing the worker to the isolated `experimental` channel. -# Run this workflow from main and provide the feature branch or pull-request -# ref to release. That lets it test PRs created before this workflow existed. - -on: - workflow_dispatch: - inputs: - source_ref: - description: 'Feature branch or pull-request ref (e.g. feat/my-worker or refs/pull/123/head)' - required: true - type: string - worker: - description: 'Worker module to publish from this branch' - required: true - type: choice - options: - - acp - - approval-gate - - bridge - - browser - - claude-code - - codex - - devin - - console - - grok - - context-manager - - cron - - database - - email - - eval - - harness - - hermes - - http - - iii-directory - - lsp - - image-resize - - llm-router - - fp - - github - - mcp - - memory - - memory-consolidate - - opencode - - pi - - provider-anthropic - - provider-claude-code - - provider-kimi - - provider-llamacpp - - provider-openai - - provider-openai-codex - - provider-xai - - provider-zai - - pubsub - - queue - - rbac-proxy - - session-manager - - slack - - telegram-bot - - shell - - state - - storage - - scrapling - - web - - worktree - bump: - description: 'Base version for the alpha (none = use the manifest version as its base)' - required: true - type: choice - options: - - patch - - minor - - major - - none - default: patch - -permissions: - contents: write - -concurrency: - group: alpha-release-${{ inputs.worker }} - cancel-in-progress: false - -jobs: - prepare: - name: Bump and tag alpha - runs-on: ubuntu-latest - timeout-minutes: 10 - - steps: - - name: Checkout source ref - uses: actions/checkout@v5 - with: - fetch-depth: 0 - ref: ${{ inputs.source_ref }} - - - name: Refuse to release main - env: - SOURCE_REF: ${{ inputs.source_ref }} - run: | - set -euo pipefail - git fetch origin main - source_sha=$(git rev-parse HEAD) - main_sha=$(git rev-parse origin/main) - if [[ "$source_sha" == "$main_sha" ]]; then - echo "::error::Alpha Release is for feature branches; use Create Tag for main releases" - exit 1 - fi - echo "::notice::Preparing alpha release from $SOURCE_REF at $source_sha" - - - name: Discover manifest - id: meta - env: - WORKER: ${{ inputs.worker }} - run: | - set -euo pipefail - if [[ ! -f "$WORKER/iii.worker.yaml" ]]; then - echo "::error::$WORKER/iii.worker.yaml is missing" - exit 1 - fi - manifest=$(grep '^manifest:' "$WORKER/iii.worker.yaml" | head -n1 | awk '{print $2}') - if [[ -z "$manifest" ]]; then - echo "::error::$WORKER/iii.worker.yaml has no 'manifest' key" - exit 1 - fi - echo "manifest=$manifest" >> "$GITHUB_OUTPUT" - - - name: Calculate and write alpha version - id: version - env: - WORKER: ${{ inputs.worker }} - BUMP: ${{ inputs.bump }} - MANIFEST: ${{ steps.meta.outputs.manifest }} - run: | - set -euo pipefail - version=$(python3 .github/scripts/manifest_version.py bump "$WORKER/$MANIFEST" \ - --kind "$BUMP" --suffix alpha --worker "$WORKER") - echo "version=$version" >> "$GITHUB_OUTPUT" - echo "tag=${WORKER}/v${version}" >> "$GITHUB_OUTPUT" - echo "::notice::${WORKER}: alpha ${version}@experimental" - - - name: Validate manifest update - env: - WORKER: ${{ inputs.worker }} - MANIFEST: ${{ steps.meta.outputs.manifest }} - VERSION: ${{ steps.version.outputs.version }} - run: | - set -euo pipefail - python3 .github/scripts/manifest_version.py verify \ - "$WORKER/$MANIFEST" --expected "$VERSION" - - - name: Sync Cargo.lock to alpha version - env: - WORKER: ${{ inputs.worker }} - MANIFEST: ${{ steps.meta.outputs.manifest }} - run: | - set -euo pipefail - python3 .github/scripts/manifest_version.py sync-lock "$WORKER/$MANIFEST" - - - name: Check tag does not exist - env: - TAG: ${{ steps.version.outputs.tag }} - run: | - if git rev-parse "$TAG" >/dev/null 2>&1; then - echo "::error::Tag $TAG already exists" - exit 1 - fi - - - name: Commit alpha version and push only its tag - env: - TAG: ${{ steps.version.outputs.tag }} - WORKER: ${{ inputs.worker }} - VERSION: ${{ steps.version.outputs.version }} - run: | - set -euo pipefail - git config user.name "workers-ci[bot]" - git config user.email "workers-ci[bot]@users.noreply.github.com" - git add -A - git commit -m "chore(${WORKER}): alpha v${VERSION} [skip ci]" - git tag -a "$TAG" -m "Alpha release $TAG - - worker: $WORKER - version: $VERSION - registry-tag: experimental - " - # The ephemeral commit is reachable only through this tag. Never - # push the selected feature branch or main from this workflow. - git push origin "$TAG" - echo "::notice::Published $TAG@experimental" diff --git a/docs/sops/release.md b/docs/sops/release.md index cc45f3dd2..88208e49b 100644 --- a/docs/sops/release.md +++ b/docs/sops/release.md @@ -176,23 +176,6 @@ way the GitHub Release is marked prerelease and still builds and publishes (unless `interface_smoke: false`). A hand-pushed tag must carry the `.N` counter — `parse_release_tag.py` detects prereleases as `-.`. -### Alpha release from a pull request branch - -To publish a worker from an unmerged pull request for integration testing, use -**Actions → Alpha Release** from `main`. Set **Source ref** to the pull request -branch (or `refs/pull//head`), then choose the worker and the intended -base-version bump. The workflow creates an ephemeral commit with an -`-alpha.N` manifest version, then pushes only its annotated tag, for example -`browser/v1.4.0-alpha.1`. - -The release pipeline publishes that tag as a GitHub prerelease and assigns it -the `experimental` registry channel (`browser@experimental`). Neither the -selected branch nor `main` is pushed or changed. The channel is shared: a new -alpha release for the same worker moves `experimental` to that version. - -**Source ref** must not resolve to `main`; use **Create Tag** for a release -that should move `latest` or `next`. - ### Dry run Tag shape: `/vX.Y.Z-dry-run.1` (parsed by `parse_release_tag.py`). From f027df0308d109780fcfe7d3631268dc51d55061 Mon Sep 17 00:00:00 2001 From: guibeira Date: Thu, 30 Jul 2026 14:59:51 -0300 Subject: [PATCH 5/7] Revert "fix(release): isolate alpha releases (#631)" This reverts commit 156f5dd0dd561c8607f1a80a6049d67fc253993b. --- .github/workflows/create-tag.yml | 21 ++++++--------------- docs/sops/release.md | 21 +++++++++------------ 2 files changed, 15 insertions(+), 27 deletions(-) diff --git a/.github/workflows/create-tag.yml b/.github/workflows/create-tag.yml index a3d2a9622..e90ba76ca 100644 --- a/.github/workflows/create-tag.yml +++ b/.github/workflows/create-tag.yml @@ -81,7 +81,7 @@ on: - stable default: none tag: - description: 'Registry channel (alpha releases are always published to experimental)' + description: 'Registry channel the version is published under (passed to POST /publish; not part of the git tag name)' required: true type: choice options: @@ -159,7 +159,6 @@ jobs: WORKER: ${{ inputs.worker }} BUMP: ${{ inputs.bump }} SUFFIX: ${{ inputs.suffix }} - REQUESTED_REGISTRY_TAG: ${{ inputs.tag }} MANIFEST: ${{ steps.meta.outputs.manifest }} run: | set -euo pipefail @@ -168,18 +167,10 @@ jobs: # (checkout above is fetch-depth 0, so they are all present). new_ver=$(python3 .github/scripts/manifest_version.py bump "$WORKER/$MANIFEST" \ --kind "$BUMP" --suffix "$SUFFIX" --worker "$WORKER") - registry_tag="$REQUESTED_REGISTRY_TAG" - if [[ "$SUFFIX" == "alpha" ]]; then - registry_tag="experimental" - echo "::notice::alpha releases always publish to the experimental channel" - fi - { - echo "current=$current" - echo "version=$new_ver" - echo "tag=${WORKER}/v${new_ver}" - echo "registry_tag=$registry_tag" - } >> "$GITHUB_OUTPUT" - echo "::notice::${WORKER}: ${current} -> ${new_ver} (channel: $registry_tag)" + echo "current=$current" >> "$GITHUB_OUTPUT" + echo "version=$new_ver" >> "$GITHUB_OUTPUT" + echo "tag=${WORKER}/v${new_ver}" >> "$GITHUB_OUTPUT" + echo "::notice::${WORKER}: ${current} -> ${new_ver} (channel: ${{ inputs.tag }})" - name: Validate manifest update env: @@ -235,7 +226,7 @@ jobs: - name: Create and push annotated tag env: TAG: ${{ steps.versions.outputs.tag }} - REGISTRY_TAG: ${{ steps.versions.outputs.registry_tag }} + REGISTRY_TAG: ${{ inputs.tag }} WORKER: ${{ inputs.worker }} NEW_VERSION: ${{ steps.versions.outputs.version }} run: | diff --git a/docs/sops/release.md b/docs/sops/release.md index 88208e49b..037ec323a 100644 --- a/docs/sops/release.md +++ b/docs/sops/release.md @@ -36,12 +36,13 @@ Actions → **Create Tag**: | Worker | Folder name (must be in workflow options) | | Bump | `patch` / `minor` / `major` / `none` — picks the base version | | Suffix | `none` / `alpha` / `beta` / `rc` / `stable` — pre-release line on that base | -| Registry tag | `latest` / `next` / `experimental` — channel the version publishes to; alpha always resolves to `experimental` | +| Registry tag | `latest` / `next` / `experimental` — channel the version publishes to | -The suffix lives in the version (`1.2.3-rc.1`); the channel is where that -version is published (`@next`). They are independent except for `alpha`, which -is always published as `@experimental` so it cannot advance `latest`. See -[Version suffixes](#version-suffixes) and [Registry tag semantics](#4-registry-tag-semantics). +**Suffix and Registry tag are independent axes.** The suffix lives in the +version (`1.2.3-rc.1`); the channel is where that version is published +(`@next`). Any combination is valid — a release is `@`, e.g. +`1.2.3-rc.1@next`. See [Version suffixes](#version-suffixes) and +[Registry tag semantics](#4-registry-tag-semantics). The workflow: @@ -123,14 +124,13 @@ a dead channel that nothing resolves. ### Version suffixes -A suffix is *what the version is*, written into the version itself. The -channel is normally chosen independently, except that alpha releases are -always isolated on `experimental`. +A suffix is *what the version is*, written into the version itself. It is +orthogonal to the channel — pick both independently. | Suffix | Result from `1.2.3` | Meaning | |---|---|---| | `none` | `1.2.4` | Stable release (default) | -| `alpha` | `1.2.4-alpha.1` | Earliest, expected to break; always `@experimental` | +| `alpha` | `1.2.4-alpha.1` | Earliest, expected to break | | `beta` | `1.2.4-beta.1` | Feature-complete but unstable | | `rc` | `1.2.4-rc.1` | Release candidate | | `stable` | `1.2.3` | Promote a pre-release to its base, no bump | @@ -143,9 +143,6 @@ Bump and suffix compose: **Bump** picks the base version, **Suffix** decides whether that base ships as a pre-release. `Bump: none` keeps the current base, which is how you iterate a pre-release without walking the version forward. -Create Tag overrides the selected registry channel to `experimental` for an -`alpha` suffix, so a test build can never advance the stable `latest` pointer. - A typical `rc` cycle, all on `@next`, then promoted: ```text From 914939be1cf209ac983a633d83d6fa274f36fa9e Mon Sep 17 00:00:00 2001 From: guibeira Date: Thu, 30 Jul 2026 14:59:51 -0300 Subject: [PATCH 6/7] Revert "chore(image-resize): bump to v0.1.13-alpha.1" This reverts commit 0cba4881b24e0c105f4465a63231296453a82506. --- image-resize/Cargo.lock | 2 +- image-resize/Cargo.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/image-resize/Cargo.lock b/image-resize/Cargo.lock index 486da4ef7..80f807c36 100644 --- a/image-resize/Cargo.lock +++ b/image-resize/Cargo.lock @@ -755,7 +755,7 @@ dependencies = [ [[package]] name = "image-resize" -version = "0.1.13-alpha.1" +version = "0.1.13" dependencies = [ "anyhow", "clap", diff --git a/image-resize/Cargo.toml b/image-resize/Cargo.toml index a62ef26ce..e32650d83 100644 --- a/image-resize/Cargo.toml +++ b/image-resize/Cargo.toml @@ -2,7 +2,7 @@ [package] name = "image-resize" -version = "0.1.13-alpha.1" +version = "0.1.13" edition = "2021" publish = false From ba1bacc77e38230f8c6f3afd6cf75d0ff54060fe Mon Sep 17 00:00:00 2001 From: guibeira Date: Thu, 30 Jul 2026 14:59:51 -0300 Subject: [PATCH 7/7] Revert "feat(release): split version suffix from registry channel (#615)" This reverts commit d0bc06da4517e9f39fb0295e5ab52d79840c775b. --- .github/scripts/_lib.py | 45 ---------- .github/scripts/manifest_version.py | 30 +------ .github/scripts/parse_release_tag.py | 14 --- .github/scripts/tests/conftest.py | 20 ----- .../scripts/tests/test_manifest_version.py | 86 +------------------ .../scripts/tests/test_parse_release_tag.py | 41 --------- .github/workflows/_container.yml | 2 +- .github/workflows/_publish-registry.yml | 2 +- .github/workflows/_publish-worker-skills.yml | 2 +- .github/workflows/create-tag.yml | 22 +---- .github/workflows/publish-worker-skills.yml | 1 - docs/sops/release.md | 80 +++-------------- 12 files changed, 21 insertions(+), 324 deletions(-) diff --git a/.github/scripts/_lib.py b/.github/scripts/_lib.py index 078e5bf2b..d0af66bd8 100644 --- a/.github/scripts/_lib.py +++ b/.github/scripts/_lib.py @@ -4,7 +4,6 @@ import json import re import subprocess -from collections.abc import Iterable from dataclasses import dataclass from pathlib import Path from typing import Literal @@ -13,11 +12,6 @@ BumpKind = Literal["patch", "minor", "major"] ManifestKind = Literal["cargo", "node", "python"] -# Pre-release suffixes offered by the Create Tag workflow. `stable` is not a -# suffix but the promotion path (drop the pre-release, keep the base version), -# so it lives in the same input. -PRERELEASE_SUFFIXES = ("alpha", "beta", "rc") - def parse_semver(v: str) -> SemverKey: """Returns a tuple suitable for lexicographic compare. @@ -58,45 +52,6 @@ def bump(current: str, kind: BumpKind) -> str: return f"{major}.{minor}.{patch}" -def core_version(v: str) -> str: - """Returns `v` without its pre-release / build suffix (1.2.3-rc.1 -> 1.2.3).""" - return v.partition("-")[0].partition("+")[0] - - -def next_prerelease(base: str, suffix: str, existing: Iterable[str]) -> str: - """Returns `base-suffix.N`, one past the highest N already released. - - `existing` is every version already tagged for the worker; only entries - matching this exact base and suffix count, so alpha and beta lines at the - same base advance independently. - """ - pattern = re.compile(rf"^{re.escape(base)}-{re.escape(suffix)}\.(\d+)$") - highest = 0 - for version in existing: - m = pattern.match(version.strip()) - if m: - highest = max(highest, int(m.group(1))) - return f"{base}-{suffix}.{highest + 1}" - - -def list_tagged_versions(worker: str) -> list[str]: - """Versions already tagged for `worker`, from git tags `/v`. - - Returns [] when git is unavailable or the worker has no tags yet, so a - first pre-release starts its counter at 1. - """ - prefix = f"{worker}/v" - try: - out = subprocess.check_output( - ["git", "tag", "--list", f"{prefix}*"], - text=True, - stderr=subprocess.DEVNULL, - ) - except (subprocess.CalledProcessError, FileNotFoundError): - return [] - return [line[len(prefix):] for line in out.splitlines() if line.startswith(prefix)] - - def detect_kind(manifest_path: Path) -> ManifestKind: """Identifies a manifest file by its filename.""" name = manifest_path.name diff --git a/.github/scripts/manifest_version.py b/.github/scripts/manifest_version.py index 100f36cc9..ce47b2203 100644 --- a/.github/scripts/manifest_version.py +++ b/.github/scripts/manifest_version.py @@ -4,7 +4,6 @@ Subcommands: read print the manifest's version to stdout bump --kind ... bump the version in-place - [--suffix alpha|beta|rc|stable|none --worker NAME] verify --expected V assert the file's version equals V deploy-mode print the interface-collection mode @@ -32,28 +31,11 @@ def cmd_read(args: argparse.Namespace) -> int: return 0 -def _resolve_version(current: str, kind: str, suffix: str, worker: str | None) -> str: - """Applies `kind` (version bump) and `suffix` (pre-release line) to `current`. - - The two are independent: `kind` picks the base version, `suffix` decides - whether that base ships as a pre-release. `none` leaves the manifest value - alone (a merged PR may have set it); `stable` promotes a pre-release to its - base without bumping (1.2.3-rc.2 -> 1.2.3). - """ - if suffix in _lib.PRERELEASE_SUFFIXES: - base = _lib.core_version(current) if kind == "none" else _lib.bump(current, kind) - existing = _lib.list_tagged_versions(worker) if worker else [] - return _lib.next_prerelease(base, suffix, existing) - if suffix == "stable": - return _lib.core_version(current) if kind == "none" else _lib.bump(current, kind) - return current if kind == "none" else _lib.bump(current, kind) - - def cmd_bump(args: argparse.Namespace) -> int: path = Path(args.manifest) try: current = _lib.read_version(path) - new = _resolve_version(current, args.kind, args.suffix, args.worker) + new = current if args.kind == "none" else _lib.bump(current, args.kind) _lib.write_version(path, new) except (FileNotFoundError, ValueError) as e: print(f"error: {e}", file=sys.stderr) @@ -149,16 +131,6 @@ def main(argv: list[str] | None = None) -> int: p_bump = sub.add_parser("bump", help="bump the manifest version in place") p_bump.add_argument("manifest") p_bump.add_argument("--kind", choices=["patch", "minor", "major", "none"], required=True) - p_bump.add_argument( - "--suffix", - choices=["none", "stable", *_lib.PRERELEASE_SUFFIXES], - default="none", - help="pre-release line for the bumped version (none = leave as-is)", - ) - p_bump.add_argument( - "--worker", - help="worker name; used to read existing git tags when numbering a pre-release", - ) p_bump.set_defaults(func=cmd_bump) p_verify = sub.add_parser("verify", help="assert the manifest version equals --expected") diff --git a/.github/scripts/parse_release_tag.py b/.github/scripts/parse_release_tag.py index 28f4cc634..26f462e34 100644 --- a/.github/scripts/parse_release_tag.py +++ b/.github/scripts/parse_release_tag.py @@ -21,13 +21,6 @@ DRY_RUN_RE = re.compile(r"-dry-run\.\d+$") PRERELEASE_RE = re.compile(r"-[a-z]+\.\d+$") -# Distribution channels, orthogonal to the version's pre-release suffix: a -# release is `@`, e.g. 1.2.3-rc.1@next. The registry stores -# `registry-tag` verbatim (a free-form string column), so a typo in the -# annotated tag message would silently create a dead channel that nothing -# resolves. Keep the accepted set closed here, matching the Create Tag options. -RELEASE_CHANNELS = ("latest", "next", "experimental") - def main(argv: list[str] | None = None) -> int: p = argparse.ArgumentParser() @@ -63,13 +56,6 @@ def main(argv: list[str] | None = None) -> int: return 1 registry_tag = _lib.read_tag_annotation(raw).get("registry-tag", "latest") or "latest" - if registry_tag not in RELEASE_CHANNELS: - print( - f"::error::Unknown registry-tag {registry_tag!r} in the annotated tag " - f"message; expected one of {', '.join(RELEASE_CHANNELS)}", - file=sys.stderr, - ) - return 1 # `targets` is an optional iii.worker.yaml field that can be either a # list (`- aarch64-apple-darwin\n- x86_64-unknown-linux-gnu`) or a comma diff --git a/.github/scripts/tests/conftest.py b/.github/scripts/tests/conftest.py index d13f16cf9..ad084f9ad 100644 --- a/.github/scripts/tests/conftest.py +++ b/.github/scripts/tests/conftest.py @@ -54,26 +54,6 @@ def pyproject_manifest(tmp_path: Path) -> Path: return p -@pytest.fixture -def git_repo_manifest(tmp_path: Path) -> tuple[Path, Path]: - """A git repo with a committed Cargo.toml at 0.1.0, for pre-release numbering. - - Returns (repo_dir, manifest_path). Tests add `/v` tags to - the repo to exercise the counter `manifest_version.py bump --worker` reads. - """ - def git(*args: str) -> None: - subprocess.run(args, cwd=tmp_path, check=True, env=GIT_HERMETIC_ENV) - - git("git", "init", "-q", "-b", "main") - git("git", "config", "user.email", "test@example.com") - git("git", "config", "user.name", "Test") - manifest = tmp_path / "Cargo.toml" - manifest.write_text('[package]\nname = "smoke"\nversion = "0.1.0"\nedition = "2021"\n') - git("git", "add", ".") - git("git", "commit", "-q", "-m", "init") - return tmp_path, manifest - - @pytest.fixture def iii_worker_yaml_dir(tmp_path: Path) -> Path: """Returns a tmp dir containing a minimal iii.worker.yaml (rust binary).""" diff --git a/.github/scripts/tests/test_manifest_version.py b/.github/scripts/tests/test_manifest_version.py index edc0af215..1bf6f1638 100644 --- a/.github/scripts/tests/test_manifest_version.py +++ b/.github/scripts/tests/test_manifest_version.py @@ -5,29 +5,15 @@ import sys from pathlib import Path -import pytest - -from _test_helpers import GIT_HERMETIC_ENV - SCRIPT = Path(__file__).resolve().parents[1] / "manifest_version.py" -def run_script(*args: str, cwd: Path | None = None) -> subprocess.CompletedProcess[str]: +def run_script(*args: str) -> subprocess.CompletedProcess[str]: """Run manifest_version.py with arguments; capture stdout/stderr/exit.""" return subprocess.run( [sys.executable, str(SCRIPT), *args], capture_output=True, text=True, - cwd=cwd, - env=GIT_HERMETIC_ENV, - ) - - -def tag(repo: Path, name: str) -> None: - """Create an annotated tag in `repo` (the shape create-tag.yml pushes).""" - subprocess.run( - ["git", "tag", "-a", name, "-m", f"Release {name}\n\nregistry-tag: next\n"], - cwd=repo, check=True, env=GIT_HERMETIC_ENV, ) @@ -82,76 +68,6 @@ def test_bump_rejects_unknown_kind(self, cargo_manifest): r = run_script("bump", str(cargo_manifest), "--kind", "weird") assert r.returncode != 0 - def test_bump_defaults_to_no_suffix(self, cargo_manifest): - r = run_script("bump", str(cargo_manifest), "--kind", "patch") - assert r.stdout.strip() == "0.1.1" - - -class TestBumpSuffix: - """`--suffix` picks the pre-release line; `--kind` still picks the base.""" - - @pytest.mark.parametrize("suffix", ["alpha", "beta", "rc"]) - def test_suffix_starts_counter_at_one(self, cargo_manifest, suffix): - r = run_script("bump", str(cargo_manifest), "--kind", "patch", "--suffix", suffix) - assert r.returncode == 0, r.stderr - assert r.stdout.strip() == f"0.1.1-{suffix}.1" - - def test_suffix_with_kind_none_keeps_base(self, cargo_manifest): - """Iterating a pre-release must not walk the base version forward.""" - r = run_script("bump", str(cargo_manifest), "--kind", "none", "--suffix", "alpha") - assert r.stdout.strip() == "0.1.0-alpha.1" - - def test_suffix_strips_existing_prerelease_before_bumping(self, cargo_manifest): - run_script("bump", str(cargo_manifest), "--kind", "none", "--suffix", "alpha") - r = run_script("bump", str(cargo_manifest), "--kind", "patch", "--suffix", "beta") - assert r.stdout.strip() == "0.1.1-beta.1" - - def test_stable_promotes_prerelease_to_base(self, cargo_manifest): - run_script("bump", str(cargo_manifest), "--kind", "none", "--suffix", "rc") - r = run_script("bump", str(cargo_manifest), "--kind", "none", "--suffix", "stable") - assert r.stdout.strip() == "0.1.0" - - def test_stable_on_stable_version_is_a_noop(self, cargo_manifest): - r = run_script("bump", str(cargo_manifest), "--kind", "none", "--suffix", "stable") - assert r.stdout.strip() == "0.1.0" - - def test_rejects_unknown_suffix(self, cargo_manifest): - r = run_script("bump", str(cargo_manifest), "--kind", "patch", "--suffix", "gamma") - assert r.returncode != 0 - - -class TestPrereleaseCounter: - """`--worker` numbers the pre-release from tags already in the repo.""" - - def test_counter_continues_from_existing_tags(self, git_repo_manifest): - repo, manifest = git_repo_manifest - tag(repo, "smoke/v0.1.1-alpha.1") - tag(repo, "smoke/v0.1.1-alpha.2") - r = run_script("bump", str(manifest), "--kind", "patch", - "--suffix", "alpha", "--worker", "smoke", cwd=repo) - assert r.stdout.strip() == "0.1.1-alpha.3" - - def test_counter_ignores_other_suffixes_at_same_base(self, git_repo_manifest): - repo, manifest = git_repo_manifest - tag(repo, "smoke/v0.1.1-alpha.4") - r = run_script("bump", str(manifest), "--kind", "patch", - "--suffix", "beta", "--worker", "smoke", cwd=repo) - assert r.stdout.strip() == "0.1.1-beta.1" - - def test_counter_ignores_other_workers(self, git_repo_manifest): - repo, manifest = git_repo_manifest - tag(repo, "other/v0.1.1-alpha.9") - r = run_script("bump", str(manifest), "--kind", "patch", - "--suffix", "alpha", "--worker", "smoke", cwd=repo) - assert r.stdout.strip() == "0.1.1-alpha.1" - - def test_counter_ignores_other_base_versions(self, git_repo_manifest): - repo, manifest = git_repo_manifest - tag(repo, "smoke/v0.2.0-alpha.7") - r = run_script("bump", str(manifest), "--kind", "patch", - "--suffix", "alpha", "--worker", "smoke", cwd=repo) - assert r.stdout.strip() == "0.1.1-alpha.1" - class TestVerifySubcommand: def test_verify_match(self, cargo_manifest): diff --git a/.github/scripts/tests/test_parse_release_tag.py b/.github/scripts/tests/test_parse_release_tag.py index a82103ff7..6a6ccc79f 100644 --- a/.github/scripts/tests/test_parse_release_tag.py +++ b/.github/scripts/tests/test_parse_release_tag.py @@ -82,47 +82,6 @@ def test_prerelease_sets_is_prerelease(self, tmp_path): assert out["dry_run"] == "false" assert out["registry_tag"] == "next" - def test_experimental_channel(self, tmp_path): - repo = make_repo_with_tagged_worker( - tmp_path, "smoke/v1.2.3", "1.2.3", - registry_tag_line="registry-tag: experimental") - out_path = tmp_path / "gh_output" - out_path.touch() - r = run_script(repo, "smoke/v1.2.3", out_path) - assert r.returncode == 0, r.stderr - assert parse_outputs(out_path)["registry_tag"] == "experimental" - - # A suffixed version is orthogonal to the channel: it ships on whichever - # channel the tag message names, and still marks the GitHub Release as a - # prerelease. - @pytest.mark.parametrize("suffix", ["alpha", "beta", "rc"]) - def test_suffixed_version_on_next_channel(self, tmp_path, suffix): - version = f"1.2.3-{suffix}.1" - repo = make_repo_with_tagged_worker( - tmp_path, f"smoke/v{version}", version, - registry_tag_line="registry-tag: next") - out_path = tmp_path / "gh_output" - out_path.touch() - r = run_script(repo, f"smoke/v{version}", out_path) - assert r.returncode == 0, r.stderr - out = parse_outputs(out_path) - assert out["version"] == version - assert out["registry_tag"] == "next" - assert out["is_prerelease"] == "true" - - # `alpha` is a version suffix, never a channel; accepting it here would - # publish a channel the registry resolves for nobody. - @pytest.mark.parametrize("bad", ["alpha", "latests", "stable"]) - def test_unknown_channel_fails(self, tmp_path, bad): - repo = make_repo_with_tagged_worker( - tmp_path, "smoke/v1.2.3", "1.2.3", - registry_tag_line=f"registry-tag: {bad}") - out_path = tmp_path / "gh_output" - out_path.touch() - r = run_script(repo, "smoke/v1.2.3", out_path) - assert r.returncode == 1 - assert "Unknown registry-tag" in r.stderr - def test_dry_run_tag(self, tmp_path): repo = make_repo_with_tagged_worker(tmp_path, "smoke/v9.9.9-dry-run.1", "9.9.9-dry-run.1") out_path = tmp_path / "gh_output" diff --git a/.github/workflows/_container.yml b/.github/workflows/_container.yml index 7cac69aae..4e8625532 100644 --- a/.github/workflows/_container.yml +++ b/.github/workflows/_container.yml @@ -12,7 +12,7 @@ on: required: true type: string registry_tag: - description: 'Registry channel to also push as an image tag (latest, next, experimental)' + description: 'Registry tag to also push (latest, next, ...)' required: false type: string default: latest diff --git a/.github/workflows/_publish-registry.yml b/.github/workflows/_publish-registry.yml index ca01d363d..ed3848d66 100644 --- a/.github/workflows/_publish-registry.yml +++ b/.github/workflows/_publish-registry.yml @@ -16,7 +16,7 @@ on: required: true type: string registry_tag: - description: 'Registry channel (latest, next, experimental)' + description: 'Registry tag (latest, next, ...)' required: false type: string default: latest diff --git a/.github/workflows/_publish-worker-skills.yml b/.github/workflows/_publish-worker-skills.yml index fbd6d7ed8..30c040b16 100644 --- a/.github/workflows/_publish-worker-skills.yml +++ b/.github/workflows/_publish-worker-skills.yml @@ -8,7 +8,7 @@ on: required: true type: string version: - description: 'Registry channel (latest, next, experimental)' + description: 'Registry tag channel (latest, next, ...)' required: true type: string api_url: diff --git a/.github/workflows/create-tag.yml b/.github/workflows/create-tag.yml index e90ba76ca..ed5bf52b4 100644 --- a/.github/workflows/create-tag.yml +++ b/.github/workflows/create-tag.yml @@ -69,25 +69,13 @@ on: - major - none default: patch - suffix: - description: 'Pre-release suffix (none = leave the version stable; stable = promote a pre-release to its base version)' - required: true - type: choice - options: - - none - - alpha - - beta - - rc - - stable - default: none tag: - description: 'Registry channel the version is published under (passed to POST /publish; not part of the git tag name)' + description: 'Registry tag (passed to POST /publish; not part of git tag name)' required: true type: choice options: - latest - next - - experimental default: latest permissions: @@ -158,19 +146,15 @@ jobs: env: WORKER: ${{ inputs.worker }} BUMP: ${{ inputs.bump }} - SUFFIX: ${{ inputs.suffix }} MANIFEST: ${{ steps.meta.outputs.manifest }} run: | set -euo pipefail current=$(python3 .github/scripts/manifest_version.py read "$WORKER/$MANIFEST") - # --worker lets the script number the pre-release from existing tags - # (checkout above is fetch-depth 0, so they are all present). - new_ver=$(python3 .github/scripts/manifest_version.py bump "$WORKER/$MANIFEST" \ - --kind "$BUMP" --suffix "$SUFFIX" --worker "$WORKER") + new_ver=$(python3 .github/scripts/manifest_version.py bump "$WORKER/$MANIFEST" --kind "$BUMP") echo "current=$current" >> "$GITHUB_OUTPUT" echo "version=$new_ver" >> "$GITHUB_OUTPUT" echo "tag=${WORKER}/v${new_ver}" >> "$GITHUB_OUTPUT" - echo "::notice::${WORKER}: ${current} -> ${new_ver} (channel: ${{ inputs.tag }})" + echo "::notice::${WORKER}: ${current} -> ${new_ver}" - name: Validate manifest update env: diff --git a/.github/workflows/publish-worker-skills.yml b/.github/workflows/publish-worker-skills.yml index c60b3f7af..1e0692e08 100644 --- a/.github/workflows/publish-worker-skills.yml +++ b/.github/workflows/publish-worker-skills.yml @@ -14,7 +14,6 @@ on: options: - latest - next - - experimental default: latest concurrency: diff --git a/docs/sops/release.md b/docs/sops/release.md index 037ec323a..cd28d44f2 100644 --- a/docs/sops/release.md +++ b/docs/sops/release.md @@ -34,22 +34,15 @@ Actions → **Create Tag**: | Input | Meaning | |---|---| | Worker | Folder name (must be in workflow options) | -| Bump | `patch` / `minor` / `major` / `none` — picks the base version | -| Suffix | `none` / `alpha` / `beta` / `rc` / `stable` — pre-release line on that base | -| Registry tag | `latest` / `next` / `experimental` — channel the version publishes to | - -**Suffix and Registry tag are independent axes.** The suffix lives in the -version (`1.2.3-rc.1`); the channel is where that version is published -(`@next`). Any combination is valid — a release is `@`, e.g. -`1.2.3-rc.1@next`. See [Version suffixes](#version-suffixes) and -[Registry tag semantics](#4-registry-tag-semantics). +| Bump | `patch` / `minor` / `major` | +| Registry tag | `latest` or `next` — channel for `iii worker add` resolution | The workflow: 1. Bumps version in the worker manifest (`Cargo.toml`, `package.json`, …). 2. Commits `chore(): bump to vX.Y.Z` to `main`. 3. Creates and pushes an **annotated** tag `/vX.Y.Z` with - `registry-tag: ` in the tag message. + `registry-tag: ` in the tag message. ### 2. Release pipeline @@ -99,61 +92,14 @@ Workers with `interface_smoke: false` skip the entire publish job. ### 4. Registry tag semantics -A channel is *where a version is published*, written `@`. - | Channel | Typical use | |---|---| | `latest` | Default; what most `iii worker add` installs resolve | -| `next` | Upcoming release; safer for a first publish | -| `experimental` | Throwaway / spike work not intended for promotion | - -A worker version carries **at most one** channel, and a worker has at most one -version per channel. Publishing moves the channel: the previous holder loses it -(`clearTagOnWorker` in the registry), so channels are reassigned on each release. +| `next` | Pre-release / risky channel; safer for first publish | The channel is stored in the **annotated tag message** (`registry-tag:`). `release.yml` refetches the annotated tag for this reason. Lightweight tags -lose the channel and default to `latest`. `parse_release_tag.py` rejects any -value outside the table above, so a typo fails the release instead of creating -a dead channel that nothing resolves. - -> **Note:** installs resolve `latest` only. `next` and `experimental` are -> published and queryable by tag through the registry API, but -> `iii worker add @next` needs resolver support in `iii-hq/registry` -> before it works. - -### Version suffixes - -A suffix is *what the version is*, written into the version itself. It is -orthogonal to the channel — pick both independently. - -| Suffix | Result from `1.2.3` | Meaning | -|---|---|---| -| `none` | `1.2.4` | Stable release (default) | -| `alpha` | `1.2.4-alpha.1` | Earliest, expected to break | -| `beta` | `1.2.4-beta.1` | Feature-complete but unstable | -| `rc` | `1.2.4-rc.1` | Release candidate | -| `stable` | `1.2.3` | Promote a pre-release to its base, no bump | - -The counter is derived from existing git tags, so re-running Create Tag with -the same worker, base and suffix advances it (`-rc.1` → `-rc.2`) instead of -colliding. Each suffix line advances independently at the same base. - -Bump and suffix compose: **Bump** picks the base version, **Suffix** decides -whether that base ships as a pre-release. `Bump: none` keeps the current base, -which is how you iterate a pre-release without walking the version forward. - -A typical `rc` cycle, all on `@next`, then promoted: - -```text -Bump: patch Suffix: rc Tag: next -> 1.2.4-rc.1@next -Bump: none Suffix: rc Tag: next -> 1.2.4-rc.2@next -Bump: none Suffix: stable Tag: latest -> 1.2.4@latest -``` - -Pre-release versions are marked as prereleases on the GitHub Release and are -skipped by the registry resolver's semver matching, so a `^1.2.0` dependency -never silently resolves to `1.2.4-rc.2`. +lose the channel and default to `latest`. ## Variants @@ -165,13 +111,14 @@ Concurrency group `release-${{ github.ref }}` serializes per tag. ### Prerelease -Use Create Tag's **Suffix** input — see [Version suffixes](#version-suffixes). +Create Tag cannot produce prerelease suffixes. Push a manual **annotated** tag: + +```text +/vX.Y.Z-beta.1 +``` -To cut one by hand instead, push an **annotated** tag shaped -`/vX.Y.Z-beta.1` with `registry-tag: ` in the message. Either -way the GitHub Release is marked prerelease and still builds and publishes -(unless `interface_smoke: false`). A hand-pushed tag must carry the `.N` -counter — `parse_release_tag.py` detects prereleases as `-.`. +With tag message including `registry-tag: next`. Marks the GitHub Release as +prerelease; still builds and publishes (unless `interface_smoke: false`). ### Dry run @@ -243,8 +190,7 @@ There is **no unpublish**. Recovery: 1. Fix the issue on `main`. 2. Cut a new patch via Create Tag (registry `latest` moves forward). -3. When uncertain, cut an `rc` suffix on the `next` channel first, then - promote with `Suffix: stable` / `Tag: latest`. +3. Use `registry-tag: next` when uncertain before promoting to `latest`. GitHub Release assets for the bad version remain (immutable history).