Skip to content

Refine optional helper approval experience (#67) #59

Refine optional helper approval experience (#67)

Refine optional helper approval experience (#67) #59

Workflow file for this run

name: Publish Yield packages
on:
push:
branches: [main]
workflow_dispatch:
inputs:
version:
description: Existing stable tag version without the leading v
type: string
required: true
permissions:
contents: read
concurrency:
group: packages-${{ github.event_name == 'push' && 'canary' || 'stable' }}
cancel-in-progress: false
jobs:
resolve:
if: github.repository == 'operatorstack/yield'
runs-on: ubuntu-latest
outputs:
publish: ${{ steps.release.outputs.publish }}
channel: ${{ steps.release.outputs.channel }}
version: ${{ steps.release.outputs.version }}
dist_tag: ${{ steps.release.outputs.dist_tag }}
source_sha: ${{ steps.release.outputs.source_sha }}
source_epoch: ${{ steps.release.outputs.source_epoch }}
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
with:
fetch-depth: 0
persist-credentials: false
ref: ${{ github.event_name == 'workflow_dispatch' && format('v{0}', inputs.version) || github.sha }}
- id: release
name: Resolve immutable source, version, and channel
env:
REQUESTED_VERSION: ${{ inputs.version }}
shell: bash
run: |
set -euo pipefail
source_sha="$(git rev-parse HEAD)"
if [[ "$GITHUB_EVENT_NAME" == push ]]; then
committed_at="$(git show -s --format=%cd --date=format:%Y%m%d%H%M%S HEAD)"
version="0.0.0-canary.${committed_at}.$(git rev-parse --short=12 HEAD)"
channel=canary
dist_tag=canary
else
version="$REQUESTED_VERSION"
tag="v${version}"
[[ "$version" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]
test "$(git rev-list -n 1 "v${version}")" = "$source_sha"
channel=stable
dist_tag=latest
fi
echo "publish=true" >> "$GITHUB_OUTPUT"
echo "channel=$channel" >> "$GITHUB_OUTPUT"
echo "version=$version" >> "$GITHUB_OUTPUT"
echo "dist_tag=$dist_tag" >> "$GITHUB_OUTPUT"
echo "source_sha=$source_sha" >> "$GITHUB_OUTPUT"
echo "source_epoch=$(git show -s --format=%ct HEAD)" >> "$GITHUB_OUTPUT"
build:
needs: resolve
if: needs.resolve.outputs.publish == 'true'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
with:
fetch-depth: 0
persist-credentials: false
ref: ${{ needs.resolve.outputs.source_sha }}
- uses: actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e # v7
with:
go-version-file: go.mod
cache: true
- uses: actions/setup-node@249970729cb0ef3589644e2896645e5dc5ba9c38 # v6
with:
node-version: "24"
package-manager-cache: false
- uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6
with:
python-version: "3.12"
- name: Verify source
run: |
go test ./...
node --test packaging/*.test.mjs sdk/typescript/bin/*.test.mjs
python -m unittest discover -s sdk/python -p 'test_*.py'
- name: Build immutable runtimes
env:
VERSION: ${{ needs.resolve.outputs.version }}
shell: bash
run: |
set -euo pipefail
mkdir -p dist/bin
for spec in darwin/amd64 darwin/arm64 linux/amd64 linux/arm64 windows/amd64 windows/arm64; do
goos="${spec%/*}"
goarch="${spec#*/}"
suffix=""
if [[ "$goos" == windows ]]; then suffix=.exe; fi
CGO_ENABLED=0 GOOS="$goos" GOARCH="$goarch" \
go build -trimpath -ldflags "-s -w -X main.version=${VERSION}" \
-o "dist/bin/yskill-${goos}-${goarch}${suffix}" ./cmd/yskill
done
node packaging/assemble.mjs --version "$VERSION" --binaries dist/bin --output dist/packages
- name: Build Python wheels
if: needs.resolve.outputs.channel == 'stable'
env:
SOURCE_DATE_EPOCH: ${{ needs.resolve.outputs.source_epoch }}
VERSION: ${{ needs.resolve.outputs.version }}
shell: bash
run: |
set -euo pipefail
python -m pip install --disable-pip-version-check build==1.3.0 setuptools==80.9.0 wheel==0.45.1
mkdir -p dist/pypi
for directory in dist/packages/python/*; do
python -m build --wheel --no-isolation --outdir "$GITHUB_WORKSPACE/dist/pypi" "$directory"
done
node packaging/pypi-release.mjs inspect --version "$VERSION" --dist dist/pypi
- name: Build Rust crates
if: needs.resolve.outputs.channel == 'stable'
env:
VERSION: ${{ needs.resolve.outputs.version }}
shell: bash
run: |
set -euo pipefail
chmod 0644 dist/packages/rust/runtime/*/runtime/*
node packaging/crates-release.mjs inspect --version "$VERSION" --rust dist/packages/rust
mkdir -p dist/crates
for directory in dist/packages/rust/runtime/*; do
cargo package --manifest-path "$directory/Cargo.toml"
name="$(sed -n 's/^name = "\([^"]*\)"/\1/p' "$directory/Cargo.toml" | head -n 1)"
cp "$directory/target/package/${name}-${VERSION}.crate" dist/crates/
done
test "$(find dist/crates -maxdepth 1 -name '*.crate' | wc -l | tr -d ' ')" = 6
- name: Pack immutable npm release unit
shell: bash
run: |
set -euo pipefail
node packaging/npm-release.mjs --source dist/packages/npm --output dist/release-unit/npm --source-sha "${{ needs.resolve.outputs.source_sha }}"
- name: Assemble immutable release unit
env:
CHANNEL: ${{ needs.resolve.outputs.channel }}
run: |
mkdir -p dist/release-unit
cp dist/packages/SHA256SUMS.json dist/release-unit/
if [[ "$CHANNEL" == stable ]]; then
cp -R dist/pypi dist/release-unit/pypi
cp -R dist/packages/rust dist/release-unit/rust
cp -R dist/crates dist/release-unit/crates
fi
- uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
with:
name: packages-${{ needs.resolve.outputs.version }}-${{ needs.resolve.outputs.source_sha }}
path: dist/release-unit/
if-no-files-found: error
npm:
needs: [resolve, build]
runs-on: ubuntu-latest
environment: ${{ needs.resolve.outputs.channel == 'stable' && 'npm-production' || 'npm-canary' }}
permissions:
contents: read
id-token: write
steps:
- uses: actions/download-artifact@634f93cb2916e3fdff6788551b99b062d0335ce0 # v5
with:
name: packages-${{ needs.resolve.outputs.version }}-${{ needs.resolve.outputs.source_sha }}
path: dist/release-unit
- uses: actions/setup-node@249970729cb0ef3589644e2896645e5dc5ba9c38 # v6
with:
node-version: "24"
registry-url: https://registry.npmjs.org
package-manager-cache: false
- name: Pin npm trusted-publishing client
run: npm install --global npm@12.0.2
- name: Publish platform runtimes
env:
DIST_TAG: ${{ needs.resolve.outputs.dist_tag }}
VERSION: ${{ needs.resolve.outputs.version }}
shell: bash
run: |
set -euo pipefail
jq -c '.archives[] | select(.name != "@operatorstack/yield" and .name != "@operatorstack/create-yield")' dist/release-unit/npm/npm-release.json | while read -r archive; do
package="$(jq -r '.name' <<< "$archive")"
file="$(jq -r '.file' <<< "$archive")"
if npm view "${package}@${VERSION}" version >/dev/null 2>&1; then
echo "${package}@${VERSION} already exists"
else
npm publish "dist/release-unit/npm/${file}" --tag "$DIST_TAG"
fi
done
- name: Publish SDK and CLI
env:
DIST_TAG: ${{ needs.resolve.outputs.dist_tag }}
VERSION: ${{ needs.resolve.outputs.version }}
shell: bash
run: |
set -euo pipefail
if npm view "@operatorstack/yield@${VERSION}" version >/dev/null 2>&1; then
echo "@operatorstack/yield@${VERSION} already exists"
else
file="$(jq -r '.archives[] | select(.name == "@operatorstack/yield") | .file' dist/release-unit/npm/npm-release.json)"
npm publish "dist/release-unit/npm/${file}" --tag "$DIST_TAG"
fi
- name: Publish npm initializer
env:
DIST_TAG: ${{ needs.resolve.outputs.dist_tag }}
VERSION: ${{ needs.resolve.outputs.version }}
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
shell: bash
run: |
set -euo pipefail
if npm view "@operatorstack/create-yield@${VERSION}" version >/dev/null 2>&1; then
echo "@operatorstack/create-yield@${VERSION} already exists"
else
file="$(jq -r '.archives[] | select(.name == "@operatorstack/create-yield") | .file' dist/release-unit/npm/npm-release.json)"
if [[ -n "${NPM_TOKEN:-}" ]]; then
NODE_AUTH_TOKEN="$NPM_TOKEN" npm publish "dist/release-unit/npm/${file}" --tag "$DIST_TAG"
else
npm publish "dist/release-unit/npm/${file}" --tag "$DIST_TAG"
fi
fi
- name: Verify complete npm release unit
env:
VERSION: ${{ needs.resolve.outputs.version }}
shell: bash
run: |
set -euo pipefail
jq -r '.archives[].name' dist/release-unit/npm/npm-release.json | while read -r package; do
for attempt in {1..12}; do
if [[ "$(npm view "${package}@${VERSION}" version 2>/dev/null || true)" == "$VERSION" ]]; then break; fi
test "$attempt" -lt 12
sleep 10
done
done
- name: Smoke test npm initializer
env:
VERSION: ${{ needs.resolve.outputs.version }}
shell: bash
run: |
set -euo pipefail
root="$RUNNER_TEMP/create-yield-smoke"
mkdir -p "$root"
npm create "@operatorstack/yield@${VERSION}" -- \
--root "$root" --agent codex --dry-run
test ! -e "$root/skills"
pypi:
needs: [resolve, build]
if: needs.resolve.outputs.channel == 'stable'
runs-on: ubuntu-latest
environment: pypi-production
permissions:
contents: read
id-token: write
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
with:
persist-credentials: false
ref: ${{ needs.resolve.outputs.source_sha }}
- uses: actions/download-artifact@634f93cb2916e3fdff6788551b99b062d0335ce0 # v5
with:
name: packages-${{ needs.resolve.outputs.version }}-${{ needs.resolve.outputs.source_sha }}
path: dist/release-unit
- uses: actions/setup-node@249970729cb0ef3589644e2896645e5dc5ba9c38 # v6
with:
node-version: "24"
package-manager-cache: false
- id: prepare
name: Refuse PyPI drift and select missing wheels
env:
VERSION: ${{ needs.resolve.outputs.version }}
run: >-
node packaging/pypi-release.mjs prepare
--version "$VERSION"
--dist dist/release-unit/pypi
--upload dist/pypi-upload
--output "$GITHUB_OUTPUT"
- name: Publish Python wheels with trusted publishing
if: steps.prepare.outputs.publish == 'true'
uses: pypa/gh-action-pypi-publish@dc37677b2e1c63e2034f94d8a5b11f265b73ba33 # v1.14.2
with:
packages-dir: dist/pypi-upload
attestations: true
verbose: true
- name: Verify complete PyPI release unit
env:
VERSION: ${{ needs.resolve.outputs.version }}
run: >-
node packaging/pypi-release.mjs verify
--version "$VERSION"
--dist dist/release-unit/pypi
--attempts 12
--delay-ms 10000
crates:
needs: [resolve, build]
if: needs.resolve.outputs.channel == 'stable'
runs-on: ubuntu-latest
environment: crates-production
permissions:
contents: read
id-token: write
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
with:
persist-credentials: false
ref: ${{ needs.resolve.outputs.source_sha }}
- uses: actions/download-artifact@634f93cb2916e3fdff6788551b99b062d0335ce0 # v5
with:
name: packages-${{ needs.resolve.outputs.version }}-${{ needs.resolve.outputs.source_sha }}
path: dist/release-unit
- id: auth
name: Request a short-lived crates.io token
uses: rust-lang/crates-io-auth-action@c6f97d42243bad5fab37ca0427f495c86d5b1a18 # v1.0.5
- name: Publish complete Rust release unit
env:
CARGO_REGISTRY_TOKEN: ${{ steps.auth.outputs.token }}
VERSION: ${{ needs.resolve.outputs.version }}
shell: bash
run: |
set -euo pipefail
publish_if_missing() {
local directory="$1"
local name
name="$(sed -n 's/^name = "\([^"]*\)"/\1/p' "$directory/Cargo.toml" | head -n 1)"
local archive="dist/release-unit/crates/${name}-${VERSION}.crate"
local state
state="$(node packaging/crates-release.mjs status --version "$VERSION" --name "$name" --archive "$archive")"
if [[ "$state" == matched ]]; then
echo "${name}@${VERSION} already exists with the release-unit checksum"
else
test "$state" = missing
(cd "$directory" && cargo publish)
fi
}
for directory in dist/release-unit/rust/runtime/*; do
publish_if_missing "$directory"
done
for attempt in {1..18}; do
missing=0
for directory in dist/release-unit/rust/runtime/*; do
name="$(sed -n 's/^name = "\([^"]*\)"/\1/p' "$directory/Cargo.toml" | head -n 1)"
archive="dist/release-unit/crates/${name}-${VERSION}.crate"
if [[ "$(node packaging/crates-release.mjs status --version "$VERSION" --name "$name" --archive "$archive")" != matched ]]; then
missing=1
fi
done
if [[ "$missing" == 0 ]]; then break; fi
test "$attempt" -lt 18
sleep 10
done
mkdir -p dist/crates-receipt
cp dist/release-unit/crates/*.crate dist/crates-receipt/
cargo package --manifest-path dist/release-unit/rust/yieldskill/Cargo.toml
cp "dist/release-unit/rust/yieldskill/target/package/yieldskill-${VERSION}.crate" dist/crates-receipt/
main_state="$(node packaging/crates-release.mjs status \
--version "$VERSION" \
--name yieldskill \
--archive "dist/crates-receipt/yieldskill-${VERSION}.crate")"
if [[ "$main_state" == matched ]]; then
echo "yieldskill@${VERSION} already exists with the publisher receipt checksum"
else
test "$main_state" = missing
cargo publish --manifest-path dist/release-unit/rust/yieldskill/Cargo.toml
fi
node packaging/crates-release.mjs verify \
--version "$VERSION" \
--archives dist/crates-receipt \
--attempts 18 \
--delay-ms 10000
- uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
with:
name: crates-${{ needs.resolve.outputs.version }}-${{ needs.resolve.outputs.source_sha }}
path: dist/crates-receipt/
if-no-files-found: error
selfhost-canary:
name: Release skill against exact canary
needs: [resolve, npm]
if: needs.resolve.outputs.channel == 'canary'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
with:
persist-credentials: false
ref: ${{ needs.resolve.outputs.source_sha }}
- uses: actions/setup-node@249970729cb0ef3589644e2896645e5dc5ba9c38 # v6
with:
node-version: "24"
package-manager-cache: false
- name: Install the exact published canary without changing repository state
env:
VERSION: ${{ needs.resolve.outputs.version }}
run: |
npm ci --ignore-scripts
npm install --no-save --package-lock=false --ignore-scripts "@operatorstack/yield@${VERSION}"
test "$(npm exec -- yskill version | awk '{print $2}')" = "$VERSION"
- name: Run the release skill contract against the canary SDK
run: |
npm run test:selfhost
journal="$RUNNER_TEMP/release-yield-journal.json"
output="$RUNNER_TEMP/release-yield-output.json"
printf '%s\n' '{"run_id":"canary-smoke","skill":{"name":"release-yield","digest":"sha256:canary"}}' > "$journal"
YIELD_JOURNAL="$journal" node skills/release-yield/main.ts > "$output"
node -e '
const output = require(process.argv[1]);
if (output.type !== "request" || output.envelope.request.id !== "select-mode") process.exit(1);
' "$output"