Skip to content

feat: add npm publishing for opencode plugin #61

feat: add npm publishing for opencode plugin

feat: add npm publishing for opencode plugin #61

Workflow file for this run

name: Release
on:
push:
tags:
- 'v*'
pull_request:
types: [opened, synchronize, reopened, labeled]
workflow_dispatch:
inputs:
dry_run:
description: 'Dry run (skip publishing)'
type: boolean
default: false
build_macos:
description: 'Build macOS binaries'
type: boolean
default: false
build_linux:
description: 'Build Linux binaries'
type: boolean
default: false
permissions:
contents: write
concurrency:
group: release-${{ github.ref }}
cancel-in-progress: ${{ github.event_name == 'pull_request' }}
jobs:
# =============================================================================
# PLAN: Centralized decision logic
# =============================================================================
# This job runs first (~5s) and computes which builds are needed.
# All other jobs check plan outputs instead of duplicating condition logic.
#
# Labels (for PRs):
# build:macos - Build macOS binaries
# build:linux - Build Linux binaries
# build:all - Build everything
# build-me - Trigger build (required for labeled events)
#
plan:
name: Plan
runs-on: ubuntu-latest
outputs:
version: ${{ steps.version.outputs.version }}
git_sha: ${{ steps.version.outputs.git_sha }}
is_release: ${{ steps.decide.outputs.is_release }}
is_dry_run: ${{ steps.decide.outputs.is_dry_run }}
build_macos: ${{ steps.decide.outputs.build_macos }}
build_linux: ${{ steps.decide.outputs.build_linux }}
should_skip: ${{ steps.decide.outputs.should_skip }}
steps:
- name: Compute version
id: version
run: |
# Version from tag (strip 'v' prefix)
if [[ "${{ github.ref_type }}" == "tag" ]]; then
VERSION="${GITHUB_REF#refs/tags/v}"
elif [[ "${{ github.event_name }}" == "pull_request" ]]; then
VERSION="0.0.0-pr${{ github.event.pull_request.number }}"
else
VERSION="0.0.0-dev"
fi
echo "version=$VERSION" >> $GITHUB_OUTPUT
# Git SHA (short)
GIT_SHA="${GITHUB_SHA:0:7}"
echo "git_sha=$GIT_SHA" >> $GITHUB_OUTPUT
echo "::notice::Build version: $VERSION ($GIT_SHA)"
- name: Decide what to build
id: decide
env:
EVENT_NAME: ${{ github.event_name }}
EVENT_ACTION: ${{ github.event.action }}
LABEL_ADDED: ${{ github.event.label.name }}
HAS_LABEL_ALL: ${{ contains(github.event.pull_request.labels.*.name, 'build:all') }}
HAS_LABEL_MACOS: ${{ contains(github.event.pull_request.labels.*.name, 'build:macos') }}
HAS_LABEL_LINUX: ${{ contains(github.event.pull_request.labels.*.name, 'build:linux') }}
INPUT_MACOS: ${{ inputs.build_macos }}
INPUT_LINUX: ${{ inputs.build_linux }}
INPUT_DRY_RUN: ${{ inputs.dry_run }}
run: |
# Skip if triggered by a non-build label (only 'build-me' triggers builds)
if [[ "$EVENT_ACTION" == "labeled" && "$LABEL_ADDED" != "build-me" ]]; then
echo "::notice::Skipping: labeled event for '$LABEL_ADDED' (only 'build-me' triggers builds)"
echo "should_skip=true" >> $GITHUB_OUTPUT
echo "is_release=false" >> $GITHUB_OUTPUT
echo "is_dry_run=true" >> $GITHUB_OUTPUT
echo "build_macos=false" >> $GITHUB_OUTPUT
echo "build_linux=false" >> $GITHUB_OUTPUT
exit 0
fi
echo "should_skip=false" >> $GITHUB_OUTPUT
# Is this a release?
IS_RELEASE=$([[ "$EVENT_NAME" == "push" && "${{ github.ref_type }}" == "tag" ]] && echo "true" || echo "false")
echo "is_release=$IS_RELEASE" >> $GITHUB_OUTPUT
# Dry run? (PRs are always dry run, workflow_dispatch can override)
if [[ "$EVENT_NAME" == "pull_request" ]]; then
IS_DRY_RUN="true"
elif [[ "$INPUT_DRY_RUN" == "true" ]]; then
IS_DRY_RUN="true"
else
IS_DRY_RUN="false"
fi
echo "is_dry_run=$IS_DRY_RUN" >> $GITHUB_OUTPUT
# Helper: should we build everything?
should_build_all() {
[[ "$IS_RELEASE" == "true" ]] && return 0
[[ "$HAS_LABEL_ALL" == "true" ]] && return 0
return 1
}
# macOS builds
BUILD_MACOS="false"
if should_build_all; then BUILD_MACOS="true"; fi
if [[ "$HAS_LABEL_MACOS" == "true" ]]; then BUILD_MACOS="true"; fi
if [[ "$INPUT_MACOS" == "true" ]]; then BUILD_MACOS="true"; fi
echo "build_macos=$BUILD_MACOS" >> $GITHUB_OUTPUT
# Linux builds
BUILD_LINUX="false"
if should_build_all; then BUILD_LINUX="true"; fi
if [[ "$HAS_LABEL_LINUX" == "true" ]]; then BUILD_LINUX="true"; fi
if [[ "$INPUT_LINUX" == "true" ]]; then BUILD_LINUX="true"; fi
echo "build_linux=$BUILD_LINUX" >> $GITHUB_OUTPUT
# Summary
echo "### 📋 Build Plan" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "| Setting | Value |" >> $GITHUB_STEP_SUMMARY
echo "|---------|-------|" >> $GITHUB_STEP_SUMMARY
echo "| Release | $IS_RELEASE |" >> $GITHUB_STEP_SUMMARY
echo "| Dry Run | $IS_DRY_RUN |" >> $GITHUB_STEP_SUMMARY
echo "| macOS | $BUILD_MACOS |" >> $GITHUB_STEP_SUMMARY
echo "| Linux | $BUILD_LINUX |" >> $GITHUB_STEP_SUMMARY
# =============================================================================
# BUILD: macOS targets (native)
# =============================================================================
build-macos:
name: Build macOS (${{ matrix.arch }})
needs: plan
if: needs.plan.outputs.build_macos == 'true' && needs.plan.outputs.should_skip != 'true'
runs-on: macos-latest
strategy:
fail-fast: false
matrix:
include:
- target: aarch64-apple-darwin
arch: arm64
- target: x86_64-apple-darwin
arch: x64
steps:
- uses: actions/checkout@v4
- name: Set version in Cargo.toml
run: |
VERSION="${{ needs.plan.outputs.version }}"
perl -i -pe "s/^version = \".*\"/version = \"${VERSION}\"/" Cargo.toml
- uses: dtolnay/rust-toolchain@stable
with:
targets: ${{ matrix.target }}
- uses: swatinem/rust-cache@v2
with:
shared-key: macos-${{ matrix.target }}
save-if: ${{ needs.plan.outputs.is_release == 'true' }}
- name: Build release binary
run: cargo build --release --target ${{ matrix.target }}
- name: Package binary
run: |
mkdir -p dist
cp target/${{ matrix.target }}/release/bottle dist/
cd dist
tar -czvf bottle-${{ needs.plan.outputs.version }}-${{ matrix.target }}.tar.gz bottle
shasum -a 256 bottle-${{ needs.plan.outputs.version }}-${{ matrix.target }}.tar.gz > bottle-${{ needs.plan.outputs.version }}-${{ matrix.target }}.tar.gz.sha256
- name: Upload artifact
uses: actions/upload-artifact@v4
with:
name: bottle-${{ matrix.target }}
path: dist/bottle-${{ needs.plan.outputs.version }}-${{ matrix.target }}.*
# =============================================================================
# BUILD: Linux targets (zigbuild for cross-compilation)
# =============================================================================
build-linux:
name: Build Linux (${{ matrix.arch }})
needs: plan
if: needs.plan.outputs.build_linux == 'true' && needs.plan.outputs.should_skip != 'true'
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
include:
- target: x86_64-unknown-linux-musl
arch: x64
- target: aarch64-unknown-linux-musl
arch: arm64
steps:
- uses: actions/checkout@v4
- name: Set version in Cargo.toml
run: |
VERSION="${{ needs.plan.outputs.version }}"
perl -i -pe "s/^version = \".*\"/version = \"${VERSION}\"/" Cargo.toml
- uses: dtolnay/rust-toolchain@stable
with:
targets: ${{ matrix.target }}
- uses: swatinem/rust-cache@v2
with:
shared-key: zigbuild-${{ matrix.target }}
save-if: ${{ needs.plan.outputs.is_release == 'true' }}
- name: Install zig
run: |
curl -L https://ziglang.org/download/0.13.0/zig-linux-x86_64-0.13.0.tar.xz | tar -xJ
sudo mv zig-linux-x86_64-0.13.0 /opt/zig
echo "/opt/zig" >> $GITHUB_PATH
- name: Cache cargo-zigbuild
id: cache-zigbuild
uses: actions/cache@v4
with:
path: ~/.cargo/bin/cargo-zigbuild
key: cargo-zigbuild-0.20
- name: Install cargo-zigbuild
if: steps.cache-zigbuild.outputs.cache-hit != 'true'
run: cargo install cargo-zigbuild --locked
- name: Build with zigbuild
run: cargo zigbuild --release --target ${{ matrix.target }}
- name: Package binary
run: |
mkdir -p dist
cp target/${{ matrix.target }}/release/bottle dist/
cd dist
tar -czvf bottle-${{ needs.plan.outputs.version }}-${{ matrix.target }}.tar.gz bottle
sha256sum bottle-${{ needs.plan.outputs.version }}-${{ matrix.target }}.tar.gz > bottle-${{ needs.plan.outputs.version }}-${{ matrix.target }}.tar.gz.sha256
- name: Upload artifact
uses: actions/upload-artifact@v4
with:
name: bottle-${{ matrix.target }}
path: dist/bottle-${{ needs.plan.outputs.version }}-${{ matrix.target }}.*
# =============================================================================
# RELEASE: Create GitHub release and upload artifacts
# =============================================================================
release:
name: Release
needs: [plan, build-macos, build-linux]
runs-on: ubuntu-latest
if: needs.plan.outputs.is_release == 'true' && needs.plan.outputs.is_dry_run != 'true'
steps:
- uses: actions/checkout@v4
- name: Download all artifacts
uses: actions/download-artifact@v4
with:
path: artifacts
merge-multiple: true
- name: List artifacts
run: ls -la artifacts/
- name: Create GitHub Release
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
gh release create "v${{ needs.plan.outputs.version }}" \
--title "v${{ needs.plan.outputs.version }}" \
--generate-notes \
artifacts/*
# =============================================================================
# PUBLISH: Homebrew tap
# =============================================================================
publish-homebrew:
name: Publish Homebrew
needs: [plan, release]
runs-on: ubuntu-latest
if: needs.plan.outputs.is_release == 'true' && needs.plan.outputs.is_dry_run != 'true'
steps:
- uses: actions/checkout@v4
with:
repository: open-horizon-labs/homebrew-tap
token: ${{ secrets.HOMEBREW_TAP_TOKEN }}
path: homebrew-tap
- name: Download artifacts for checksums
uses: actions/download-artifact@v4
with:
path: artifacts
merge-multiple: true
- name: Update Homebrew formula
run: |
VERSION="${{ needs.plan.outputs.version }}"
ARM64_MAC_SHA=$(cat artifacts/bottle-${VERSION}-aarch64-apple-darwin.tar.gz.sha256 | awk '{print $1}')
X64_MAC_SHA=$(cat artifacts/bottle-${VERSION}-x86_64-apple-darwin.tar.gz.sha256 | awk '{print $1}')
X64_LINUX_SHA=$(cat artifacts/bottle-${VERSION}-x86_64-unknown-linux-musl.tar.gz.sha256 | awk '{print $1}')
ARM64_LINUX_SHA=$(cat artifacts/bottle-${VERSION}-aarch64-unknown-linux-musl.tar.gz.sha256 | awk '{print $1}')
cat > homebrew-tap/Formula/bottle.rb << EOF
class Bottle < Formula
desc "Curated snapshot manager for the Open Horizon Labs tool stack"
homepage "https://github.com/open-horizon-labs/bottle"
version "${VERSION}"
license "MIT"
on_macos do
if Hardware::CPU.arm?
url "https://github.com/open-horizon-labs/bottle/releases/download/v${VERSION}/bottle-${VERSION}-aarch64-apple-darwin.tar.gz"
sha256 "${ARM64_MAC_SHA}"
else
url "https://github.com/open-horizon-labs/bottle/releases/download/v${VERSION}/bottle-${VERSION}-x86_64-apple-darwin.tar.gz"
sha256 "${X64_MAC_SHA}"
end
end
on_linux do
if Hardware::CPU.arm?
url "https://github.com/open-horizon-labs/bottle/releases/download/v${VERSION}/bottle-${VERSION}-aarch64-unknown-linux-musl.tar.gz"
sha256 "${ARM64_LINUX_SHA}"
else
url "https://github.com/open-horizon-labs/bottle/releases/download/v${VERSION}/bottle-${VERSION}-x86_64-unknown-linux-musl.tar.gz"
sha256 "${X64_LINUX_SHA}"
end
end
def install
bin.install "bottle"
end
test do
system "#{bin}/bottle", "--version"
end
end
EOF
- name: Commit and push
run: |
cd homebrew-tap
git config user.name "github-actions[bot]"
git config user.email "github-actions[bot]@users.noreply.github.com"
git add Formula/bottle.rb
git commit -m "bottle ${{ needs.plan.outputs.version }}"
git push
# =============================================================================
# PUBLISH: crates.io
# =============================================================================
publish-crates:
name: Publish crates.io
needs: [plan, release]
runs-on: ubuntu-latest
if: needs.plan.outputs.is_release == 'true' && needs.plan.outputs.is_dry_run != 'true'
steps:
- uses: actions/checkout@v4
- name: Set version in Cargo.toml
run: |
VERSION="${{ needs.plan.outputs.version }}"
perl -i -pe "s/^version = \".*\"/version = \"${VERSION}\"/" Cargo.toml
- uses: dtolnay/rust-toolchain@stable
- uses: swatinem/rust-cache@v2
- name: Publish to crates.io
env:
CARGO_REGISTRY_TOKEN: ${{ secrets.CARGO_REGISTRY_TOKEN }}
run: cargo publish --allow-dirty
# =============================================================================
# PUBLISH: npm (OpenCode plugin)
# =============================================================================
publish-npm:
name: Publish npm
needs: [plan, release]
runs-on: ubuntu-latest
if: needs.plan.outputs.is_release == 'true' && needs.plan.outputs.is_dry_run != 'true'
environment: npm
permissions:
contents: read
id-token: write
steps:
- uses: actions/checkout@v4
- name: Setup Bun
uses: oven-sh/setup-bun@v2
- name: Setup Node for npm publish
uses: actions/setup-node@v4
with:
node-version: '22'
registry-url: 'https://registry.npmjs.org'
- name: Upgrade npm for OIDC support
run: npm install -g npm@latest
- name: Extract version from tag
id: version
run: echo "VERSION=${GITHUB_REF_NAME#v}" >> $GITHUB_OUTPUT
- name: Update package.json version
working-directory: opencode-plugin
run: |
jq --arg v "${{ steps.version.outputs.VERSION }}" '.version = $v' package.json > package.json.tmp
mv package.json.tmp package.json
- name: Install dependencies
working-directory: opencode-plugin
run: bun install
- name: Build
working-directory: opencode-plugin
run: bun run build
- name: Publish to npm
working-directory: opencode-plugin
run: npm publish --access public --provenance
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}