Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
75 changes: 64 additions & 11 deletions .github/workflows/promote-worker.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand All @@ -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
}
Comment on lines +91 to 94

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Reject numeric identifiers with leading zeroes.

The expression accepts values such as 01.2.3. Those values are not valid SemVer. This permits an invalid override to pass validation and fail later when the workflow uses the mismatched tag.

Proposed fix
-          [[ "$version" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]] || {
+          [[ "$version" =~ ^(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)$ ]] || {

Semantic Versioning forbids leading zeroes in normal version identifiers. (semver.org)

📝 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
[[ "$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
}
[[ "$version" =~ ^(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)$ ]] || {
echo "::error::candidate must be stable semver MAJOR.MINOR.PATCH (got ${version}); prereleases are not promotable"
exit 2
}
🤖 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/workflows/promote-worker.yml around lines 91 - 94, Update the
stable-version validation regex in the workflow’s version-check block to require
each MAJOR, MINOR, and PATCH component to be either 0 or a nonzero digit
followed by digits, rejecting values such as 01.2.3 while preserving acceptance
of valid stable MAJOR.MINOR.PATCH versions.

[[ "$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}"
Comment on lines +96 to +105

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

workflow=.github/workflows/promote-worker.yml
sed -n '96,105p' "$workflow"

# Expected: the workflow-run API query uses `branch="$tag"`.
rg -n -C 2 'actions/workflows/release\.yml/runs|head_branch|branch=' "$workflow"

Repository: iii-hq/workers

Length of output: 1561


🌐 Web query:

GitHub REST API list workflow runs query parameter branch workflow_runs documentation

💡 Result:

In the GitHub REST API, the branch parameter is used to filter workflow runs by a specific branch name [1][2]. This parameter is available for the following endpoints used to list workflow runs: 1. List workflow runs for a repository: GET /repos/{owner}/{repo}/actions/runs [1][2] 2. List workflow runs for a workflow: GET /repos/{owner}/{repo}/actions/workflows/{workflow_id}/runs [1][2] When using the branch parameter, you must provide the name of the branch associated with the push event [1][2]. It is a query parameter used to narrow the results of the request [1][2].

Citations:


Query the Release workflow runs by branch, not head_branch.

head_branch is a response field from workflow runs, while the documented query filter is branch. Use branch="${WORKER}/v${version}" so the lookup can only return runs pushed on that tag.

Proposed fix
             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')
+              -f branch="$tag" --jq '.workflow_runs[0].id // empty')
📝 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
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}"
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 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}"
🤖 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/workflows/promote-worker.yml around lines 96 - 105, Update the
Release workflow lookup in the run_id resolution block to pass the tag through
the documented branch filter instead of head_branch. Keep the existing tag
construction, run selection, and missing-run handling unchanged, using
branch="${WORKER}/v${version}" in the gh api request.

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 }}
Expand Down Expand Up @@ -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
Expand Down
24 changes: 0 additions & 24 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down Expand Up @@ -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:
Expand Down
7 changes: 6 additions & 1 deletion docs/sops/release.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
Loading