# Generated release train for Adapter-For-All-Agents.
# Regenerate from the release declaration instead of editing this file by
# hand; .github/release.lock records the source inputs that produced it.
#
# Two ways to release, one pipeline:
#
# - Weekly train: every Fri through 23:59 UTC+08:00 (Asia/Shanghai). The candidate
# is the last commit merged into the default branch before the cutoff.
# The bump is the highest `release:patch`, `release:minor` or
# `release:major` label among its pull requests, patch when none is set.
# The train runs as a rehearsal.
# - Run workflow: releases the head of the default branch (or the
# requested commit) now, at the version typed in or bumped by the chosen
# level.
#
# There is deliberately no tag trigger: `publish.yml` accepts release
# inputs only from verify runs on the default branch, and a pushed tag is
# only as trustworthy as whoever pushed it. The release job creates the
# tag itself after verification.
#
# A candidate identical to the last published release is skipped. A failed
# step stops the run until a person reruns it; a rerun finishes a partly
# uploaded version instead of skipping past it.
#
# Two files split at the trust boundary. This file is the verify side: its
# `verify` job runs on every trigger with a read-only token — it selects,
# waits on siblings, verifies, versions and packages the candidate, then
# hands the `.crate` set and the release decision over as artifacts.
# `publish.yml` (the release side) is the only file that holds the OIDC
# publishing job; it runs the default branch's own copy after this verify
# train completes, so a candidate ref can never supply the workflow that
# receives the publishing identity.
#
# Credentials: this file needs none; a read-only token suffices.
name: verify
on:
schedule:
- cron: "0 16 * * 5"
workflow_dispatch:
inputs:
version:
description: Exact version to publish, such as 0.3.0 or v0.3.0; empty bumps by level
required: false
default: ""
level:
description: Bump level when no version is given
type: choice
options: [patch, minor, major]
default: patch
candidate:
description: Commit or branch to release; empty takes the head of the default branch
required: false
default: ""
rehearse:
description: Rehearse only; nothing is pushed, tagged or uploaded
type: boolean
default: false
permissions: {}
concurrency:
group: release-train
cancel-in-progress: false
jobs:
verify:
# The job name is rendered by GitHub from this workflow file, outside the
# reach of anything a step executes: it is the signature the publish gate
# binds the release decision to. `sig=` versions the wire format so a
# stale signature refuses instead of misparsing; `sched=` signs the same
# expression RELEASE_MODE runs under; `rehearse`/`version`/`level`/`cand`
# sign the dispatch inputs. The scheduled candidate is computed after the
# name renders, so it cannot be signed — the publish job re-derives it
# from the same cutoff and requires equality. The parser lives in
# publish.yml's `gate` job — keep both sides of this format in sync.
name: verify-train | sig=1 | sched=${{ github.event_name == 'schedule' && 'dry-run' || inputs.rehearse && 'dry-run' || 'live' }} | rehearse=${{ inputs.rehearse }} | version=${{ inputs.version || '-' }} | level=${{ inputs.level }} | cand=${{ inputs.candidate || '-' }}
runs-on: ubuntu-latest
timeout-minutes: 180
permissions:
contents: read
pull-requests: read
outputs:
release: ${{ steps.version.outputs.release }}
next: ${{ steps.version.outputs.next }}
needs_set: ${{ steps.version.outputs.needs_set }}
excludes: ${{ steps.version.outputs.excludes }}
sha: ${{ steps.candidate.outputs.sha }}
branch: ${{ steps.candidate.outputs.branch }}
env:
# The train's declared mode governs scheduled runs only; a manual
# dispatch releases live unless it opted into a rehearsal — the same
# expression the signed job name reports in `sched=`.
RELEASE_MODE: ${{ github.event_name == 'schedule' && 'dry-run' || inputs.rehearse && 'dry-run' || 'live' }}
RELEASE_KIND: ${{ github.event_name == 'schedule' && 'train' || 'manual' }}
INPUT_VERSION: ${{ inputs.version }}
INPUT_LEVEL: ${{ inputs.level }}
SIBLINGS: '[{"dir":"Agent-Context-Operators","slug":"Unka-Malloc/Agent-Context-Operators","crates":["acop","acop-antigravity","acop-claude","acop-cli","acop-codex","acop-copilot","acop-cursor","acop-dsh","acop-hermes","acop-kilo-code","acop-kimi-code","acop-openclaw","acop-opencode","acop-pi"]},{"dir":"Hook-Engine","slug":"Unka-Malloc/Agent-Hook-Engine","crates":["ahe"]},{"dir":"Metera","slug":"Unka-Malloc/Metera","crates":["agent-statistics","collection-store","balance-detector","token-usage-detector"]}]'
DIR: 'Adapter-For-All-Agents'
# Optional registry-index override (repository variable); empty
# falls through to release.py's documented crates.io default.
CARGO_REGISTRY_INDEX: ${{ vars.CARGO_REGISTRY_INDEX }}
defaults:
run:
shell: bash
working-directory: Adapter-For-All-Agents
steps:
# The checkout runs candidate code (verify scripts, build scripts);
# persist-credentials must stay off so the repository token never
# reaches a file it could read. gh calls get GH_TOKEN explicitly.
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4
with:
path: ${{ env.DIR }}
fetch-depth: 0
persist-credentials: false
# Copied before the candidate checkout replaces the working tree, so the
# helper always matches this workflow.
- name: Stage the train helper
run: cp .github/release.py "$RUNNER_TEMP/release.py"
- name: Select the candidate
id: candidate
env:
GH_TOKEN: ${{ github.token }}
OVERRIDE: ${{ inputs.candidate }}
run: |
default_branch="$(gh api "repos/$GITHUB_REPOSITORY" --jq .default_branch)"
python3 "$RUNNER_TEMP/release.py" candidate --weekday=fri --time=23:59 \
--utc-offset=+08:00 --default-branch="$default_branch" --override="$OVERRIDE"
- uses: dtolnay/rust-toolchain@b725eda525362e7cf34af9a7243cac1e3f01a7ec # stable
with:
components: rustfmt, clippy
- name: Install the pinned toolchain
run: |
if [ -f rust-toolchain.toml ] || [ -f rust-toolchain ]; then
rustup toolchain install
fi
- name: Wait for cross-repository dependencies
id: deps
run: python3 "$RUNNER_TEMP/release.py" deps --wait-minutes="${{ env.RELEASE_MODE == 'live' && '150' || '5' }}"
- name: Check out Agent-Context-Operators at its resolved release
if: steps.deps.outputs.ready == 'true'
uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4
with:
repository: Unka-Malloc/Agent-Context-Operators
ref: ${{ steps.deps.outputs['ref_Agent-Context-Operators'] }}
path: Agent-Context-Operators
persist-credentials: false
- name: Check out Hook-Engine at its resolved release
if: steps.deps.outputs.ready == 'true'
uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4
with:
repository: Unka-Malloc/Agent-Hook-Engine
ref: ${{ steps.deps.outputs['ref_Hook-Engine'] }}
path: Hook-Engine
persist-credentials: false
- name: Check out Metera at its resolved release
if: steps.deps.outputs.ready == 'true'
uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4
with:
repository: Unka-Malloc/Metera
ref: ${{ steps.deps.outputs['ref_Metera'] }}
path: Metera
persist-credentials: false
- name: Compute the release version
id: version
if: steps.deps.outputs.ready == 'true'
env:
GH_TOKEN: ${{ github.token }}
CANDIDATE: ${{ steps.candidate.outputs.sha }}
BRANCH: ${{ steps.candidate.outputs.branch }}
run: python3 "$RUNNER_TEMP/release.py" version --candidate="$CANDIDATE" --branch="$BRANCH"
- name: Verify
if: steps.version.outputs.release == 'true'
run: |
scripts/verify.sh
- name: Check surface-freshness
if: steps.version.outputs.release == 'true'
run: python3 $RUNNER_TEMP/release.py freshness --window-days=14
# Every mode runs the release-shaped bump: the manifest rewriter is
# hand-written, and a rehearsal that skipped it would let a defect
# surface for the first time inside the OIDC job — after the approval
# gate. That job still recreates the bump from the bound commit with
# its own staged helper, so nothing a verify-runner produced crosses
# the boundary either way.
- name: Apply the release version
if: >-
steps.version.outputs.release == 'true' &&
steps.version.outputs.needs_set == 'true'
env:
NEXT: ${{ steps.version.outputs.next }}
run: |
python3 "$RUNNER_TEMP/release.py" set-version "$NEXT"
cargo update --workspace
# Rehearsals build the .crate set with --no-verify — manifest
# normalization and member-pin resolution run for real, only the
# compile and the upload dry-run stay unexercised — then restore the
# tree the bump rewrote. Live runs pay for `cargo publish --dry-run`.
# No artifacts ship to the OIDC job either way: it repackages the
# bound commit itself with `cargo package --no-verify`, collecting
# manifest-declared files without compiling or running build scripts.
- name: Package
if: steps.version.outputs.release == 'true'
env:
EXCLUDES: ${{ steps.version.outputs.excludes }}
run: |
args=()
for crate in $EXCLUDES; do args+=(--exclude "$crate"); done
if [ "$RELEASE_MODE" = "live" ]; then
cargo publish --workspace --dry-run --allow-dirty "${args[@]}"
else
cargo package --workspace --allow-dirty --no-verify "${args[@]}"
# Restore what the rehearsal rewrote: tracked manifests and the
# lockfile, plus a Cargo.lock created where none was tracked.
git checkout -- .
git clean -fq -- Cargo.lock
fi
# The release train (default branch) reads this data-only file for the
# candidate pointer and a skip pre-filter — the only fields the gate
# consumes, so they are the only fields written. It lands for every
# trigger so the gate can short-circuit cleanly on unqualified
# candidates: a deps-not-ready or no-release run still produces a
# parseable record. Nothing here authorizes a publish: the gate
# derives kind and mode from GitHub-minted run metadata (the event and
# this job's signed name), and the release job recomputes the version
# decision from the bound commit. Values still expand from immutable
# run metadata inline — never from a variable candidate code could
# persist through $GITHUB_ENV.
- name: Record the release inputs
if: always()
run: |
mkdir -p target/release-inputs
cat > target/release-inputs/env <<EOF
sha=${{ steps.candidate.outputs.sha }}
release=${{ steps.version.outputs.release || 'false' }}
EOF
- uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
if: always()
with:
name: release-inputs
path: ${{ env.DIR }}/target/release-inputs/
retention-days: 7
if-no-files-found: error