Skip to content

release: prepare v0.2.1 corrective install release #31

release: prepare v0.2.1 corrective install release

release: prepare v0.2.1 corrective install release #31

Workflow file for this run

# 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' }}