Skip to content
Merged
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
195 changes: 195 additions & 0 deletions .github/workflows/alpha-release.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,195 @@
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 }}
Comment on lines +97 to +101

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🔒 Security & Privacy | 🔴 Critical | 🏗️ Heavy lift

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== locate workflow files =="
fd -a 'alpha-release\.yml$|.*release.*\.ya?ml$|.*workflow.*\.ya?ml$' .github/workflows | sed 's#^\./##'

echo "== relevant workflow excerpt =="
if [ -f .github/workflows/alpha-release.yml ]; then
  nl -ba .github/workflows/alpha-release.yml | sed -n '1,240p'
fi

echo "== search manifest_version.py and _lib.py =="
git ls-files | grep -E '(^|/)manifest_version\.py$|(^|/)_lib\.py$|\.github/scripts/' | sed -n '1,200p'

Repository: iii-hq/workers

Length of output: 416


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== alpha-release.yml excerpt with line numbers =="
python3 - <<'PY'
from pathlib import Path
p = Path('.github/workflows/alpha-release.yml')
for i, line in enumerate(p.read_text().splitlines(), 1):
    if 1 <= i <= 250:
        print(f"{i:4d}\t{line}")
PY

echo "== manifest_version.py candidates =="
git ls-files | grep -E '(^|/)manifest_version\.py$|(^|/)_lib\.py$|(^|/)\.github/scripts/[^/]+$' || true

echo "== all manifest_version.py locations =="
python3 - <<'PY'
from pathlib import Path
import subprocess
files = subprocess.check_output(['git', 'ls-files'], text=True).splitlines()
for f in files:
    if f.endswith('manifest_version.py') or f.endswith('_lib.py'):
        print(f)
PY

Repository: iii-hq/workers

Length of output: 7929


🌐 Web query:

actions/checkout persist-credentials default true README

💡 Result:

The persist-credentials input for the actions/checkout GitHub Action determines whether the authentication token or SSH key used for the checkout is configured in the local git configuration [1][2]. By default, persist-credentials is set to true [1][2]. When enabled, this allows subsequent steps in the same job to execute authenticated git commands using those credentials [3][4]. The action removes these credentials from the local git configuration during the post-job cleanup phase [3][5]. If you wish to prevent these credentials from being stored in the local git configuration, you can set persist-credentials to false [3][6]. This is often recommended for security purposes to ensure that subsequent steps in a workflow cannot access the repository token if they do not require it [7].

Citations:


Do not execute release tooling from the caller-selected ref.

The workflow checks out inputs.source_ref but later runs .github/scripts/manifest_version.py from actions/checkout’s default persisted GITHUB_TOKEN; after that, Git commands can push arbitrary refs before the explicit git push origin "$TAG". Keep checkout credentials disabled for the selected source, fetch the trusted release tooling/_lib.py from main, and use a scoped push token only where the tag is pushed.

🧰 Tools
🪛 zizmor (1.28.0)

[warning] 97-101: credential persistence through GitHub Actions artifacts (artipacked): does not set persist-credentials: false

(artipacked)

🤖 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/alpha-release.yml around lines 97 - 101, Update the
Checkout source ref step to disable persisted credentials, then explicitly fetch
the trusted release tooling and _lib.py from main before invoking
manifest_version.py. Ensure release operations use those main-sourced scripts
rather than files from inputs.source_ref, and configure a narrowly scoped push
token only for the explicit git push of the release tag.

Source: Linters/SAST tools


- 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"
17 changes: 17 additions & 0 deletions docs/sops/release.md
Original file line number Diff line number Diff line change
Expand Up @@ -173,6 +173,23 @@ 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 `-<word>.<number>`.

### 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/<number>/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: `<worker>/vX.Y.Z-dry-run.1` (parsed by `parse_release_tag.py`).
Expand Down
Loading