mirror of
https://github.com/lahfir/agent-desktop.git
synced 2026-08-01 11:49:20 +00:00
448 lines
16 KiB
YAML
448 lines
16 KiB
YAML
name: Release
|
|
|
|
on:
|
|
push:
|
|
branches: [main]
|
|
workflow_dispatch:
|
|
inputs:
|
|
tag_name:
|
|
description: Release tag to build and publish, for example v0.2.0
|
|
required: true
|
|
type: string
|
|
version:
|
|
description: Semver version without the v prefix, for example 0.2.0
|
|
required: true
|
|
type: string
|
|
publish_npm:
|
|
description: Publish the npm package after GitHub assets are uploaded
|
|
required: true
|
|
default: true
|
|
type: boolean
|
|
publish_skills:
|
|
description: Publish skills after the release is built
|
|
required: true
|
|
default: true
|
|
type: boolean
|
|
|
|
# One release run per commit — never cancel, just queue.
|
|
concurrency:
|
|
group: release-${{ github.sha }}
|
|
|
|
# Deny all permissions at workflow level; each job declares only what it needs.
|
|
permissions: {}
|
|
|
|
env:
|
|
CARGO_TERM_COLOR: always
|
|
|
|
jobs:
|
|
release-please:
|
|
name: Release Please
|
|
runs-on: ubuntu-latest
|
|
timeout-minutes: 10
|
|
permissions:
|
|
contents: write
|
|
pull-requests: write
|
|
outputs:
|
|
release_created: ${{ steps.release.outputs.release_created || steps.manual.outputs.release_created }}
|
|
tag_name: ${{ steps.release.outputs.tag_name || steps.manual.outputs.tag_name }}
|
|
version: ${{ steps.release.outputs.major && format('{0}.{1}.{2}', steps.release.outputs.major, steps.release.outputs.minor, steps.release.outputs.patch) || steps.manual.outputs.version }}
|
|
publish_npm: ${{ steps.manual.outputs.publish_npm || 'true' }}
|
|
publish_skills: ${{ steps.manual.outputs.publish_skills || steps.release.outputs.release_created }}
|
|
steps:
|
|
- uses: googleapis/release-please-action@45996ed1f6d02564a971a2fa1b5860e934307cf7 # v5.0.0
|
|
if: github.event_name != 'workflow_dispatch'
|
|
id: release
|
|
with:
|
|
config-file: release-please-config.json
|
|
manifest-file: .release-please-manifest.json
|
|
|
|
- name: Use manual release inputs
|
|
if: github.event_name == 'workflow_dispatch'
|
|
id: manual
|
|
env:
|
|
TAG_NAME: ${{ inputs.tag_name }}
|
|
VERSION: ${{ inputs.version }}
|
|
PUBLISH_NPM: ${{ inputs.publish_npm }}
|
|
PUBLISH_SKILLS: ${{ inputs.publish_skills }}
|
|
run: |
|
|
if [[ ! "$VERSION" =~ ^[0-9]+\.[0-9]+\.[0-9]+(-[0-9A-Za-z.-]+)?$ ]]; then
|
|
echo "Invalid version: ${VERSION}" >&2
|
|
exit 1
|
|
fi
|
|
if [[ "$TAG_NAME" != "v${VERSION}" ]]; then
|
|
echo "Tag ${TAG_NAME} must match version ${VERSION}" >&2
|
|
exit 1
|
|
fi
|
|
{
|
|
echo "release_created=true"
|
|
echo "tag_name=${TAG_NAME}"
|
|
echo "version=${VERSION}"
|
|
echo "publish_npm=${PUBLISH_NPM}"
|
|
echo "publish_skills=${PUBLISH_SKILLS}"
|
|
} >> "$GITHUB_OUTPUT"
|
|
|
|
- name: Sync Cargo.lock on release PR
|
|
if: github.event_name != 'workflow_dispatch' && steps.release.outputs.release_created != 'true'
|
|
env:
|
|
GH_TOKEN: ${{ github.token }}
|
|
GH_REPO: ${{ github.repository }}
|
|
run: |
|
|
set -euo pipefail
|
|
BRANCH="release-please--branches--main--components--agent-desktop"
|
|
OPEN_RELEASE_PRS=$(gh pr list \
|
|
--head "$BRANCH" \
|
|
--state open \
|
|
--json number \
|
|
--jq 'length')
|
|
if [ "$OPEN_RELEASE_PRS" = "0" ]; then
|
|
echo "No open release PR branch to sync"
|
|
exit 0
|
|
fi
|
|
|
|
git init -b main release-pr
|
|
cd release-pr
|
|
git remote add origin "https://x-access-token:${GH_TOKEN}@github.com/${GH_REPO}.git"
|
|
git fetch --depth=1 origin "$BRANCH"
|
|
git checkout -B "$BRANCH" FETCH_HEAD
|
|
|
|
rustup show
|
|
VERSION=$(sed -n 's/.*version *= *"\([^"]*\)".*/\1/p' Cargo.toml | head -1)
|
|
cargo update -p agent-desktop --precise "$VERSION"
|
|
scripts/check-release-consistency.sh
|
|
|
|
if git diff --quiet -- Cargo.lock; then
|
|
echo "Cargo.lock already matches release version ${VERSION}"
|
|
exit 0
|
|
fi
|
|
|
|
git config user.name "github-actions[bot]"
|
|
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
|
|
git add Cargo.lock
|
|
git commit -m "chore: sync cargo lock for release ${VERSION}"
|
|
git push origin "HEAD:${BRANCH}"
|
|
|
|
build:
|
|
name: Build (${{ matrix.target }})
|
|
needs: release-please
|
|
if: needs.release-please.outputs.release_created == 'true'
|
|
runs-on: macos-latest
|
|
timeout-minutes: 30
|
|
permissions:
|
|
contents: read
|
|
strategy:
|
|
fail-fast: false
|
|
matrix:
|
|
target:
|
|
- aarch64-apple-darwin
|
|
- x86_64-apple-darwin
|
|
steps:
|
|
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
|
with:
|
|
ref: ${{ needs.release-please.outputs.tag_name }}
|
|
|
|
- name: Install Rust toolchain
|
|
run: |
|
|
rustup show
|
|
rustup target add ${{ matrix.target }}
|
|
|
|
- name: Cache cargo registry
|
|
uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
|
|
with:
|
|
path: |
|
|
~/.cargo/registry/index/
|
|
~/.cargo/registry/cache/
|
|
~/.cargo/git/db/
|
|
key: ${{ runner.os }}-${{ matrix.target }}-cargo-${{ hashFiles('Cargo.lock', 'Cargo.toml', 'crates/**/Cargo.toml', 'src/Cargo.toml') }}
|
|
restore-keys: ${{ runner.os }}-${{ matrix.target }}-cargo-
|
|
|
|
- name: Build release binary
|
|
run: cargo build --locked --release --target ${{ matrix.target }}
|
|
|
|
- name: Check binary size
|
|
run: |
|
|
SIZE=$(stat -f%z target/${{ matrix.target }}/release/agent-desktop)
|
|
LIMIT=$((15 * 1024 * 1024))
|
|
echo "Binary size: $(du -sh target/${{ matrix.target }}/release/agent-desktop | cut -f1)"
|
|
if [ "$SIZE" -gt "$LIMIT" ]; then
|
|
echo "FAIL: binary exceeds 15MB limit (${SIZE} bytes)"
|
|
exit 1
|
|
fi
|
|
|
|
- name: Create tarball
|
|
env:
|
|
VERSION: ${{ needs.release-please.outputs.version }}
|
|
run: |
|
|
TARBALL="agent-desktop-v${VERSION}-${{ matrix.target }}.tar.gz"
|
|
tar -czf "${TARBALL}" -C target/${{ matrix.target }}/release agent-desktop
|
|
shasum -a 256 "${TARBALL}" > "${TARBALL}.sha256"
|
|
|
|
- name: Upload artifact
|
|
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
|
with:
|
|
name: binary-${{ matrix.target }}
|
|
path: |
|
|
agent-desktop-v*.tar.gz
|
|
agent-desktop-v*.tar.gz.sha256
|
|
retention-days: 1
|
|
|
|
# FFI cdylib for language-binding consumers (Python/Swift/Go/Ruby/Node/C).
|
|
# Ships alongside CLI tarballs on the same release tag; npm stays CLI-only.
|
|
build-ffi:
|
|
name: Build FFI (${{ matrix.target }})
|
|
needs: release-please
|
|
if: needs.release-please.outputs.release_created == 'true'
|
|
runs-on: ${{ matrix.runner }}
|
|
timeout-minutes: 30
|
|
permissions:
|
|
contents: read
|
|
strategy:
|
|
fail-fast: false
|
|
matrix:
|
|
include:
|
|
- target: aarch64-apple-darwin
|
|
runner: macos-latest
|
|
archive: tar.gz
|
|
lib_name: libagent_desktop_ffi.dylib
|
|
- target: x86_64-apple-darwin
|
|
runner: macos-latest
|
|
archive: tar.gz
|
|
lib_name: libagent_desktop_ffi.dylib
|
|
- target: x86_64-unknown-linux-gnu
|
|
runner: ubuntu-22.04
|
|
archive: tar.gz
|
|
lib_name: libagent_desktop_ffi.so
|
|
- target: aarch64-unknown-linux-gnu
|
|
runner: ubuntu-22.04-arm
|
|
archive: tar.gz
|
|
lib_name: libagent_desktop_ffi.so
|
|
- target: x86_64-pc-windows-msvc
|
|
runner: windows-latest
|
|
archive: zip
|
|
lib_name: agent_desktop_ffi.dll
|
|
steps:
|
|
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
|
with:
|
|
ref: ${{ needs.release-please.outputs.tag_name }}
|
|
|
|
- name: Install Rust toolchain
|
|
run: |
|
|
rustup show
|
|
rustup target add ${{ matrix.target }}
|
|
|
|
- name: Cache cargo registry
|
|
uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
|
|
with:
|
|
path: |
|
|
~/.cargo/registry/index/
|
|
~/.cargo/registry/cache/
|
|
~/.cargo/git/db/
|
|
key: ${{ runner.os }}-${{ matrix.target }}-ffi-cargo-${{ hashFiles('Cargo.lock', 'Cargo.toml', 'crates/**/Cargo.toml', 'src/Cargo.toml') }}
|
|
restore-keys: ${{ runner.os }}-${{ matrix.target }}-ffi-cargo-
|
|
|
|
- name: Build FFI cdylib (release-ffi profile)
|
|
run: cargo build --locked --profile release-ffi -p agent-desktop-ffi --target ${{ matrix.target }}
|
|
|
|
# build.rs bakes install_name=@rpath/... ; a regression here silently
|
|
# breaks Swift/SPM consumers, so verify before shipping.
|
|
- name: Verify macOS install_name
|
|
if: runner.os == 'macOS'
|
|
shell: bash
|
|
run: |
|
|
ACTUAL=$(otool -D target/${{ matrix.target }}/release-ffi/${{ matrix.lib_name }} | tail -1)
|
|
EXPECTED="@rpath/libagent_desktop_ffi.dylib"
|
|
if [ "$ACTUAL" != "$EXPECTED" ]; then
|
|
echo "FAIL: install_name is '$ACTUAL', expected '$EXPECTED'"
|
|
exit 1
|
|
fi
|
|
echo "OK: install_name = $ACTUAL"
|
|
|
|
- name: Stage tarball contents
|
|
shell: bash
|
|
env:
|
|
VERSION: ${{ needs.release-please.outputs.version }}
|
|
run: |
|
|
STAGE="agent-desktop-ffi-v${VERSION}-${{ matrix.target }}"
|
|
mkdir -p "$STAGE/lib" "$STAGE/include"
|
|
cp "target/${{ matrix.target }}/release-ffi/${{ matrix.lib_name }}" "$STAGE/lib/"
|
|
cp crates/ffi/include/agent_desktop.h "$STAGE/include/"
|
|
cp LICENSE "$STAGE/"
|
|
cat > "$STAGE/README.md" <<EOF
|
|
# agent-desktop FFI cdylib — v${VERSION} (${{ matrix.target }})
|
|
|
|
- \`lib/${{ matrix.lib_name }}\` — shared library
|
|
- \`include/agent_desktop.h\` — committed C ABI header
|
|
- \`LICENSE\` — Apache-2.0
|
|
|
|
Docs: https://github.com/lahfir/agent-desktop/tree/main/skills/agent-desktop-ffi
|
|
|
|
macOS \`install_name\` is \`@rpath/libagent_desktop_ffi.dylib\` — linking
|
|
consumers need \`-rpath\` pointing at the dylib's directory; \`dlopen\` /
|
|
ctypes callers resolve by path and need nothing.
|
|
EOF
|
|
echo "STAGE=$STAGE" >> "$GITHUB_ENV"
|
|
|
|
- name: Create archive (Unix)
|
|
if: runner.os != 'Windows'
|
|
shell: bash
|
|
run: |
|
|
tar -czf "${STAGE}.tar.gz" "${STAGE}"
|
|
shasum -a 256 "${STAGE}.tar.gz" > "${STAGE}.tar.gz.sha256"
|
|
|
|
- name: Create archive (Windows)
|
|
if: runner.os == 'Windows'
|
|
shell: pwsh
|
|
run: |
|
|
Compress-Archive -Path "${env:STAGE}" -DestinationPath "${env:STAGE}.zip"
|
|
$hash = (Get-FileHash "${env:STAGE}.zip" -Algorithm SHA256).Hash.ToLower()
|
|
"$hash ${env:STAGE}.zip" | Out-File -Encoding ascii "${env:STAGE}.zip.sha256"
|
|
|
|
- name: Upload artifact
|
|
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
|
with:
|
|
name: ffi-${{ matrix.target }}
|
|
path: |
|
|
agent-desktop-ffi-v*.tar.gz
|
|
agent-desktop-ffi-v*.tar.gz.sha256
|
|
agent-desktop-ffi-v*.zip
|
|
agent-desktop-ffi-v*.zip.sha256
|
|
retention-days: 1
|
|
|
|
publish-github:
|
|
name: Publish to GitHub Release
|
|
needs: [release-please, build, build-ffi]
|
|
runs-on: ubuntu-latest
|
|
timeout-minutes: 10
|
|
permissions:
|
|
contents: write
|
|
id-token: write # Sigstore OIDC exchange
|
|
attestations: write # write provenance bundle back to GitHub
|
|
steps:
|
|
- name: Download all artifacts
|
|
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
|
|
with:
|
|
pattern: "{binary,ffi}-*"
|
|
merge-multiple: true
|
|
|
|
- name: Create checksums file
|
|
run: |
|
|
cat ./*.sha256 > checksums.txt
|
|
cat checksums.txt
|
|
|
|
- name: Upload assets to GitHub Release
|
|
env:
|
|
GH_TOKEN: ${{ github.token }}
|
|
GH_REPO: ${{ github.repository }}
|
|
TAG_NAME: ${{ needs.release-please.outputs.tag_name }}
|
|
run: |
|
|
gh release view "$TAG_NAME" >/dev/null 2>&1 \
|
|
|| gh release create "$TAG_NAME" \
|
|
--title "$TAG_NAME" \
|
|
--notes "Release ${TAG_NAME}"
|
|
gh release upload "$TAG_NAME" \
|
|
./*.tar.gz ./*.zip checksums.txt \
|
|
--clobber
|
|
|
|
# Keyless Sigstore provenance. Verify with:
|
|
# gh attestation verify <file> --repo lahfir/agent-desktop
|
|
- name: Attest build provenance
|
|
uses: actions/attest-build-provenance@a2bbfa25375fe432b6a289bc6b6cd05ecd0c4c32 # v4.1.0
|
|
with:
|
|
subject-path: |
|
|
*.tar.gz
|
|
*.zip
|
|
checksums.txt
|
|
|
|
publish-npm:
|
|
name: Publish to npm
|
|
needs: [release-please, publish-github]
|
|
if: needs.release-please.outputs.release_created == 'true' && needs.release-please.outputs.publish_npm == 'true'
|
|
runs-on: ubuntu-latest
|
|
timeout-minutes: 15
|
|
permissions:
|
|
contents: read
|
|
id-token: write
|
|
steps:
|
|
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
|
with:
|
|
ref: ${{ needs.release-please.outputs.tag_name }}
|
|
|
|
- uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
|
|
with:
|
|
node-version: '24'
|
|
registry-url: https://registry.npmjs.org
|
|
package-manager-cache: false
|
|
|
|
- name: Verify GitHub Release assets
|
|
env:
|
|
GH_TOKEN: ${{ github.token }}
|
|
TAG_NAME: ${{ needs.release-please.outputs.tag_name }}
|
|
run: |
|
|
# Expected: 2 CLI tarballs + 4 FFI tarballs + 1 FFI zip + checksums = 8.
|
|
ASSETS=$(gh release view "$TAG_NAME" --json assets --jq '.assets | length')
|
|
if [ "$ASSETS" -lt 8 ]; then
|
|
echo "FAIL: expected at least 8 assets, found ${ASSETS}"
|
|
exit 1
|
|
fi
|
|
echo "OK: ${ASSETS} assets found on release"
|
|
|
|
- name: Prepare npm package
|
|
env:
|
|
VERSION: ${{ needs.release-please.outputs.version }}
|
|
run: |
|
|
cd npm
|
|
node -e "
|
|
const version = process.env.VERSION;
|
|
const pkg = require('./package.json');
|
|
pkg.version = version;
|
|
require('fs').writeFileSync('package.json', JSON.stringify(pkg, null, 2) + '\n');
|
|
"
|
|
echo "npm package version set to ${VERSION}"
|
|
cp ../README.md README.md
|
|
cp ../CHANGELOG.md CHANGELOG.md 2>/dev/null || true
|
|
|
|
- name: Publish to npm
|
|
run: npm publish --access public
|
|
working-directory: npm
|
|
|
|
publish-skills:
|
|
name: Publish Skills to ClawHub
|
|
needs: [release-please]
|
|
if: needs.release-please.outputs.release_created == 'true' && needs.release-please.outputs.publish_skills == 'true'
|
|
runs-on: ubuntu-latest
|
|
timeout-minutes: 10
|
|
permissions:
|
|
contents: read
|
|
steps:
|
|
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
|
with:
|
|
ref: ${{ needs.release-please.outputs.tag_name }}
|
|
|
|
- uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
|
|
with:
|
|
node-version: '24'
|
|
|
|
- name: Install ClawHub CLI
|
|
run: npm i -g clawhub
|
|
|
|
- name: Authenticate with ClawHub
|
|
run: clawhub login --token "$CLAWHUB_TOKEN" --no-browser
|
|
env:
|
|
CLAWHUB_TOKEN: ${{ secrets.CLAWHUB_TOKEN }}
|
|
|
|
- name: Publish all skills to ClawHub
|
|
run: |
|
|
set -euo pipefail
|
|
SOURCE_COMMIT="$(git rev-parse HEAD)"
|
|
for skill_dir in skills/*; do
|
|
[ -d "$skill_dir" ] || continue
|
|
[ -f "$skill_dir/SKILL.md" ] || continue
|
|
slug="$(basename "$skill_dir")"
|
|
clawhub publish "$skill_dir" \
|
|
--slug "$slug" \
|
|
--changelog "Release ${{ needs.release-please.outputs.tag_name }}" \
|
|
--tags latest \
|
|
--source-repo "${GITHUB_REPOSITORY}" \
|
|
--source-commit "$SOURCE_COMMIT" \
|
|
--source-ref "${{ needs.release-please.outputs.tag_name }}" \
|
|
--source-path "$skill_dir"
|
|
done
|