feat: add npm publishing for opencode plugin #61
Workflow file for this run
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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 }} |