Files
actionbook__actionbook/.github/workflows/release-cli.yml
Junliang Feng fa5b733781 [root]chore: split CLI release into release-cli.yml and scope changeset to CLI only (#594)
* [root]chore: split CLI release into release-cli.yml and scope changeset to CLI only

- Rename .github/workflows/release.yml -> release-cli.yml, keeping only
  version-or-release / build-cli / publish-cli jobs; drop publish-js,
  release-extension, release-dify-plugin and their detection outputs
- Extend .changeset/config.json ignore list with JS packages, extension
  and dify-plugin so `pnpm changeset` only proposes CLI-related packages
- Other package release pipelines will be re-introduced later as their
  own independent workflows when needed

* [root]fix: silence shellcheck warnings in release-cli.yml

- SC2193: assign matrix.platform-suffix to a shell var before pattern
  matching so shellcheck stops flagging the == win32-* comparison
- SC2086: switch TAG_FLAG string to a TAG_FLAGS bash array and expand
  with "${TAG_FLAGS[@]}" in both platform and CLI publish steps
2026-04-23 01:38:46 +08:00

504 lines
18 KiB
YAML

name: Release CLI
on:
push:
branches:
- main
workflow_dispatch:
concurrency: ${{ github.workflow }}-${{ github.ref }}
permissions:
contents: write
pull-requests: write
id-token: write
jobs:
# --------------------------------------------------------------------------
# Job 1: Create Version PR or determine whether CLI needs a release
# --------------------------------------------------------------------------
version-or-release:
name: Version or Release
runs-on: ubuntu-latest
outputs:
should-release: ${{ steps.changesets.outputs.hasChangesets == 'false' }}
cli-needs-release: ${{ steps.check-releases.outputs.cli }}
cli-changed: ${{ steps.changes.outputs.cli }}
steps:
- name: Checkout
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: "20"
registry-url: "https://registry.npmjs.org"
- name: Install pnpm
uses: pnpm/action-setup@v4
- name: Install dependencies
run: pnpm install --frozen-lockfile
- name: Detect changed packages
id: changes
run: |
# workflow_dispatch or first push: treat CLI as changed
if [ "${{ github.event_name }}" = "workflow_dispatch" ] || \
[ "${{ github.event.before }}" = "0000000000000000000000000000000000000000" ]; then
echo "cli=true" >> "$GITHUB_OUTPUT"
exit 0
fi
CHANGED=$(git diff --name-only "${{ github.event.before }}" "${{ github.sha }}")
echo "Changed files:"
echo "$CHANGED"
if echo "$CHANGED" | grep -qE "^packages/cli[-/]"; then
echo "cli=true" >> "$GITHUB_OUTPUT"
else
echo "cli=false" >> "$GITHUB_OUTPUT"
fi
- name: Create Release Pull Request
id: changesets
uses: changesets/action@v1
with:
version: pnpm version-packages
commit: "chore: version packages"
title: "chore: version packages"
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Check whether CLI needs release
id: check-releases
if: steps.changesets.outputs.hasChangesets == 'false'
run: |
CLI_LOCAL=$(node -e "console.log(JSON.parse(require('fs').readFileSync('packages/cli/package.json','utf8')).version)")
CLI_NPM=$(npm view @actionbookdev/cli version 2>/dev/null || echo "0.0.0")
if [ "$CLI_LOCAL" != "$CLI_NPM" ]; then
echo "cli=true" >> "$GITHUB_OUTPUT"
echo "CLI needs release: $CLI_LOCAL (local) vs $CLI_NPM (npm)"
else
echo "cli=false" >> "$GITHUB_OUTPUT"
echo "CLI is up to date: $CLI_LOCAL"
fi
# --------------------------------------------------------------------------
# Job 2: Build CLI binaries (cross-platform Rust compilation)
# --------------------------------------------------------------------------
build-cli:
name: Build CLI (${{ matrix.platform-suffix }})
needs: version-or-release
if: needs.version-or-release.outputs.cli-needs-release == 'true' && needs.version-or-release.outputs.cli-changed == 'true'
strategy:
matrix:
include:
- target: aarch64-apple-darwin
os: macos-latest
platform-suffix: darwin-arm64
- target: x86_64-apple-darwin
os: macos-latest
platform-suffix: darwin-x64
- target: x86_64-unknown-linux-gnu
os: ubuntu-22.04
platform-suffix: linux-x64
- target: aarch64-unknown-linux-gnu
os: ubuntu-latest
platform-suffix: linux-arm64
use-cross: true
- target: x86_64-pc-windows-msvc
os: windows-latest
platform-suffix: win32-x64
- target: aarch64-pc-windows-msvc
os: windows-latest
platform-suffix: win32-arm64
runs-on: ${{ matrix.os }}
defaults:
run:
working-directory: packages/cli
steps:
- uses: actions/checkout@v4
- name: Install Rust toolchain
uses: dtolnay/rust-toolchain@stable
with:
targets: ${{ matrix.target }}
- name: Install cross (Linux ARM64)
if: matrix.use-cross
run: cargo install cross --git https://github.com/cross-rs/cross
- name: Build (cross)
if: matrix.use-cross
working-directory: .
run: cross build --release --target ${{ matrix.target }} --manifest-path packages/cli/Cargo.toml
- name: Build (native)
if: ${{ !matrix.use-cross }}
run: cargo build --release --target ${{ matrix.target }}
- name: Determine binary name
id: binary
shell: bash
run: |
if [[ "${{ matrix.os }}" == "windows-latest" ]]; then
echo "name=actionbook.exe" >> "$GITHUB_OUTPUT"
else
echo "name=actionbook" >> "$GITHUB_OUTPUT"
fi
- name: Rename binary for platform
shell: bash
run: |
CANDIDATES=(
"target/${{ matrix.target }}/release/${{ steps.binary.outputs.name }}"
"../target/${{ matrix.target }}/release/${{ steps.binary.outputs.name }}"
)
SRC=""
for candidate in "${CANDIDATES[@]}"; do
if [[ -f "$candidate" ]]; then
SRC="$candidate"
break
fi
done
if [[ -z "$SRC" ]]; then
echo "ERROR: Built binary not found in expected target directories."
printf 'Checked:\n - %s\n' "${CANDIDATES[@]}"
exit 1
fi
ARTIFACT_DIR="${RUNNER_TEMP}/actionbook-binaries"
mkdir -p "$ARTIFACT_DIR"
PLATFORM_SUFFIX="${{ matrix.platform-suffix }}"
if [[ "$PLATFORM_SUFFIX" == win32-* ]]; then
DEST="$ARTIFACT_DIR/actionbook-${PLATFORM_SUFFIX}.exe"
else
DEST="$ARTIFACT_DIR/actionbook-${PLATFORM_SUFFIX}"
fi
cp "$SRC" "$DEST"
chmod +x "$DEST"
- name: Upload artifact
uses: actions/upload-artifact@v4
with:
name: actionbook-${{ matrix.platform-suffix }}
path: ${{ runner.temp }}/actionbook-binaries/actionbook-${{ matrix.platform-suffix }}*
if-no-files-found: error
# --------------------------------------------------------------------------
# Job 3: Publish CLI npm packages + GitHub Release
# --------------------------------------------------------------------------
publish-cli:
name: Publish CLI
needs: [version-or-release, build-cli]
if: needs.version-or-release.outputs.cli-needs-release == 'true' && needs.version-or-release.outputs.cli-changed == 'true'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: "20"
registry-url: "https://registry.npmjs.org"
- name: Download all artifacts
uses: actions/download-artifact@v4
with:
path: artifacts
- name: Read CLI version
id: version
run: |
VERSION=$(node -e "console.log(JSON.parse(require('fs').readFileSync('packages/cli/package.json','utf8')).version)")
echo "version=${VERSION}" >> "$GITHUB_OUTPUT"
- name: Prepare platform package binaries
run: |
declare -A PLATFORM_PKG_DIRS=(
["darwin-arm64"]="packages/cli-darwin-arm64"
["darwin-x64"]="packages/cli-darwin-x64"
["linux-x64"]="packages/cli-linux-x64-gnu"
["linux-arm64"]="packages/cli-linux-arm64-gnu"
["win32-x64"]="packages/cli-win32-x64"
["win32-arm64"]="packages/cli-win32-arm64"
)
for suffix in "${!PLATFORM_PKG_DIRS[@]}"; do
artifact_dir="artifacts/actionbook-${suffix}"
package_dir="${PLATFORM_PKG_DIRS[$suffix]}"
if [[ "$suffix" == win32-* ]]; then
src_name="actionbook-${suffix}.exe"
dest_name="actionbook.exe"
else
src_name="actionbook-${suffix}"
dest_name="actionbook"
fi
cp "${artifact_dir}/${src_name}" "${package_dir}/bin/${dest_name}"
chmod +x "${package_dir}/bin/${dest_name}"
echo "Copied ${src_name} -> ${package_dir}/bin/${dest_name}"
done
- name: Validate platform binaries
run: |
declare -A PLATFORM_PKG_DIRS=(
["darwin-arm64"]="packages/cli-darwin-arm64"
["darwin-x64"]="packages/cli-darwin-x64"
["linux-x64"]="packages/cli-linux-x64-gnu"
["linux-arm64"]="packages/cli-linux-arm64-gnu"
["win32-x64"]="packages/cli-win32-x64"
["win32-arm64"]="packages/cli-win32-arm64"
)
min_size=100000
failures=0
for suffix in "${!PLATFORM_PKG_DIRS[@]}"; do
package_dir="${PLATFORM_PKG_DIRS[$suffix]}"
if [[ "$suffix" == win32-* ]]; then
binary_path="${package_dir}/bin/actionbook.exe"
else
binary_path="${package_dir}/bin/actionbook"
fi
if [[ ! -f "${binary_path}" ]]; then
echo "ERROR: Missing binary ${binary_path}"
failures=$((failures + 1))
continue
fi
size=$(stat -c%s "${binary_path}")
if [[ "$size" -lt "$min_size" ]]; then
echo "ERROR: Binary too small ${binary_path} (${size} bytes)"
failures=$((failures + 1))
continue
fi
echo "OK: ${binary_path} (${size} bytes)"
done
if [[ "$failures" -gt 0 ]]; then
echo "Found ${failures} platform binary validation errors"
exit 1
fi
- name: Generate SHA256 checksums for CLI binaries
run: |
EXPECTED_ASSETS=(
"actionbook-darwin-arm64"
"actionbook-darwin-x64"
"actionbook-linux-x64"
"actionbook-linux-arm64"
"actionbook-win32-x64.exe"
"actionbook-win32-arm64.exe"
)
missing=0
for asset in "${EXPECTED_ASSETS[@]}"; do
artifact_dir="${asset%.exe}"
binary_path="artifacts/${artifact_dir}/${asset}"
if [[ ! -f "$binary_path" ]]; then
echo "ERROR: Expected binary not found: $binary_path"
missing=$((missing + 1))
fi
done
if [[ "$missing" -gt 0 ]]; then
echo "Missing $missing expected binaries — aborting checksum generation"
exit 1
fi
mkdir -p artifacts/checksums
: > artifacts/checksums/SHA256SUMS
for asset in "${EXPECTED_ASSETS[@]}"; do
artifact_dir="${asset%.exe}"
binary_path="artifacts/${artifact_dir}/${asset}"
checksum=$(sha256sum "$binary_path" | awk '{print $1}')
printf '%s %s\n' "$checksum" "$asset" >> artifacts/checksums/SHA256SUMS
done
echo "--- SHA256SUMS ---"
cat artifacts/checksums/SHA256SUMS
- name: Publish platform packages
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
run: |
declare -A PLATFORM_PKG_DIRS=(
["darwin-arm64"]="packages/cli-darwin-arm64"
["darwin-x64"]="packages/cli-darwin-x64"
["linux-x64"]="packages/cli-linux-x64-gnu"
["linux-arm64"]="packages/cli-linux-arm64-gnu"
["win32-x64"]="packages/cli-win32-x64"
["win32-arm64"]="packages/cli-win32-arm64"
)
declare -A PLATFORM_PKG_NAMES=(
["darwin-arm64"]="@actionbookdev/cli-darwin-arm64"
["darwin-x64"]="@actionbookdev/cli-darwin-x64"
["linux-x64"]="@actionbookdev/cli-linux-x64-gnu"
["linux-arm64"]="@actionbookdev/cli-linux-arm64-gnu"
["win32-x64"]="@actionbookdev/cli-win32-x64"
["win32-arm64"]="@actionbookdev/cli-win32-arm64"
)
for suffix in darwin-arm64 darwin-x64 linux-x64 linux-arm64 win32-x64 win32-arm64; do
pkg_dir="${PLATFORM_PKG_DIRS[$suffix]}"
pkg_name="${PLATFORM_PKG_NAMES[$suffix]}"
LOCAL_VER=$(node -e "console.log(JSON.parse(require('fs').readFileSync('${pkg_dir}/package.json','utf8')).version)")
NPM_VER=$(npm view "${pkg_name}" version 2>/dev/null || echo "0.0.0")
if [ "$LOCAL_VER" != "$NPM_VER" ]; then
TAG_FLAGS=()
if [[ "$LOCAL_VER" == *-alpha* ]]; then
TAG_FLAGS=(--tag alpha)
fi
echo "Publishing ${pkg_name}@${LOCAL_VER} (npm has ${NPM_VER})..."
(cd "${pkg_dir}" && npm publish --access public --provenance "${TAG_FLAGS[@]}")
else
echo "Skipping ${pkg_name}@${LOCAL_VER} (already published)"
fi
done
- name: Replace workspace protocol in CLI package
run: |
VERSION="${{ steps.version.outputs.version }}"
node -e "
const fs = require('fs');
const pkg = JSON.parse(fs.readFileSync('packages/cli/package.json', 'utf8'));
for (const [name, ver] of Object.entries(pkg.optionalDependencies || {})) {
if (ver.startsWith('workspace:')) {
pkg.optionalDependencies[name] = '${VERSION}';
}
}
fs.writeFileSync('packages/cli/package.json', JSON.stringify(pkg, null, 2) + '\n');
console.log('Replaced workspace:* with ${VERSION} in CLI optionalDependencies');
"
- name: Copy root README to CLI package
run: cp README.md packages/cli/README.md
- name: Publish CLI package
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
run: |
LOCAL_VER="${{ steps.version.outputs.version }}"
NPM_VER=$(npm view @actionbookdev/cli version 2>/dev/null || echo "0.0.0")
if [ "$LOCAL_VER" != "$NPM_VER" ]; then
TAG_FLAGS=()
if [[ "$LOCAL_VER" == *-alpha* ]]; then
TAG_FLAGS=(--tag alpha)
fi
echo "Publishing @actionbookdev/cli@${LOCAL_VER} (npm has ${NPM_VER})..."
(cd packages/cli && npm publish --access public --provenance "${TAG_FLAGS[@]}")
else
echo "Skipping @actionbookdev/cli@${LOCAL_VER} (already published)"
fi
- name: Create CLI GitHub Release
uses: softprops/action-gh-release@v2
with:
token: ${{ secrets.RELEASE_PAT || github.token }}
tag_name: actionbook-cli-v${{ steps.version.outputs.version }}
name: Actionbook CLI v${{ steps.version.outputs.version }}
files: |
artifacts/actionbook-darwin-arm64/actionbook-darwin-arm64
artifacts/actionbook-darwin-x64/actionbook-darwin-x64
artifacts/actionbook-linux-x64/actionbook-linux-x64
artifacts/actionbook-linux-arm64/actionbook-linux-arm64
artifacts/actionbook-win32-x64/actionbook-win32-x64.exe
artifacts/actionbook-win32-arm64/actionbook-win32-arm64.exe
artifacts/checksums/SHA256SUMS
generate_release_notes: true
- name: Update Homebrew formula
env:
HOMEBREW_TAP_TOKEN: ${{ secrets.HOMEBREW_TAP_TOKEN }}
run: |
VERSION="${{ steps.version.outputs.version }}"
TAG="actionbook-cli-v${VERSION}"
# Read SHA256SUMS generated earlier in this job
CHECKSUMS_FILE="artifacts/checksums/SHA256SUMS"
if [[ ! -f "$CHECKSUMS_FILE" ]]; then
echo "WARNING: SHA256SUMS not found, skipping Homebrew formula update"
exit 0
fi
get_sha() {
local asset="$1"
grep " ${asset}$" "$CHECKSUMS_FILE" | cut -d' ' -f1
}
SHA_DARWIN_ARM64=$(get_sha "actionbook-darwin-arm64")
SHA_DARWIN_X64=$(get_sha "actionbook-darwin-x64")
SHA_LINUX_ARM64=$(get_sha "actionbook-linux-arm64")
SHA_LINUX_X64=$(get_sha "actionbook-linux-x64")
# Generate the updated formula
cat > /tmp/actionbook.rb << RUBY_EOF
# typed: false
# frozen_string_literal: true
# Formula auto-updated by actionbook/actionbook release CI.
# Manual edits will be overwritten on the next release.
class Actionbook < Formula
desc "CLI for Actionbook - run AI actions from your terminal"
homepage "https://actionbook.dev"
version "${VERSION}"
license "MIT"
on_macos do
if Hardware::CPU.arm?
url "https://github.com/actionbook/actionbook/releases/download/${TAG}/actionbook-darwin-arm64"
sha256 "${SHA_DARWIN_ARM64}"
else
url "https://github.com/actionbook/actionbook/releases/download/${TAG}/actionbook-darwin-x64"
sha256 "${SHA_DARWIN_X64}"
end
end
on_linux do
if Hardware::CPU.arm?
url "https://github.com/actionbook/actionbook/releases/download/${TAG}/actionbook-linux-arm64"
sha256 "${SHA_LINUX_ARM64}"
else
url "https://github.com/actionbook/actionbook/releases/download/${TAG}/actionbook-linux-x64"
sha256 "${SHA_LINUX_X64}"
end
end
def install
binary_name = "actionbook"
if OS.mac?
bin.install (Hardware::CPU.arm? ? "actionbook-darwin-arm64" : "actionbook-darwin-x64") => binary_name
else
bin.install (Hardware::CPU.arm? ? "actionbook-linux-arm64" : "actionbook-linux-x64") => binary_name
end
end
test do
assert_match version.to_s, shell_output("#{bin}/actionbook --version")
end
end
RUBY_EOF
# Remove leading whitespace (heredoc indentation)
sed -i "s/^ //" /tmp/actionbook.rb
# Push to homebrew-tap repo
cd "$(mktemp -d)"
git clone "https://x-access-token:${HOMEBREW_TAP_TOKEN}@github.com/actionbook/homebrew-tap.git" .
cp /tmp/actionbook.rb Formula/actionbook.rb
git config user.name "github-actions[bot]"
git config user.email "github-actions[bot]@users.noreply.github.com"
git add Formula/actionbook.rb
git commit -m "actionbook ${VERSION}" || echo "No changes to commit"
git push