From 5da06015a236fdf5b9be3c6f9a8d053e4ca776ad Mon Sep 17 00:00:00 2001 From: Ytallo Layon Date: Wed, 5 Aug 2026 10:21:37 -0300 Subject: [PATCH 1/2] chore(release): drop unused harness quickstart smoke detection The harness_smoke setup output has had no consumer since 7f574187 gated candidates on the smoke only; candidate-ready hardcodes the harness gate as skipped. --- .github/workflows/release.yml | 24 ------------------------ 1 file changed, 24 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 8c88e9ff8..afcf50007 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -96,7 +96,6 @@ jobs: tag_sha: ${{ steps.meta.outputs.tag_sha }} web_bundle: ${{ steps.web.outputs.needed }} interface_smoke: ${{ steps.smoke.outputs.interface_smoke }} - harness_smoke: ${{ steps.harness_smoke.outputs.enabled }} staged: ${{ steps.release_state.outputs.staged }} promotable: ${{ steps.release_state.outputs.promotable }} steps: @@ -158,29 +157,6 @@ jobs: echo "::notice::$WORKER opts out of interface collection; registry publish will be skipped" fi - # The published Harness quickstart also covers every worker declared as - # a mandatory Harness dependency. Keep this derived from the manifest so - # the post-deploy smoke follows dependency changes automatically. - - name: Detect Harness quickstart smoke target - id: harness_smoke - env: - WORKER: ${{ steps.meta.outputs.worker }} - run: | - set -euo pipefail - enabled=$(WORKER="$WORKER" python3 - <<'PY' - import os - from pathlib import Path - import yaml - - worker = os.environ["WORKER"] - manifest = yaml.safe_load(Path("harness/iii.worker.yaml").read_text()) - dependencies = manifest.get("dependencies", {}) or {} - print("true" if worker == "harness" or worker in dependencies else "false") - PY - ) - echo "enabled=$enabled" >> "$GITHUB_OUTPUT" - echo "::notice::Harness quickstart smoke target=$enabled worker=$WORKER" - - name: Classify staged release id: release_state env: From 52c40607545857bf3d2cdbc88014207376fee0e1 Mon Sep 17 00:00:00 2001 From: Ytallo Layon Date: Wed, 5 Aug 2026 10:21:37 -0300 Subject: [PATCH 2/2] feat(release): resolve promotion candidates from next by default A promotion always ships the candidate behind next, so Promote Worker now only requires the worker: the version is resolved from the Registry and the Release run is located from the resulting tag. Both inputs remain as overrides for the repair paths (retrying after next moved on, dispatched Release re-runs). --- .github/workflows/promote-worker.yml | 75 ++++++++++++++++++++++++---- docs/sops/release.md | 7 ++- 2 files changed, 70 insertions(+), 12 deletions(-) diff --git a/.github/workflows/promote-worker.yml b/.github/workflows/promote-worker.yml index 035c108c3..1dfce81d3 100644 --- a/.github/workflows/promote-worker.yml +++ b/.github/workflows/promote-worker.yml @@ -8,13 +8,15 @@ on: required: true type: string version: - description: Stable semver candidate version - required: true + description: 'Candidate version (empty = whatever next resolves to; set it only to retry an interrupted promotion after next moved on)' + required: false type: string + default: '' release_run_id: - description: Release workflow run containing the candidate evidence - required: true + description: 'Release run with the candidate evidence (empty = auto-locate the run for the release tag)' + required: false type: string + default: '' permissions: actions: read @@ -27,13 +29,11 @@ concurrency: jobs: promote: - name: Promote ${{ inputs.worker }} v${{ inputs.version }} + name: Promote ${{ inputs.worker }}@${{ inputs.version || 'next' }} runs-on: ubuntu-latest timeout-minutes: 15 env: WORKER: ${{ inputs.worker }} - VERSION: ${{ inputs.version }} - RELEASE_RUN_ID: ${{ inputs.release_run_id }} API_URL: https://api.workers.iii.dev steps: - uses: actions/checkout@v5 @@ -53,15 +53,68 @@ jobs: echo "::error::worker must be a Registry slug" exit 2 } - [[ "$VERSION" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]] || { - echo "::error::version must be stable semver MAJOR.MINOR.PATCH" + + # A promotion always ships the candidate behind `next`, so the dispatch + # only has to name the worker: the version comes from the Registry and + # the Release run is located from the resulting tag (tag-push runs carry + # the tag as head_branch). Both inputs remain as overrides for the repair + # path — re-running an interrupted promotion after `next` moved on, or a + # dispatched Release re-run whose head_branch is `main`. + - name: Resolve candidate from next + id: resolve + env: + VERSION_INPUT: ${{ inputs.version }} + RUN_ID_INPUT: ${{ inputs.release_run_id }} + GH_TOKEN: ${{ github.token }} + run: | + set -euo pipefail + version="$VERSION_INPUT" + if [[ -z "$version" ]]; then + version=$(python3 - <<'PY' + import os + import sys + + sys.path.insert(0, ".github/scripts") + from registry_release import RegistryError, resolve_version + + try: + version = resolve_version(os.environ["API_URL"], os.environ["WORKER"], "next", allow_missing=True) + except RegistryError as error: + raise SystemExit(str(error)) + if not version: + raise SystemExit(f"{os.environ['WORKER']} has no candidate behind next in the Registry") + print(version) + PY + ) + echo "::notice::next resolves to ${WORKER}@${version}" + fi + [[ "$version" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]] || { + echo "::error::candidate must be stable semver MAJOR.MINOR.PATCH (got ${version}); prereleases are not promotable" exit 2 } - [[ "$RELEASE_RUN_ID" =~ ^[0-9]+$ ]] || { + + run_id="$RUN_ID_INPUT" + if [[ -z "$run_id" ]]; then + tag="${WORKER}/v${version}" + run_id=$(gh api -X GET "repos/${GITHUB_REPOSITORY}/actions/workflows/release.yml/runs" \ + -f head_branch="$tag" --jq '.workflow_runs[0].id // empty') + [[ -n "$run_id" ]] || { + echo "::error::No Release run found for tag ${tag}; pass release_run_id explicitly" + exit 2 + } + echo "::notice::candidate evidence expected in Release run ${run_id}" + fi + [[ "$run_id" =~ ^[0-9]+$ ]] || { echo "::error::release_run_id must be numeric" exit 2 } + { + echo "VERSION=${version}" + echo "RELEASE_RUN_ID=${run_id}" + } >>"$GITHUB_ENV" + echo "version=${version}" >>"$GITHUB_OUTPUT" + - name: Validate Release workflow run env: GH_TOKEN: ${{ github.token }} @@ -246,7 +299,7 @@ jobs: if: always() uses: actions/upload-artifact@v6 with: - name: promotion-${{ inputs.worker }}-${{ inputs.version }} + name: promotion-${{ inputs.worker }}-${{ steps.resolve.outputs.version }} path: | validated-candidate.json release-run.json diff --git a/docs/sops/release.md b/docs/sops/release.md index 114b12855..12379a968 100644 --- a/docs/sops/release.md +++ b/docs/sops/release.md @@ -133,7 +133,12 @@ candidate lifecycle. ### 6. Promote to latest After `candidate-ready` passes, run Actions → **Promote Worker** from `main` and -enter the worker, version, and Release run id. The workflow: +enter the worker. Version and Release run id are optional: a promotion always +ships the candidate behind `next`, so the workflow resolves the version from +the Registry and locates the Release run from the resulting tag. Fill them in +only for the repair paths — retrying an interrupted promotion after `next` +already moved on, or pointing at a dispatched Release re-run (whose run is not +findable by tag). The workflow: 1. Downloads and validates the candidate evidence and Git tag commit. 2. Confirms `next` still points to the exact candidate.