chore(release): 0.19.2 #48
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 | |
| # Create as a DRAFT. The build matrix uploads binaries to it, but it | |
| # stays invisible (assets not publicly downloadable) until the | |
| # finalize_release job promotes it — and that job only runs once | |
| # checksums (which requires all 5 platform binaries) succeeds. A failed | |
| # platform build therefore leaves a hidden draft, not a live release | |
| # with missing binaries and no checksums (the v0.10.0 failure mode that | |
| # broke the plugin launcher's checksum-verified download). | |
| gh release create "$tag" --title "$tag" --notes "Release $tag" --verify-tag --draft $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 | |
| # Intel macOS: native x86_64 build on the macos-15-intel runner. ort has no | |
| # static x86_64-apple-darwin ONNX Runtime, so this leg swaps in ort-dynamic | |
| # (ort/load-dynamic) and package-release.sh vendors libonnxruntime.dylib next | |
| # to the binary. `full` still names ort-bundled but disable-linking wins. | |
| - triple: x86_64-apple-darwin | |
| os: macos-15-intel | |
| features: full,ort-dynamic | |
| - 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 "${{ matrix.features || 'full' }}" --bin basemind \ | |
| --target "${{ matrix.triple }}" | |
| - name: Package release | |
| shell: bash | |
| run: ./scripts/package-release.sh ${{ matrix.triple }} | |
| # Intel macOS ships ONNX Runtime dynamically (ort/load-dynamic) with the dylib | |
| # vendored next to the binary. That packaging can't be exercised on the arm64 | |
| # dev machines, so validate the shipped archive here — before it's uploaded — so | |
| # a broken bundle fails the release instead of a user's first embedding call. | |
| - name: Smoke-test packaged Intel binary (ONNX Runtime loads) | |
| if: matrix.triple == 'x86_64-apple-darwin' | |
| shell: bash | |
| run: | | |
| set -euo pipefail | |
| dir="$(mktemp -d)" | |
| tar xzf "basemind-x86_64-apple-darwin.tar.gz" -C "$dir" | |
| # Binary + its otool-discovered lib/ closure load, and the ad-hoc signature is | |
| # valid (a bad install_name_tool rewrite would get the process SIGKILLed). | |
| "$dir/basemind" --version | |
| # ONNX Runtime is vendored next to the binary; ort/load-dynamic resolves it | |
| # relative to the executable. dlopen the vendored dylib + its full @loader_path | |
| # closure — the exact load ort performs at runtime — so a missing closure | |
| # member, broken install name, or invalid signature fails here. @loader_path | |
| # never falls back to the runner's Homebrew copy, so this proves the bundle is | |
| # self-contained even though onnxruntime is brew-installed on this runner. | |
| test -f "$dir/libonnxruntime.dylib" | |
| ( cd "$dir" && python3 -c "import ctypes; ctypes.CDLL('./libonnxruntime.dylib'); print('libonnxruntime.dylib + closure load OK')" ) | |
| - 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 5 expected archives are present | |
| env: | |
| GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| run: | | |
| set -euo pipefail | |
| tag="${{ needs.meta.outputs.tag }}" | |
| # Expected archives for the 5 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-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 5 platform archives missing. The release must have all 5 binaries before checksums can be generated." | |
| exit 1 | |
| fi | |
| echo "✓ All 5 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" | |
| finalize_release: | |
| name: Publish GitHub release | |
| needs: [meta, checksums] | |
| # Run after the build/checksums jobs settle, regardless of their result, so a | |
| # draft left complete by a cancelled/partial earlier run can still be promoted. | |
| # The step re-verifies completeness itself, so it never publishes an incomplete | |
| # release — it only flips the draft to live once ALL 5 platform archives AND the | |
| # checksums file are present. This is the gate that makes a partial publish a | |
| # hidden draft instead of a live, half-populated release. | |
| if: always() && needs.meta.result == 'success' | |
| runs-on: ubuntu-latest | |
| steps: | |
| - name: Promote draft to a published release once complete | |
| env: | |
| GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| run: | | |
| set -euo pipefail | |
| tag="${{ needs.meta.outputs.tag }}" | |
| version="${{ needs.meta.outputs.version }}" | |
| # The full asset set a launcher / npm install.js / pip downloader needs: | |
| # every platform archive plus the checksums file they verify against. | |
| required=( | |
| "basemind-x86_64-unknown-linux-gnu.tar.gz" | |
| "basemind-aarch64-unknown-linux-gnu.tar.gz" | |
| "basemind-aarch64-apple-darwin.tar.gz" | |
| "basemind-x86_64-apple-darwin.tar.gz" | |
| "basemind-x86_64-pc-windows-msvc.zip" | |
| "basemind_${version}_checksums.txt" | |
| ) | |
| gh release view "$tag" --repo "$GITHUB_REPOSITORY" --json assets -q '.assets[].name' \ | |
| > /tmp/assets.txt || true | |
| missing=0 | |
| for asset in "${required[@]}"; do | |
| if ! grep -qx "$asset" /tmp/assets.txt; then | |
| echo "::error::release $tag is missing $asset" | |
| missing=$((missing + 1)) | |
| fi | |
| done | |
| if [ "$missing" -gt 0 ]; then | |
| echo "::error::release $tag is incomplete ($missing required assets missing); leaving it as a draft" | |
| exit 1 | |
| fi | |
| gh release edit "$tag" --draft=false --repo "$GITHUB_REPOSITORY" | |
| echo "✓ Promoted $tag to a published 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 | |
| # Gated on finalize_release: the npm install.js downloads the platform binary | |
| # from the GitHub release, so it must not publish until that release is live. | |
| needs: [meta, finalize_release] | |
| if: needs.meta.outputs.npm_exists != 'true' && needs.finalize_release.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, finalize_release] | |
| if: needs.meta.outputs.opencode_exists != 'true' && needs.finalize_release.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 | |
| # Gated on finalize_release: the pip downloader fetches the platform binary | |
| # from the GitHub release, so it must not publish until that release is live. | |
| needs: [meta, finalize_release] | |
| if: needs.meta.outputs.pypi_exists != 'true' && needs.finalize_release.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 the release to be finalized (all 5 binaries + checksums present and | |
| # the release published), since the formula points at the release assets. | |
| needs: [meta, finalize_release] | |
| if: needs.finalize_release.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 |