Skip to content

(MOT-4299) feat: stage worker releases before promotion - #692

Merged
ytallo merged 1 commit into
mainfrom
feat/staged-worker-releases
Aug 4, 2026
Merged

(MOT-4299) feat: stage worker releases before promotion#692
ytallo merged 1 commit into
mainfrom
feat/staged-worker-releases

Conversation

@ytallo

@ytallo ytallo commented Aug 4, 2026

Copy link
Copy Markdown
Contributor

Summary

  • publish Registry-backed worker releases to @next and keep the GitHub Release marked as a prerelease until promotion
  • validate each candidate through resolve/install/boot checks, plus Harness quickstart and deployed E2E for the Harness stack
  • persist candidate evidence and require it during a manual promotion to Registry @latest
  • promote the matching GHCR image alias and finalize the existing GitHub Release without changing the repository-global Latest release
  • separate the iii installer channel from the Registry worker tag in Harness validation

Why

Publishing directly to @latest makes a new worker version the default before its published artifacts have been exercised. This adds an explicit candidate stage, preserves immutable versioned artifacts, and requires successful evidence before a manual promotion.

Impact

Create Tag now publishes stable worker versions to @next. Maintainers promote a validated candidate with the Promote Worker action using its worker, version, and Release run ID. Existing GitHub-only releases continue through the non-staged path.

Validation

  • python -m pytest -q .github/scripts/tests — 176 passed
  • actionlint
  • shellcheck harness/tests/quickstart/run-ci.sh harness/tests/e2e/run-deployed-ci.sh
  • git diff --check

Refs MOT-4299

Summary by CodeRabbit

  • New Features
    • Added staged release candidate validation, smoke testing, and controlled worker promotion.
    • Added separate CLI channel and worker registry tag selection for release and E2E workflows.
    • Added promotion verification, rollback safeguards, release evidence, and deployment metadata.
  • Bug Fixes
    • Improved prerelease detection and handling of duplicate or failed registry promotions.
    • Strengthened release validation for tags, versions, readiness, and published artifacts.
  • Documentation
    • Updated release procedures and quickstart guidance for candidate testing, promotion, and rollback.

@vercel

vercel Bot commented Aug 4, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
workers Ready Ready Preview Aug 4, 2026 4:15pm
workers-tech-spec Ready Ready Preview Aug 4, 2026 4:15pm

Request Review

@coderabbitai

coderabbitai Bot commented Aug 4, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

The release pipeline now creates staged candidates, validates release evidence and smoke-test gates, publishes candidates to next, and supports manual promotion to latest. Harness workflows separately select CLI channels and worker Registry tags.

Changes

Staged worker release

