release: prepare v0.2.1 corrective install release #31
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
| # SPDX-License-Identifier: Apache-2.0 | |
| # Copyright 2026 Firelock, LLC | |
| name: Release | |
| on: | |
| push: | |
| tags: | |
| - "v*.*.*" | |
| permissions: | |
| contents: write | |
| env: | |
| CARGO_TERM_COLOR: always | |
| jobs: | |
| build: | |
| name: Build (${{ matrix.artifact }}) | |
| runs-on: ${{ matrix.os }} | |
| # Windows is a work-in-progress release target (FIR-847 / stale kin-model | |
| # path in the vector-free patch). It must not block the macOS/Linux release: | |
| # the experimental leg is non-blocking so Publish Release + npm still run. | |
| continue-on-error: ${{ matrix.experimental || false }} | |
| # macOS signing/notarization secrets are surfaced as env so they can be used | |
| # in step `if:` guards. The `secrets` context is NOT available in `if:` | |
| # conditions, so guarding on `env.MACOS_CERTIFICATE != ''` is the supported | |
| # pattern. When the secrets are unset every signing step is skipped and the | |
| # pipeline still builds, packages, and publishes the (unsigned) binaries. | |
| env: | |
| MACOS_CERTIFICATE: ${{ secrets.MACOS_CERTIFICATE }} | |
| MACOS_CERTIFICATE_PWD: ${{ secrets.MACOS_CERTIFICATE_PWD }} | |
| MACOS_DEVELOPER_ID: ${{ secrets.MACOS_DEVELOPER_ID }} | |
| APPLE_ID: ${{ secrets.APPLE_ID }} | |
| APPLE_APP_SPECIFIC_PASSWORD: ${{ secrets.APPLE_APP_SPECIFIC_PASSWORD }} | |
| APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }} | |
| strategy: | |
| fail-fast: false | |
| matrix: | |
| include: | |
| - os: ubuntu-latest | |
| target: x86_64-unknown-linux-gnu | |
| artifact: kin-linux-x86_64 | |
| shim_name: libkin_vfs_shim.so | |
| - os: ubuntu-24.04-arm | |
| target: aarch64-unknown-linux-gnu | |
| artifact: kin-linux-aarch64 | |
| shim_name: libkin_vfs_shim.so | |
| - os: macos-latest | |
| target: x86_64-apple-darwin | |
| artifact: kin-macos-x86_64 | |
| shim_name: libkin_vfs_shim.dylib | |
| - os: macos-latest | |
| target: aarch64-apple-darwin | |
| artifact: kin-macos-aarch64 | |
| shim_name: libkin_vfs_shim.dylib | |
| - os: windows-latest | |
| target: x86_64-pc-windows-msvc | |
| artifact: kin-windows-x86_64 | |
| shim_name: kin_vfs_shim.dll | |
| skip_vector: true | |
| experimental: true | |
| steps: | |
| - name: Checkout | |
| uses: actions/checkout@v6 | |
| - name: Install Rust stable | |
| uses: dtolnay/rust-toolchain@1.96.0 | |
| with: | |
| targets: ${{ matrix.target }} | |
| - name: Install cross (ARM64) | |
| if: matrix.cross | |
| run: cargo install cross --git https://github.com/cross-rs/cross | |
| - name: Checkout kin-vfs | |
| uses: actions/checkout@v6 | |
| with: | |
| repository: firelock-ai/kin-vfs | |
| path: kin-vfs | |
| - name: Checkout kin-db (for Windows vector-free build) | |
| if: ${{ matrix.skip_vector }} | |
| uses: actions/checkout@v6 | |
| with: | |
| repository: firelock-ai/kin-db | |
| path: kin-db-local | |
| - name: Patch kin-db to disable vector on Windows | |
| if: ${{ matrix.skip_vector }} | |
| shell: bash | |
| run: | | |
| # 1. Remove vector feature from kin-db defaults | |
| sed -i 's/^default = \["vector"\]/default = []/' kin-db-local/crates/kin-db/Cargo.toml | |
| # 2. Remove features = ["vector"] from kin's dep on kin-db | |
| sed -i 's/, features = \["vector"\]//' Cargo.toml | |
| # 3. Point kin's dep to the patched local checkout | |
| mkdir -p .cargo | |
| cat >> .cargo/config.toml << 'TOML' | |
| [patch."https://github.com/firelock-ai/kin-db.git"] | |
| kin-db = { path = "kin-db-local/crates/kin-db" } | |
| # kin-model is its own repo (no longer vendored under kin-db/crates) — | |
| # it resolves from the main [patch.kin] git pin like every other | |
| # platform; only kin-db needs the local vector-disabled override here. | |
| TOML | |
| - name: Build kin-cli + kin-daemon (native) | |
| if: ${{ !matrix.cross }} | |
| shell: bash | |
| run: | | |
| FLAGS="" | |
| if [ "$SKIP_VECTOR" = "true" ]; then | |
| FLAGS="--no-default-features" | |
| fi | |
| # FIR-967: the daemon IS the runtime — `kin init` works without it but | |
| # `kin status`/`kin search`/the MCP server all require kin-daemon on | |
| # PATH, so a clean public install must ship it alongside `kin`. | |
| cargo build --release --target "$TARGET" -p kin-cli -p kin-daemon $FLAGS | |
| env: | |
| TARGET: ${{ matrix.target }} | |
| SKIP_VECTOR: ${{ matrix.skip_vector }} | |
| - name: Build kin-cli + kin-daemon (cross) | |
| if: ${{ matrix.cross }} | |
| run: cross build --release --target "$TARGET" -p kin-cli -p kin-daemon | |
| env: | |
| TARGET: ${{ matrix.target }} | |
| - name: Build kin-vfs (native) | |
| if: ${{ !matrix.cross && !matrix.skip_vector }} | |
| working-directory: kin-vfs | |
| shell: bash | |
| run: | | |
| cargo build --release --target "$TARGET" -p kin-vfs-cli | |
| cargo build --release --target "$TARGET" -p kin-vfs-shim | |
| env: | |
| TARGET: ${{ matrix.target }} | |
| - name: Build kin-vfs (cross) | |
| if: ${{ matrix.cross }} | |
| working-directory: kin-vfs | |
| run: | | |
| cross build --release --target "$TARGET" -p kin-vfs-cli | |
| cross build --release --target "$TARGET" -p kin-vfs-shim | |
| env: | |
| TARGET: ${{ matrix.target }} | |
| # --- macOS code signing + notarization ------------------------------- | |
| # Runs only on the macOS legs, and only when signing secrets are present | |
| # (see the job-level env block). Signing happens BEFORE packaging so the | |
| # tarball + its published sha256 cover the signed binaries. | |
| - name: Import code signing certificate (macOS) | |
| if: ${{ runner.os == 'macOS' && env.MACOS_CERTIFICATE != '' }} | |
| run: | | |
| CERT_PATH="$RUNNER_TEMP/build_certificate.p12" | |
| KEYCHAIN_PATH="$RUNNER_TEMP/app-signing.keychain-db" | |
| KEYCHAIN_PWD="$(openssl rand -base64 24)" | |
| echo -n "$MACOS_CERTIFICATE" | base64 --decode -o "$CERT_PATH" | |
| security create-keychain -p "$KEYCHAIN_PWD" "$KEYCHAIN_PATH" | |
| security set-keychain-settings -lut 21600 "$KEYCHAIN_PATH" | |
| security unlock-keychain -p "$KEYCHAIN_PWD" "$KEYCHAIN_PATH" | |
| security import "$CERT_PATH" -P "$MACOS_CERTIFICATE_PWD" -A -t cert -f pkcs12 -k "$KEYCHAIN_PATH" | |
| security set-key-partition-list -S apple-tool:,apple: -k "$KEYCHAIN_PWD" "$KEYCHAIN_PATH" | |
| security list-keychain -d user -s "$KEYCHAIN_PATH" | |
| rm -f "$CERT_PATH" | |
| - name: Sign macOS binaries | |
| if: ${{ runner.os == 'macOS' && env.MACOS_CERTIFICATE != '' }} | |
| run: | | |
| for f in \ | |
| "target/${TARGET}/release/kin" \ | |
| "target/${TARGET}/release/kin-daemon" \ | |
| "kin-vfs/target/${TARGET}/release/kin-vfs" \ | |
| "kin-vfs/target/${TARGET}/release/${SHIM_NAME}"; do | |
| if [ -f "$f" ]; then | |
| codesign --force --options runtime --timestamp \ | |
| --sign "$MACOS_DEVELOPER_ID" "$f" | |
| codesign --verify --strict --verbose=2 "$f" | |
| fi | |
| done | |
| env: | |
| TARGET: ${{ matrix.target }} | |
| SHIM_NAME: ${{ matrix.shim_name }} | |
| - name: Notarize macOS binaries | |
| if: ${{ runner.os == 'macOS' && env.MACOS_CERTIFICATE != '' && env.APPLE_ID != '' }} | |
| run: | | |
| NOTARIZE_ZIP="$RUNNER_TEMP/${ARTIFACT}-notarize.zip" | |
| FILES=() | |
| for f in \ | |
| "target/${TARGET}/release/kin" \ | |
| "target/${TARGET}/release/kin-daemon" \ | |
| "kin-vfs/target/${TARGET}/release/kin-vfs" \ | |
| "kin-vfs/target/${TARGET}/release/${SHIM_NAME}"; do | |
| [ -f "$f" ] && FILES+=("$f") | |
| done | |
| zip -j "$NOTARIZE_ZIP" "${FILES[@]}" | |
| xcrun notarytool submit "$NOTARIZE_ZIP" \ | |
| --apple-id "$APPLE_ID" \ | |
| --password "$APPLE_APP_SPECIFIC_PASSWORD" \ | |
| --team-id "$APPLE_TEAM_ID" \ | |
| --wait | |
| # NOTE: a bare CLI binary (and a .tar.gz) cannot be stapled — `xcrun | |
| # stapler` only supports .app bundles, .dmg, .pkg, and .xip. The | |
| # notarization ticket is stored on Apple's servers and validated | |
| # online by Gatekeeper on first run. If a .dmg/.pkg installer is added | |
| # later, run `xcrun stapler staple <installer>` here. | |
| env: | |
| TARGET: ${{ matrix.target }} | |
| SHIM_NAME: ${{ matrix.shim_name }} | |
| ARTIFACT: ${{ matrix.artifact }} | |
| - name: Package (Unix) | |
| if: runner.os != 'Windows' | |
| run: | | |
| mkdir "$ARTIFACT" | |
| cp "target/${TARGET}/release/kin" "$ARTIFACT/" | |
| # FIR-967: kin-daemon is mandatory (no `|| true`) — a daemon-less | |
| # archive lets `kin init` succeed but then `kin status`/`kin search` | |
| # fail with no daemon on PATH, so a missing daemon must fail the build. | |
| cp "target/${TARGET}/release/kin-daemon" "$ARTIFACT/" | |
| cp "kin-vfs/target/${TARGET}/release/kin-vfs" "$ARTIFACT/" 2>/dev/null || true | |
| cp "kin-vfs/target/${TARGET}/release/${SHIM_NAME}" "$ARTIFACT/" 2>/dev/null || true | |
| tar czf "${ARTIFACT}.tar.gz" "$ARTIFACT" | |
| # FIR-967: assert the published archive actually contains kin-daemon so | |
| # a daemon-less build can never be released green. | |
| if ! tar tzf "${ARTIFACT}.tar.gz" | grep -q "/kin-daemon$"; then | |
| echo "::error::kin-daemon missing from ${ARTIFACT}.tar.gz — refusing to publish a daemon-less archive" | |
| exit 1 | |
| fi | |
| shasum -a 256 "${ARTIFACT}.tar.gz" > "${ARTIFACT}.tar.gz.sha256" | |
| env: | |
| TARGET: ${{ matrix.target }} | |
| ARTIFACT: ${{ matrix.artifact }} | |
| SHIM_NAME: ${{ matrix.shim_name }} | |
| - name: Package (Windows) | |
| if: runner.os == 'Windows' | |
| shell: pwsh | |
| run: | | |
| New-Item -ItemType Directory -Path $env:ARTIFACT | |
| Copy-Item "target/$env:TARGET/release/kin.exe" "$env:ARTIFACT/" | |
| # FIR-967: kin-daemon is mandatory — fail hard if it's missing rather | |
| # than ship a daemon-less archive that can `kin init` but nothing else. | |
| Copy-Item "target/$env:TARGET/release/kin-daemon.exe" "$env:ARTIFACT/" -ErrorAction Stop | |
| Copy-Item "kin-vfs/target/$env:TARGET/release/kin-vfs.exe" "$env:ARTIFACT/" -ErrorAction SilentlyContinue | |
| Copy-Item "kin-vfs/target/$env:TARGET/release/$env:SHIM_NAME" "$env:ARTIFACT/" -ErrorAction SilentlyContinue | |
| Compress-Archive -Path "$env:ARTIFACT/*" -DestinationPath "$env:ARTIFACT.zip" | |
| # FIR-967: assert the produced zip actually contains kin-daemon.exe. | |
| $entries = [System.IO.Compression.ZipFile]::OpenRead("$PWD/$env:ARTIFACT.zip").Entries.Name | |
| if ($entries -notcontains "kin-daemon.exe") { | |
| Write-Error "kin-daemon.exe missing from $env:ARTIFACT.zip — refusing to publish a daemon-less archive" | |
| exit 1 | |
| } | |
| Get-FileHash -Algorithm SHA256 "$env:ARTIFACT.zip" | Format-List Hash | Out-File -Encoding utf8 "$env:ARTIFACT.zip.sha256" | |
| env: | |
| TARGET: ${{ matrix.target }} | |
| ARTIFACT: ${{ matrix.artifact }} | |
| SHIM_NAME: ${{ matrix.shim_name }} | |
| - name: Upload artifact | |
| uses: actions/upload-artifact@v7 | |
| with: | |
| name: ${{ matrix.artifact }} | |
| path: | | |
| ${{ matrix.artifact }}.tar.gz | |
| ${{ matrix.artifact }}.tar.gz.sha256 | |
| ${{ matrix.artifact }}.zip | |
| ${{ matrix.artifact }}.zip.sha256 | |
| publish: | |
| name: Publish Release | |
| needs: build | |
| runs-on: ubuntu-latest | |
| steps: | |
| - name: Checkout | |
| uses: actions/checkout@v6 | |
| - name: Download all artifacts | |
| uses: actions/download-artifact@v8 | |
| with: | |
| merge-multiple: true | |
| - name: Generate release notes from changelog | |
| run: | | |
| VERSION="${GITHUB_REF_NAME#v}" | |
| node ./scripts/extract-release-notes.mjs \ | |
| --version "$VERSION" \ | |
| --input ./CHANGELOG.md \ | |
| --output ./release-notes.md | |
| - name: Aggregate per-artifact checksums | |
| run: | | |
| # Forward-compat: combine the per-artifact "*.sha256" files into a | |
| # single checksums-sha256.txt. Installers verify against the | |
| # per-artifact files, so this is published as a convenience only. | |
| : > checksums-sha256.txt | |
| for f in *.tar.gz.sha256 *.zip.sha256; do | |
| [ -e "$f" ] || continue | |
| cat "$f" >> checksums-sha256.txt | |
| done | |
| - name: Create GitHub Release | |
| uses: softprops/action-gh-release@v2 | |
| with: | |
| body_path: ./release-notes.md | |
| generate_release_notes: true | |
| prerelease: ${{ contains(github.ref_name, '-') }} | |
| files: | | |
| kin-linux-x86_64.tar.gz | |
| kin-linux-x86_64.tar.gz.sha256 | |
| kin-linux-aarch64.tar.gz | |
| kin-linux-aarch64.tar.gz.sha256 | |
| kin-macos-x86_64.tar.gz | |
| kin-macos-x86_64.tar.gz.sha256 | |
| kin-macos-aarch64.tar.gz | |
| kin-macos-aarch64.tar.gz.sha256 | |
| kin-windows-x86_64.zip | |
| kin-windows-x86_64.zip.sha256 | |
| checksums-sha256.txt | |
| publish_npm: | |
| name: Publish npm Wrapper | |
| needs: publish | |
| runs-on: ubuntu-latest | |
| # Trusted Publishing (OIDC): npm authenticates this workflow via a short-lived | |
| # OIDC token instead of a long-lived NPM_TOKEN secret — no token to leak or | |
| # rotate, and it satisfies the @kinlab org's 2FA-for-publish policy. Requires a | |
| # Trusted Publisher configured on npm for @kinlab/kin-mcp pointing at | |
| # firelock-ai/kin + this workflow (release.yml). | |
| permissions: | |
| contents: read | |
| id-token: write | |
| steps: | |
| - name: Checkout | |
| uses: actions/checkout@v6 | |
| - name: Install Node | |
| uses: actions/setup-node@v6 | |
| with: | |
| node-version: 20 | |
| registry-url: https://registry.npmjs.org | |
| - name: Upgrade npm for OIDC trusted publishing (needs >= 11.5.1) | |
| run: npm install -g npm@latest | |
| - name: Verify npm package version matches tag | |
| run: | | |
| PACKAGE_VERSION=$(node -p "require('./packages/kin-mcp/package.json').version") | |
| TAG_VERSION="${GITHUB_REF_NAME#v}" | |
| test "$PACKAGE_VERSION" = "$TAG_VERSION" | |
| - name: Resolve npm dist-tag | |
| id: dist-tag | |
| shell: bash | |
| run: | | |
| ref="${GITHUB_REF_NAME#v}" | |
| case "$ref" in | |
| *-alpha*) tag="alpha" ;; | |
| *-beta*) tag="beta" ;; | |
| *-rc*) tag="rc" ;; | |
| *) tag="latest" ;; | |
| esac | |
| echo "tag=$tag" >> "$GITHUB_OUTPUT" | |
| - name: Publish npm package (Trusted Publishing via OIDC) | |
| run: | | |
| PKG=$(node -p "require('./packages/kin-mcp/package.json').name") | |
| VER=$(node -p "require('./packages/kin-mcp/package.json').version") | |
| if npm view "${PKG}@${VER}" version >/dev/null 2>&1; then | |
| echo "${PKG}@${VER} already published — skipping (idempotent recut)." | |
| exit 0 | |
| fi | |
| npm publish ./packages/kin-mcp --access public --tag "${NPM_DIST_TAG}" | |
| env: | |
| NPM_DIST_TAG: ${{ steps.dist-tag.outputs.tag }} | |
| publish_boundary_contracts: | |
| name: Publish boundary contracts to KinLab | |
| needs: publish | |
| runs-on: ubuntu-latest | |
| steps: | |
| - name: Checkout | |
| uses: actions/checkout@v6 | |
| - name: Install Node | |
| uses: actions/setup-node@v6 | |
| with: | |
| node-version: 20 | |
| - name: Publish @kin/boundary-contracts to KinLab registry | |
| id: publish-boundary | |
| shell: bash | |
| run: | | |
| if [ -z "${KINLAB_NPM_TOKEN:-}" ]; then | |
| echo "KINLAB_NPM_TOKEN is not configured; skipping KinLab registry publish" | |
| exit 0 | |
| fi | |
| node ./scripts/publish-boundary-contracts.mjs | |
| env: | |
| TAG_NAME: ${{ github.ref_name }} | |
| KINLAB_NPM_TOKEN: ${{ secrets.KINLAB_NPM_TOKEN }} | |
| KINLAB_NPM_REGISTRY_URL: ${{ vars.KINLAB_NPM_REGISTRY_URL }} | |
| KINLAB_NPM_DIST_TAG: ${{ contains(github.ref_name, '-alpha') && 'alpha' || contains(github.ref_name, '-beta') && 'beta' || contains(github.ref_name, '-rc') && 'rc' || 'latest' }} |