Publish Packages #29
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 | |
| # macOS + Windows binaries. Linux binaries are built separately in | |
| # build-linux-binaries (a manylinux_2_28 container is required to link ort's | |
| # prebuilt ONNX Runtime at a glibc 2.28 floor — see that job). | |
| 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: 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 | |
| shell: bash | |
| 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 | |
| # Linux binaries (x86_64 + aarch64). Built inside the official manylinux_2_28 | |
| # containers with native gcc so ort's prebuilt ONNX Runtime links at a glibc | |
| # 2.28 floor (RHEL 8 / Debian 11 / Ubuntu 20.04+ / Amazon Linux 2023). zig as | |
| # the linker cannot do this (libc++ vs the prebuilt's libstdc++ ABI), and the | |
| # prebuilt itself references glibc 2.38 symbols — backfilled by the small | |
| # scripts/glibc228_compat.c shim linked via a target-scoped RUSTFLAGS. | |
| build-linux-binaries: | |
| name: Build Linux binaries / ${{ matrix.triple }} | |
| needs: [meta, create_release] | |
| if: needs.meta.outputs.release_assets_exist != 'true' | |
| runs-on: ${{ matrix.os }} | |
| container: ${{ matrix.image }} | |
| strategy: | |
| fail-fast: false | |
| matrix: | |
| include: | |
| - triple: x86_64-unknown-linux-gnu | |
| os: ubuntu-latest | |
| image: quay.io/pypa/manylinux_2_28_x86_64:latest | |
| protoc_arch: linux-x86_64 | |
| - triple: aarch64-unknown-linux-gnu | |
| os: ubuntu-24.04-arm | |
| image: quay.io/pypa/manylinux_2_28_aarch64:latest | |
| protoc_arch: linux-aarch_64 | |
| steps: | |
| - name: Checkout | |
| uses: actions/checkout@v4 | |
| with: | |
| fetch-depth: 0 | |
| ref: ${{ github.event_name == 'workflow_dispatch' && github.event.inputs.tag || '' }} | |
| - name: Install Rust (in container) | |
| shell: bash | |
| run: | | |
| set -euo pipefail | |
| # Pin CARGO_HOME/RUSTUP_HOME to absolute paths and export them to | |
| # GITHUB_ENV. Container steps do not share $HOME reliably, so rustup | |
| # installs its default toolchain under one HOME and a later step cannot | |
| # find it ("no default is configured"). Fixed paths remove the ambiguity. | |
| export CARGO_HOME=/opt/cargo | |
| export RUSTUP_HOME=/opt/rustup | |
| curl -fsSL https://sh.rustup.rs | \ | |
| sh -s -- -y --profile minimal --default-toolchain stable | |
| export PATH="$CARGO_HOME/bin:$PATH" | |
| rustup default stable | |
| rustup target add "${{ matrix.triple }}" | |
| echo "CARGO_HOME=/opt/cargo" >> "$GITHUB_ENV" | |
| echo "RUSTUP_HOME=/opt/rustup" >> "$GITHUB_ENV" | |
| echo "/opt/cargo/bin" >> "$GITHUB_PATH" | |
| - name: Install system dependencies (AlmaLinux 8) | |
| shell: bash | |
| run: | | |
| set -euo pipefail | |
| dnf install -y epel-release | |
| dnf install -y dnf-plugins-core | |
| # gh CLI for the release-asset upload from inside the container. | |
| dnf config-manager --add-repo https://cli.github.com/packages/rpm/gh-cli.repo | |
| # Core build + image/text libraries. Some codecs (libde265, x265, dav1d) | |
| # and libmagic-devel are unavailable on AlmaLinux 8; libaom is via EPEL. | |
| # tesseract-langpack-eng conflicts with tesseract, so install tesseract only. | |
| dnf install -y \ | |
| gcc gcc-c++ make cmake pkgconfig \ | |
| openssl-devel \ | |
| tesseract \ | |
| libaom-devel \ | |
| patchelf gh \ | |
| git curl wget unzip | |
| echo "gcc: $(gcc --version | head -1)" | |
| - name: Install protoc from official release | |
| shell: bash | |
| run: | | |
| set -euo pipefail | |
| ver="28.2" | |
| zip="protoc-${ver}-${{ matrix.protoc_arch }}.zip" | |
| mkdir -p /usr/local/protoc | |
| cd /tmp | |
| curl -fsSL -o "$zip" \ | |
| "https://github.com/protocolbuffers/protobuf/releases/download/v${ver}/${zip}" | |
| unzip -q "$zip" -d /usr/local/protoc | |
| ln -sf /usr/local/protoc/bin/protoc /usr/local/bin/protoc | |
| echo "PROTOC=/usr/local/bin/protoc" >> "$GITHUB_ENV" | |
| protoc --version | |
| - name: Build libheif from source (1.23.0) | |
| shell: bash | |
| run: | | |
| set -euo pipefail | |
| ver="1.23.0" | |
| d="$(mktemp -d)"; trap 'rm -rf "$d"' EXIT | |
| cd "$d" | |
| curl -fsSL -o libheif.tar.gz \ | |
| "https://github.com/strukturag/libheif/releases/download/v${ver}/libheif-${ver}.tar.gz" | |
| tar xzf libheif.tar.gz | |
| cd "libheif-${ver}" | |
| mkdir build && cd build | |
| # Disable codecs unavailable on AlmaLinux 8 (libde265, x265, dav1d); | |
| # libaom is present via EPEL. | |
| cmake .. \ | |
| -DCMAKE_BUILD_TYPE=Release \ | |
| -DCMAKE_INSTALL_PREFIX=/usr/local \ | |
| -DCMAKE_INSTALL_LIBDIR=lib \ | |
| -DWITH_EXAMPLES=OFF \ | |
| -DWITH_GDK_PIXBUF=OFF \ | |
| -DBUILD_TESTING=OFF \ | |
| -DWITH_LIBDE265=OFF \ | |
| -DWITH_X265=OFF \ | |
| -DWITH_DAV1D=OFF \ | |
| -DWITH_AOM=ON | |
| make -j"$(nproc)" | |
| make install | |
| ldconfig | |
| { | |
| echo "PKG_CONFIG_PATH=/usr/local/lib/pkgconfig:${PKG_CONFIG_PATH:-}" | |
| echo "LD_LIBRARY_PATH=/usr/local/lib:${LD_LIBRARY_PATH:-}" | |
| } >> "$GITHUB_ENV" | |
| - name: Build release binary (native gcc + glibc-2.28 compat shim) | |
| shell: bash | |
| env: | |
| TRIPLE: ${{ matrix.triple }} | |
| run: | | |
| set -euo pipefail | |
| # Compile the glibc-2.28 compat shim and link it into the binary so | |
| # ort's prebuilt ONNX Runtime (which references glibc 2.38 symbols) | |
| # links against the 2.28 floor. Scoped to this target's RUSTFLAGS. | |
| shim_obj="$PWD/glibc228_compat.o" | |
| gcc -O2 -fPIC -c scripts/glibc228_compat.c -o "$shim_obj" | |
| rustflags_var="CARGO_TARGET_$(echo "$TRIPLE" | tr 'a-z-' 'A-Z_')_RUSTFLAGS" | |
| export "${rustflags_var}=-C link-arg=${shim_obj}" | |
| cargo build --release --features full --bin basemind --target "$TRIPLE" | |
| - name: Verify glibc floor (<= 2.28) | |
| shell: bash | |
| run: | | |
| set -euo pipefail | |
| bin="target/${{ matrix.triple }}/release/basemind" | |
| max="$(objdump -T "$bin" | grep -oE 'GLIBC_[0-9]+\.[0-9]+' | sort -V | tail -1 || true)" | |
| echo "highest glibc symbol required: ${max:-none}" | |
| if [ -n "$max" ] && \ | |
| [ "$(printf '%s\nGLIBC_2.28\n' "$max" | sort -V | tail -1)" != "GLIBC_2.28" ]; then | |
| echo "::error::basemind requires $max (> GLIBC_2.28); a dependency pulled a newer glibc symbol" | |
| exit 1 | |
| fi | |
| echo "✓ glibc floor verified: ${max:-none} <= GLIBC_2.28" | |
| - 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: | | |
| set -euo pipefail | |
| tag="${{ needs.meta.outputs.tag }}" | |
| archive="basemind-${{ matrix.triple }}.tar.gz" | |
| if [ ! -f "$archive" ]; then | |
| echo "::error::Archive not found: $archive" | |
| exit 1 | |
| fi | |
| # The container runs as root but the workspace is owned by the runner | |
| # uid; gh shells out to git, which refuses with "dubious ownership". | |
| # Mark it safe and pass --repo so gh never needs the local git remote. | |
| git config --global --add safe.directory "${GITHUB_WORKSPACE:-/__w/basemind/basemind}" | |
| gh release upload "$tag" "$archive" --clobber --repo "$GITHUB_REPOSITORY" | |
| checksums: | |
| name: Generate and upload checksums | |
| needs: [meta, build-binaries, build-linux-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 |