Layer / File(s) Summary
Release metadata and candidate evidence
.github/scripts/parse_release_tag.py, .github/scripts/release_candidate.py, .github/scripts/tests/*
Release parsing emits tag_sha. Stable versions require numeric X.Y.Z form. Candidate evidence validates release identity, SHAs, versions, gate results, and promotion fields.
Registry promotion and finalization
.github/scripts/registry_release.py, .github/workflows/promote-worker.yml, .github/scripts/tests/test_registry_release.py
The Registry utility resolves and promotes tags with ownership checks. The promotion workflow validates evidence, updates Registry and image tags, finalizes the GitHub release, and sends Slack notifications.
Candidate release workflow
.github/workflows/release.yml, .github/workflows/_candidate-smoke.yml, .github/workflows/_publish-registry.yml, .github/workflows/create-tag.yml, docs/sops/release.md
Staged releases run candidate smoke and Harness validation. Candidate evidence gates announcements and promotion. Registry candidates use next, with idempotent conflict handling.
Separate CLI and worker channels
.github/workflows/_harness-e2e.yml, .github/workflows/harness-quickstart.yml, .github/workflows/harness-e2e-deployed.yml, harness/tests/*
Workflows and test scripts replace the combined channel with separate CLI channel and worker tag values. Release overrides install and verify the exact candidate version.

Estimated code review effort: 5 (Critical) | ~120 minutes

Sequence Diagram(s)

sequenceDiagram
  participant ReleaseWorkflow
  participant CandidateSmoke
  participant CandidateEvidence
  participant PromoteWorker
  participant RegistryAPI
  ReleaseWorkflow->>CandidateSmoke: install and test worker@next
  CandidateSmoke-->>ReleaseWorkflow: return smoke artifacts
  ReleaseWorkflow->>CandidateEvidence: build and validate release-candidate.json
  CandidateEvidence-->>ReleaseWorkflow: return candidate_ready
  PromoteWorker->>CandidateEvidence: validate immutable candidate evidence
  PromoteWorker->>RegistryAPI: promote verified version to latest
  RegistryAPI-->>PromoteWorker: return promotion metadata
Loading

Possibly related PRs

Suggested reviewers: guibeira

Poem

A rabbit checks the tag in flight,
Then tests the candidate through the night.
next waits by the Registry door,
latest moves when checks are sure.
SHA and gates are neatly shown—
The release hops safely home.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the primary change: staging worker releases before promotion.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches 💡 1
📝 Generate docstrings 💡
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/staged-worker-releases

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@github-actions

github-actions Bot commented Aug 4, 2026

Copy link
Copy Markdown
Contributor

skill-check — worker

0 verified, 53 skipped (no docs/).

Layer Result
structure
vale
ai
render

Four for four. Nicely done.

@ytallo
ytallo marked this pull request as ready for review August 4, 2026 16:26

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In @.github/scripts/registry_release.py:
- Around line 76-85: The promotion flow around promotion_payload() must not
require ownership of next when current_latest already equals version. Return a
verified no-op for this idempotent retry path, or use a promotion API that omits
expected_tag: next, while preserving the existing next precondition for first
promotions. Update the related tests to stop expecting expected_tag: next on
idempotent retries.

In @.github/scripts/release_candidate.py:
- Around line 92-96: Update the Harness validation flow around the
evidence.get("harness_gate_required") check to derive the expected gate
requirement from trusted promotion inputs rather than trusting the candidate
evidence. Compare the derived policy with the evidence before validating
harness_quickstart and harness_e2e results, and ensure a harness worker with
harness_gate_required=False is rejected when the trusted policy requires the
gates; add coverage for that case.

In `@docs/sops/release.md`:
- Around line 57-77: Update the release flow description around the
create-release stage and its “Public GitHub prerelease” output to distinguish
release types: describe staged Registry releases and semantic prereleases as
GitHub prereleases, while stable workers with interface_smoke disabled are
documented as normal GitHub Releases. Keep the existing workflow stages and
outputs unchanged apart from this release-type clarification.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 2c3d52bd-f01c-4bf8-b122-1ff1f90da057

📥 Commits

Reviewing files that changed from the base of the PR and between b3a44c4 and 575d252.

📒 Files selected for processing (19)
  • .github/scripts/parse_release_tag.py
  • .github/scripts/registry_release.py
  • .github/scripts/release_candidate.py
  • .github/scripts/tests/test_parse_release_tag.py
  • .github/scripts/tests/test_registry_release.py
  • .github/scripts/tests/test_release_candidate.py
  • .github/workflows/_candidate-smoke.yml
  • .github/workflows/_container.yml
  • .github/workflows/_harness-e2e.yml
  • .github/workflows/_publish-registry.yml
  • .github/workflows/create-tag.yml
  • .github/workflows/harness-e2e-deployed.yml
  • .github/workflows/harness-quickstart.yml
  • .github/workflows/promote-worker.yml
  • .github/workflows/release.yml
  • docs/sops/release.md
  • harness/tests/e2e/run-deployed-ci.sh
  • harness/tests/quickstart/README.md
  • harness/tests/quickstart/run-ci.sh

Comment on lines +76 to +85
# A first promotion must still own `next`. Once Registry latest already
# points at the requested immutable version, allow an idempotent retry to
# repair GitHub/GHCR/Slack even if a newer candidate has moved `next`.
if current_latest != version and current_next != version:
raise RegistryError(f"next points to {current_next}, expected {version}")
encoded_worker = urllib.parse.quote(worker, safe="")
status, response = request_json(
"PUT",
f"{api_url.rstrip('/')}/w/{encoded_worker}/tags/latest",
promotion_payload(version, current_latest),

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

rg -n -C 8 \
  'expected_tag|expected_current_version|tags/latest|promotion_payload' \
  .

Repository: iii-hq/workers

Length of output: 152


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "Files matching registry_release.py:"
fd -a 'registry_release\.py$' . || true

echo
echo "Changed files/stat:"
git diff --stat || true

echo
echo "Search target filename:"
if [ -f .github/scripts/registry_release.py ]; then
  nl -ba .github/scripts/registry_release.py | sed -n '1,180p'
else
  echo ".github/scripts/registry_release.py not found"
fi

echo
echo "Broader search for target terms (case-insensitive):"
rg -n -i -C 6 \
  'expected_tag|expected_current_version|tags/latest|promotion_payload|current_next|current_latest' .github src . 2>/dev/null || true

Repository: iii-hq/workers

Length of output: 275


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "pwd:"
pwd
echo

echo "Files matching registry_release.py:"
find . -type f -name 'registry_release.py' 2>/dev/null || true
echo

echo "Changed files/stat:"
git diff --stat 2>/dev/null || true
echo

echo "File contents:"
if [ -f .github/scripts/registry_release.py ]; then
  awk '{ printf "%6d\t%s\n", NR, $0 }' .github/scripts/registry_release.py | sed -n '1,180p'
else
  echo ".github/scripts/registry_release.py not found"
fi
echo

echo "Broader search for target terms:"
grep -RIn -C 6 -E 'expected_tag|expected_current_version|tags/latest|promotion_payload|current_next|current_latest' .github src . 2>/dev/null || true
echo

echo "Git status short:"
git status --short 2>/dev/null || true

Repository: iii-hq/workers

Length of output: 17601


Remove the next precondition from the idempotent retry path.

When latest already equals version, latest has moved, and next is version, the check allows the retry, but promotion_payload() still sends expected_tag: next. A Registry that requires ownership of the source tag can reject this retry. Return a verified no-op when current_latest == version, or use a promotion API that does not require ownership of next. Update the tests to stop asserting that idempotent reties include expected_tag: next.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.github/scripts/registry_release.py around lines 76 - 85, The promotion flow
around promotion_payload() must not require ownership of next when
current_latest already equals version. Return a verified no-op for this
idempotent retry path, or use a promotion API that omits expected_tag: next,
while preserving the existing next precondition for first promotions. Update the
related tests to stop expecting expected_tag: next on idempotent retries.

Comment on lines +92 to +96
if evidence.get("harness_gate_required"):
if results.get("harness_quickstart") != "success":
failures.append("Harness quickstart gate did not succeed")
if results.get("harness_e2e") != "success":
failures.append("Harness E2E gate did not succeed")

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Derive the Harness gate policy independently of the evidence.

The evidence controls harness_gate_required. If a harness candidate records this field as false, validation accepts skipped Harness gates while candidate_ready remains true.

Require the expected gate policy from trusted promotion inputs. Compare that policy with the evidence before checking the results. Add a test for worker="harness" with harness_gate_required=False.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.github/scripts/release_candidate.py around lines 92 - 96, Update the
Harness validation flow around the evidence.get("harness_gate_required") check
to derive the expected gate requirement from trusted promotion inputs rather
than trusting the candidate evidence. Compare the derived policy with the
evidence before validating harness_quickstart and harness_e2e results, and
ensure a harness worker with harness_gate_required=False is rejected when the
trusted policy requires the gates; add coverage for that case.

Comment thread docs/sops/release.md
Comment on lines +57 to +77
setupJob --> ghRelease[create public GitHub prerelease]
ghRelease --> buildBinary["binary: _rust-binary.yml"]
ghRelease --> buildImage["image: _container.yml"]
ghRelease --> buildBundle["bundle: _bundle.yml"]
buildBinary --> publishJob[_publish-registry.yml]
buildImage --> publishJob
buildBundle --> publishJob
publishJob --> postPublish["POST /publish + skills"]
publishJob --> candidateSmoke[resolve / install / boot next]
candidateSmoke --> harnessGate{Harness or dependency?}
harnessGate -->|yes| quickstart[Harness quickstart]
quickstart --> e2e[Harness deployed E2E]
harnessGate -->|no| evidence[candidate evidence]
e2e --> evidence
evidence --> promotion[manual Promote Worker]
promotion --> latest[Registry latest + GitHub Release]
```

| Stage | Job | Output |
|---|---|---|
| setup | Parse tag + `iii.worker.yaml`; detect web bundle / smoke opt-out | worker, version, deploy, targets, … |
| create-release | GitHub Release shell | Release page for the tag |
| create-release | Public GitHub prerelease, never repository-global Latest | Release page and downloadable assets |

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Correct the release-type description.

Line 57 and Line 77 state that every release creates a GitHub prerelease. A stable worker with interface_smoke: false has staged=false, so .github/workflows/release.yml creates a normal GitHub Release when is_prerelease=false. Restrict the prerelease description to staged Registry releases and semantic prereleases.

Proposed documentation update
-  setupJob --> ghRelease[create public GitHub prerelease]
+  setupJob --> ghRelease[create GitHub Release]
...
-| create-release | Public GitHub prerelease, never repository-global Latest | Release page and downloadable assets |
+| create-release | Staged Registry releases and semantic prereleases: public GitHub prerelease. Stable GitHub-only releases: normal GitHub Release. Never repository-global Latest. | Release page and downloadable assets |
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
setupJob --> ghRelease[create public GitHub prerelease]
ghRelease --> buildBinary["binary: _rust-binary.yml"]
ghRelease --> buildImage["image: _container.yml"]
ghRelease --> buildBundle["bundle: _bundle.yml"]
buildBinary --> publishJob[_publish-registry.yml]
buildImage --> publishJob
buildBundle --> publishJob
publishJob --> postPublish["POST /publish + skills"]
publishJob --> candidateSmoke[resolve / install / boot next]
candidateSmoke --> harnessGate{Harness or dependency?}
harnessGate -->|yes| quickstart[Harness quickstart]
quickstart --> e2e[Harness deployed E2E]
harnessGate -->|no| evidence[candidate evidence]
e2e --> evidence
evidence --> promotion[manual Promote Worker]
promotion --> latest[Registry latest + GitHub Release]
```
| Stage | Job | Output |
|---|---|---|
| setup | Parse tag + `iii.worker.yaml`; detect web bundle / smoke opt-out | worker, version, deploy, targets, … |
| create-release | GitHub Release shell | Release page for the tag |
| create-release | Public GitHub prerelease, never repository-global Latest | Release page and downloadable assets |
setupJob --> ghRelease[create GitHub Release]
ghRelease --> buildBinary["binary: _rust-binary.yml"]
ghRelease --> buildImage["image: _container.yml"]
ghRelease --> buildBundle["bundle: _bundle.yml"]
buildBinary --> publishJob[_publish-registry.yml]
buildImage --> publishJob
buildBundle --> publishJob
publishJob --> candidateSmoke[resolve / install / boot next]
candidateSmoke --> harnessGate{Harness or dependency?}
harnessGate -->|yes| quickstart[Harness quickstart]
quickstart --> e2e[Harness deployed E2E]
harnessGate -->|no| evidence[candidate evidence]
e2e --> evidence
evidence --> promotion[manual Promote Worker]
promotion --> latest[Registry latest + GitHub Release]
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@docs/sops/release.md` around lines 57 - 77, Update the release flow
description around the create-release stage and its “Public GitHub prerelease”
output to distinguish release types: describe staged Registry releases and
semantic prereleases as GitHub prereleases, while stable workers with
interface_smoke disabled are documented as normal GitHub Releases. Keep the
existing workflow stages and outputs unchanged apart from this release-type
clarification.

@ytallo
ytallo merged commit 34e266c into main Aug 4, 2026
18 checks passed
ytallo added a commit that referenced this pull request Aug 4, 2026
The provider-deepseek worker (#691) resolves its credential from the
DEEPSEEK_API_KEY env on the router, but the E2E workflows only forwarded
the anthropic/openai/zai secrets, so a deepseek subject or judge could
never discover models in CI. Declare the optional secret in the reusable
workflow, forward it from every caller (including the release pipeline's
E2E gate from #692), add the provider to the Rust cache workspaces, and
export it in both run steps.
ytallo added a commit that referenced this pull request Aug 4, 2026
)

The provider-deepseek worker (#691) resolves its credential from the
DEEPSEEK_API_KEY env on the router, but the E2E workflows only forwarded
the anthropic/openai/zai secrets, so a deepseek subject or judge could
never discover models in CI. Declare the optional secret in the reusable
workflow, forward it from every caller (including the release pipeline's
E2E gate from #692), add the provider to the Rust cache workspaces, and
export it in both run steps.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant