chore(release): v0.6.2 #22
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: Publish Packages | |
| on: | |
| push: | |
| tags: | |
| - v[0-9]+.* | |
| workflow_dispatch: | |
| inputs: | |
| tag: | |
| description: "Release tag to publish (e.g., v0.1.0)" | |
| required: true | |
| type: string | |
| concurrency: | |
| group: ${{ github.workflow }}-${{ github.ref }} | |
| cancel-in-progress: true | |
| permissions: | |
| contents: write | |
| id-token: write | |
| jobs: | |
| meta: | |
| name: Resolve versions | |
| runs-on: ubuntu-latest | |
| outputs: | |
| version: ${{ steps.version.outputs.version }} | |
| python_version: ${{ steps.version.outputs.python_version }} | |
| tag: ${{ steps.version.outputs.tag }} | |
| crates_exists: ${{ steps.published.outputs.crates_exists }} | |
| npm_exists: ${{ steps.published.outputs.npm_exists }} | |
| opencode_exists: ${{ steps.published.outputs.opencode_exists }} | |
| pypi_exists: ${{ steps.published.outputs.pypi_exists }} | |
| release_assets_exist: ${{ steps.release_assets.outputs.release_assets_exist }} | |
| steps: | |
| - name: Checkout | |
| uses: actions/checkout@v6 | |
| with: | |
| fetch-depth: 0 | |
| ref: ${{ github.event_name == 'workflow_dispatch' && github.event.inputs.tag || '' }} | |
| - name: Setup Node.js | |
| uses: actions/setup-node@v6 | |
| with: | |
| node-version: "20" | |
| - name: Extract version from tag | |
| id: version | |
| run: | | |
| if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then | |
| tag="${{ github.event.inputs.tag }}" | |
| else | |
| tag="${GITHUB_REF#refs/tags/}" | |
| fi | |
| version="${tag#v}" | |
| python_version="${version//-rc./rc}" | |
| { | |
| echo "version=$version" | |
| echo "python_version=$python_version" | |
| echo "tag=$tag" | |
| } >> "$GITHUB_OUTPUT" | |
| echo "Git tag: $tag" | |
| echo "Cargo/npm version: $version" | |
| echo "Python version: $python_version" | |
| - name: Verify version consistency across all surfaces | |
| run: | | |
| set -euo pipefail | |
| target_version="${{ steps.version.outputs.version }}" | |
| target_python_version="${{ steps.version.outputs.python_version }}" | |
| echo "Verifying version consistency for: $target_version (Python form: $target_python_version)" | |
| # Check Cargo.toml | |
| cargo_version="$(grep -E '^version = "' Cargo.toml | head -1 | cut -d'"' -f2)" | |
| if [ "$cargo_version" != "$target_version" ]; then | |
| echo "::error::Cargo.toml version mismatch: expected $target_version, got $cargo_version" | |
| exit 1 | |
| fi | |
| # Check npm-package/package.json | |
| if [ -f npm-package/package.json ]; then | |
| npm_version="$(jq -r '.version' npm-package/package.json)" | |
| if [ "$npm_version" != "$target_version" ]; then | |
| echo "::error::npm-package/package.json version mismatch: expected $target_version, got $npm_version" | |
| exit 1 | |
| fi | |
| fi | |
| # Check pip-package/pyproject.toml (PyPI form) | |
| if [ -f pip-package/pyproject.toml ]; then | |
| pypi_version="$(grep -E '^version = "' pip-package/pyproject.toml | head -1 | cut -d'"' -f2)" | |
| if [ "$pypi_version" != "$target_python_version" ]; then | |
| echo "::error::pip-package/pyproject.toml version mismatch: expected $target_python_version, got $pypi_version" | |
| exit 1 | |
| fi | |
| fi | |
| # Check pip-package/basemind/__init__.py | |
| if [ -f pip-package/basemind/__init__.py ]; then | |
| init_version="$(grep -E '^__version__ = "' pip-package/basemind/__init__.py | cut -d'"' -f2)" | |
| if [ "$init_version" != "$target_python_version" ]; then | |
| echo "::error::pip-package/basemind/__init__.py version mismatch: expected $target_python_version, got $init_version" | |
| exit 1 | |
| fi | |
| fi | |
| # Check root package.json | |
| if [ -f package.json ]; then | |
| root_version="$(jq -r '.version' package.json)" | |
| if [ "$root_version" != "$target_version" ]; then | |
| echo "::error::package.json (root) version mismatch: expected $target_version, got $root_version" | |
| exit 1 | |
| fi | |
| fi | |
| # Check opencode-plugin/package.json | |
| if [ -f opencode-plugin/package.json ]; then | |
| opencode_version="$(jq -r '.version' opencode-plugin/package.json)" | |
| if [ "$opencode_version" != "$target_version" ]; then | |
| echo "::error::opencode-plugin/package.json version mismatch: expected $target_version, got $opencode_version" | |
| exit 1 | |
| fi | |
| fi | |
| # Check .claude-plugin/plugin.json | |
| if [ -f .claude-plugin/plugin.json ]; then | |
| claude_version="$(jq -r '.version' .claude-plugin/plugin.json)" | |
| if [ "$claude_version" != "$target_version" ]; then | |
| echo "::error::.claude-plugin/plugin.json version mismatch: expected $target_version, got $claude_version" | |
| exit 1 | |
| fi | |
| fi | |
| # Check .claude-plugin/marketplace.json | |
| if [ -f .claude-plugin/marketplace.json ]; then | |
| marketplace_version="$(jq -r '.plugins[0].version' .claude-plugin/marketplace.json)" | |
| if [ "$marketplace_version" != "$target_version" ]; then | |
| echo "::error::.claude-plugin/marketplace.json version mismatch: expected $target_version, got $marketplace_version" | |
| exit 1 | |
| fi | |
| fi | |
| # Check .codex-plugin/plugin.json | |
| if [ -f .codex-plugin/plugin.json ]; then | |
| codex_version="$(jq -r '.version' .codex-plugin/plugin.json)" | |
| if [ "$codex_version" != "$target_version" ]; then | |
| echo "::error::.codex-plugin/plugin.json version mismatch: expected $target_version, got $codex_version" | |
| exit 1 | |
| fi | |
| fi | |
| # Check .cursor-plugin/plugin.json | |
| if [ -f .cursor-plugin/plugin.json ]; then | |
| cursor_version="$(jq -r '.version' .cursor-plugin/plugin.json)" | |
| if [ "$cursor_version" != "$target_version" ]; then | |
| echo "::error::.cursor-plugin/plugin.json version mismatch: expected $target_version, got $cursor_version" | |
| exit 1 | |
| fi | |
| fi | |
| # Check gemini-extension.json | |
| if [ -f gemini-extension.json ]; then | |
| gemini_version="$(jq -r '.version' gemini-extension.json)" | |
| if [ "$gemini_version" != "$target_version" ]; then | |
| echo "::error::gemini-extension.json version mismatch: expected $target_version, got $gemini_version" | |
| exit 1 | |
| fi | |
| fi | |
| echo "✓ All version surfaces match: $target_version" | |
| - name: Detect already-published versions | |
| id: published | |
| run: | | |
| version="${{ steps.version.outputs.version }}" | |
| python_version="${{ steps.version.outputs.python_version }}" | |
| crates_exists=false | |
| if curl -fsS "https://crates.io/api/v1/crates/basemind/${version}" >/dev/null; then | |
| crates_exists=true | |
| fi | |
| npm_exists=false | |
| if npm view "basemind@${version}" version >/dev/null 2>&1; then | |
| npm_exists=true | |
| fi | |
| opencode_exists=false | |
| if npm view "basemind-opencode@${version}" version >/dev/null 2>&1; then | |
| opencode_exists=true | |
| fi | |
| pypi_exists=false | |
| if curl -fsS "https://pypi.org/pypi/basemind/${python_version}/json" >/dev/null; then | |
| pypi_exists=true | |
| fi | |
| { | |
| echo "crates_exists=$crates_exists" | |
| echo "npm_exists=$npm_exists" | |
| echo "opencode_exists=$opencode_exists" | |
| echo "pypi_exists=$pypi_exists" | |
| } >> "$GITHUB_OUTPUT" | |
| echo "Already published?" | |
| echo " crates.io: $crates_exists" | |
| echo " npm: $npm_exists" | |
| echo " npm (opencode): $opencode_exists" | |
| echo " PyPI: $pypi_exists" | |
| - name: Detect existing GitHub release assets | |
| id: release_assets | |
| env: | |
| GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| run: | | |
| tag="${{ steps.version.outputs.tag }}" | |
| release_assets_exist=false | |
| if gh release view "$tag" >/dev/null 2>&1; then | |
| count="$(gh release view "$tag" --json assets --jq '.assets | length')" | |
| if [ "${count:-0}" -gt 0 ]; then | |
| release_assets_exist=true | |
| fi | |
| fi | |
| echo "release_assets_exist=$release_assets_exist" >> "$GITHUB_OUTPUT" | |
| echo "GitHub release assets exist? $release_assets_exist" | |
| create_release: | |
| name: Create GitHub release | |
| needs: meta | |
| if: needs.meta.outputs.release_assets_exist != 'true' | |
| runs-on: ubuntu-latest | |
| steps: | |
| - name: Checkout | |
| uses: actions/checkout@v6 | |
| with: | |
| fetch-depth: 0 | |
| ref: ${{ github.event_name == 'workflow_dispatch' && github.event.inputs.tag || '' }} | |
| - name: Ensure the GitHub release exists | |
| # The native build matrix uploads assets to this release; gh release upload | |
| # requires it to already exist (goreleaser used to create it). Create it once | |
| # here, marked prerelease for rc/beta/alpha tags. | |
| env: | |
| GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| run: | | |
| tag="${{ needs.meta.outputs.tag }}" | |
| if gh release view "$tag" >/dev/null 2>&1; then | |
| echo "Release $tag already exists." | |
| else | |
| prerelease="" | |
| case "$tag" in | |
| *-rc.* | *-beta* | *-alpha*) prerelease="--prerelease" ;; | |
| esac | |
| gh release create "$tag" --title "$tag" --notes "Release $tag" --verify-tag $prerelease | |
| fi | |
| build-binaries: | |
| name: Build binaries / ${{ matrix.triple }} | |
| needs: [meta, create_release] | |
| if: needs.meta.outputs.release_assets_exist != 'true' | |
| strategy: | |
| fail-fast: false | |
| matrix: | |
| include: | |
| - triple: x86_64-unknown-linux-gnu | |
| os: ubuntu-latest | |
| - triple: aarch64-unknown-linux-gnu | |
| os: ubuntu-24.04-arm | |
| - triple: aarch64-apple-darwin | |
| os: macos-latest | |
| - triple: x86_64-pc-windows-msvc | |
| os: windows-latest | |
| runs-on: ${{ matrix.os }} | |
| steps: | |
| - name: Checkout | |
| uses: actions/checkout@v6 | |
| with: | |
| fetch-depth: 0 | |
| ref: ${{ github.event_name == 'workflow_dispatch' && github.event.inputs.tag || '' }} | |
| - name: Setup Rust | |
| uses: dtolnay/rust-toolchain@stable | |
| with: | |
| targets: ${{ matrix.triple }} | |
| - name: Install system dependencies | |
| uses: ./.github/actions/install-system-deps | |
| - name: Setup cache | |
| uses: Swatinem/rust-cache@v2 | |
| with: | |
| key: ${{ matrix.triple }} | |
| - name: Build release binary | |
| run: cargo build --release --features full --bin basemind --target ${{ matrix.triple }} | |
| - name: Package release | |
| shell: bash | |
| run: ./scripts/package-release.sh ${{ matrix.triple }} | |
| - name: Upload to GitHub release | |
| shell: bash | |
| env: | |
| GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| run: | | |
| tag="${{ needs.meta.outputs.tag }}" | |
| triple="${{ matrix.triple }}" | |
| if [ "$triple" = "x86_64-pc-windows-msvc" ]; then | |
| archive="basemind-${triple}.zip" | |
| else | |
| archive="basemind-${triple}.tar.gz" | |
| fi | |
| if [ ! -f "$archive" ]; then | |
| echo "::error::Archive not found: $archive" | |
| exit 1 | |
| fi | |
| gh release upload "$tag" "$archive" --clobber | |
| checksums: | |
| name: Generate and upload checksums | |
| needs: [meta, build-binaries] | |
| runs-on: ubuntu-latest | |
| steps: | |
| - name: Checkout | |
| uses: actions/checkout@v6 | |
| with: | |
| ref: ${{ github.event_name == 'workflow_dispatch' && github.event.inputs.tag || '' }} | |
| - name: Verify all 4 expected archives are present | |
| env: | |
| GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| run: | | |
| set -euo pipefail | |
| tag="${{ needs.meta.outputs.tag }}" | |
| # Expected archives for the 4 build targets | |
| expected=( | |
| "basemind-x86_64-unknown-linux-gnu.tar.gz" | |
| "basemind-aarch64-unknown-linux-gnu.tar.gz" | |
| "basemind-aarch64-apple-darwin.tar.gz" | |
| "basemind-x86_64-pc-windows-msvc.zip" | |
| ) | |
| # Fetch the list of release assets | |
| gh release view "$tag" --json assets -q '.assets[].name' > /tmp/release_assets.txt || true | |
| missing_count=0 | |
| for expected_asset in "${expected[@]}"; do | |
| if ! grep -q "^${expected_asset}$" /tmp/release_assets.txt 2>/dev/null; then | |
| echo "::error::Missing expected archive: $expected_asset" | |
| missing_count=$((missing_count + 1)) | |
| fi | |
| done | |
| if [ $missing_count -gt 0 ]; then | |
| echo "::error::Release is incomplete: $missing_count of 4 platform archives missing. The release must have all 4 binaries before checksums can be generated." | |
| exit 1 | |
| fi | |
| echo "✓ All 4 expected archives present" | |
| - name: Download archives and generate checksums | |
| env: | |
| GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| run: | | |
| set -euo pipefail | |
| tag="${{ needs.meta.outputs.tag }}" | |
| # Download all archives from the release | |
| gh release view "$tag" --json assets -q '.assets[] | select(.name | test("basemind-.+\\.(tar\\.gz|zip)$")).name' | while read -r asset; do | |
| echo "Downloading $asset..." | |
| gh release download "$tag" -p "$asset" -O "$asset" | |
| done | |
| # Generate checksums file with explicit platform list for clarity | |
| sha256sum basemind-*.tar.gz basemind-*.zip | sort > "basemind_${{ needs.meta.outputs.version }}_checksums.txt" | |
| echo "✓ Generated checksums file" | |
| cat "basemind_${{ needs.meta.outputs.version }}_checksums.txt" | |
| - name: Upload checksums to release | |
| env: | |
| GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| run: | | |
| tag="${{ needs.meta.outputs.tag }}" | |
| gh release upload "$tag" basemind_${{ needs.meta.outputs.version }}_checksums.txt --clobber | |
| echo "✓ Uploaded checksums to release" | |
| publish_crates: | |
| name: Publish crates.io | |
| needs: meta | |
| if: needs.meta.outputs.crates_exists != 'true' | |
| runs-on: ubuntu-latest | |
| steps: | |
| - name: Checkout | |
| uses: actions/checkout@v6 | |
| with: | |
| fetch-depth: 0 | |
| ref: ${{ github.event_name == 'workflow_dispatch' && github.event.inputs.tag || '' }} | |
| - name: Setup Rust | |
| uses: dtolnay/rust-toolchain@stable | |
| - name: Authenticate to crates.io | |
| uses: rust-lang/crates-io-auth-action@v1 | |
| id: crates_auth | |
| - name: Publish to crates.io | |
| env: | |
| CARGO_REGISTRY_TOKEN: ${{ steps.crates_auth.outputs.token }} | |
| run: | | |
| set -euo pipefail | |
| if cargo publish --allow-dirty 2>cargo-publish.err; then | |
| exit 0 | |
| fi | |
| if grep -Eq "crate .* already exists" cargo-publish.err; then | |
| echo "crate already published; skipping" | |
| exit 0 | |
| fi | |
| cat cargo-publish.err >&2 | |
| exit 1 | |
| publish_npm: | |
| name: Publish npm | |
| needs: [meta, checksums] | |
| if: needs.meta.outputs.npm_exists != 'true' && needs.checksums.result == 'success' | |
| runs-on: ubuntu-latest | |
| steps: | |
| - name: Checkout | |
| uses: actions/checkout@v6 | |
| with: | |
| fetch-depth: 0 | |
| ref: ${{ github.event_name == 'workflow_dispatch' && github.event.inputs.tag || '' }} | |
| - name: Setup Node.js | |
| uses: actions/setup-node@v6 | |
| with: | |
| node-version: "20" | |
| registry-url: "https://registry.npmjs.org" | |
| - name: Update npm | |
| run: npm install -g npm@latest | |
| - name: Publish to npm | |
| env: | |
| NODE_AUTH_TOKEN: "" | |
| run: | | |
| unset NODE_AUTH_TOKEN | |
| cd npm-package | |
| version="${{ needs.meta.outputs.version }}" | |
| if [[ "$version" == *"-rc."* ]]; then | |
| npm_tag="beta" | |
| else | |
| npm_tag="latest" | |
| fi | |
| npm publish --provenance --access public --tag "$npm_tag" | |
| publish_opencode: | |
| name: Publish npm (basemind-opencode) | |
| needs: [meta, checksums] | |
| if: needs.meta.outputs.opencode_exists != 'true' && needs.checksums.result == 'success' | |
| runs-on: ubuntu-latest | |
| steps: | |
| - name: Checkout | |
| uses: actions/checkout@v6 | |
| with: | |
| fetch-depth: 0 | |
| ref: ${{ github.event_name == 'workflow_dispatch' && github.event.inputs.tag || '' }} | |
| - name: Setup Node.js | |
| uses: actions/setup-node@v6 | |
| with: | |
| node-version: "20" | |
| registry-url: "https://registry.npmjs.org" | |
| - name: Update npm | |
| run: npm install -g npm@latest | |
| - name: Publish to npm | |
| env: | |
| NODE_AUTH_TOKEN: "" | |
| run: | | |
| unset NODE_AUTH_TOKEN | |
| cd opencode-plugin | |
| version="${{ needs.meta.outputs.version }}" | |
| if [[ "$version" == *"-rc."* ]]; then | |
| npm_tag="beta" | |
| else | |
| npm_tag="latest" | |
| fi | |
| npm publish --provenance --access public --tag "$npm_tag" | |
| publish_pypi: | |
| name: Publish PyPI | |
| needs: [meta, checksums] | |
| if: needs.meta.outputs.pypi_exists != 'true' && needs.checksums.result == 'success' | |
| runs-on: ubuntu-latest | |
| environment: pypi | |
| steps: | |
| - name: Checkout | |
| uses: actions/checkout@v6 | |
| with: | |
| fetch-depth: 0 | |
| ref: ${{ github.event_name == 'workflow_dispatch' && github.event.inputs.tag || '' }} | |
| - name: Setup Python | |
| uses: actions/setup-python@v6 | |
| with: | |
| python-version: "3.8" | |
| - name: Build Python package | |
| run: | | |
| python -m pip install --upgrade pip | |
| python -m pip install build | |
| cd pip-package | |
| python -m build | |
| - name: Publish to PyPI | |
| uses: pypa/gh-action-pypi-publish@release/v1 | |
| with: | |
| packages-dir: pip-package/dist | |
| skip-existing: true | |
| publish_homebrew: | |
| name: Update Homebrew formula | |
| # Stable releases only — rc/beta/alpha pre-releases do not update the tap. | |
| # Requires checksums job to succeed (which ensures all 4 binaries exist). | |
| needs: [meta, checksums] | |
| if: needs.checksums.result == 'success' && !contains(needs.meta.outputs.version, '-rc') && !contains(needs.meta.outputs.version, '-beta') && !contains(needs.meta.outputs.version, '-alpha') | |
| runs-on: ubuntu-latest | |
| steps: | |
| - name: Checkout | |
| uses: actions/checkout@v6 | |
| with: | |
| fetch-depth: 0 | |
| ref: ${{ github.event_name == 'workflow_dispatch' && github.event.inputs.tag || '' }} | |
| - name: Generate formula and push to tap | |
| env: | |
| GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| HOMEBREW_TOKEN: ${{ secrets.HOMEBREW_TOKEN }} | |
| run: | | |
| set -euo pipefail | |
| if [ -z "${HOMEBREW_TOKEN:-}" ]; then | |
| echo "HOMEBREW_TOKEN not set — skipping the Homebrew formula update." | |
| exit 0 | |
| fi | |
| tag="${{ needs.meta.outputs.tag }}" | |
| version="${{ needs.meta.outputs.version }}" | |
| gh release download "$tag" -p "basemind_${version}_checksums.txt" -O checksums.txt | |
| ./scripts/update-homebrew-formula.sh "$version" checksums.txt > basemind.rb | |
| git clone --depth 1 "https://x-access-token:${HOMEBREW_TOKEN}@github.com/Goldziher/homebrew-tap.git" tap | |
| cp basemind.rb tap/Formula/basemind.rb | |
| cd tap | |
| git config user.name "github-actions[bot]" | |
| git config user.email "github-actions[bot]@users.noreply.github.com" | |
| git add Formula/basemind.rb | |
| if git diff --cached --quiet; then | |
| echo "Formula already up to date." | |
| else | |
| git commit -m "basemind ${version}" | |
| git push | |
| fi |