name: Release
on:
push:
tags:
- 'v*.*.*'
- '*.*.*'
workflow_dispatch:
inputs:
dry_run_tag:
description: Tag-like version to validate without creating a release.
required: true
default: v1.1.14
permissions:
contents: read
concurrency:
group: release-${{ github.ref }}
cancel-in-progress: false
env:
CARGO_TERM_COLOR: always
BIN_NAME: relay-knowledge
jobs:
verify:
name: Verify release inputs and quality gates
runs-on: ubuntu-latest
outputs:
tag: ${{ steps.version.outputs.tag }}
version: ${{ steps.version.outputs.version }}
prerelease: ${{ steps.version.outputs.prerelease }}
publish_crate: ${{ steps.version.outputs.publish_crate }}
steps:
- name: Checkout repository
uses: actions/checkout@v6
- name: Install Rust toolchain
run: rustup toolchain install stable --profile minimal --component rustfmt --component clippy
- name: Validate release version
id: version
shell: bash
run: |
set -euo pipefail
if [[ "${{ github.event_name }}" == "workflow_dispatch" ]]; then
tag="${{ inputs.dry_run_tag }}"
else
tag="${GITHUB_REF_NAME}"
fi
if [[ ! "$tag" =~ ^v?[0-9]+\.[0-9]+\.[0-9]+(-[0-9A-Za-z.-]+)?$ ]]; then
echo "Release tag must look like v1.0.0, 1.0.0, or v1.0.0-rc.1: $tag" >&2
exit 1
fi
version="${tag#v}"
manifest_version="$(cargo metadata --no-deps --format-version 1 \
| python3 -c 'import json,sys; print(json.load(sys.stdin)["packages"][0]["version"])')"
if [[ "$version" != "$manifest_version" ]]; then
echo "Tag $tag does not match Cargo.toml package.version $manifest_version" >&2
exit 1
fi
prerelease=false
publish_crate=true
if [[ "$version" == *-* || "${{ github.event_name }}" == "workflow_dispatch" ]]; then
prerelease=true
publish_crate=false
fi
{
echo "tag=$tag"
echo "version=$version"
echo "prerelease=$prerelease"
echo "publish_crate=$publish_crate"
} >> "$GITHUB_OUTPUT"
- name: Documentation structure and local-link gate
run: |
python3 tools/docs/check_docs.py --self-test
python3 tools/docs/check_docs.py
- name: Format gate
run: cargo fmt --all -- --check
- name: Clippy gate
run: cargo clippy --all-targets --all-features -- -D warnings
- name: Unit and integration test suite
run: cargo test --all-targets --all-features
- name: Package crate
run: cargo package
- name: Validate CLI skill bundle
shell: bash
run: |
set -euo pipefail
manifest_version="$(cargo metadata --no-deps --format-version 1 \
| python3 -c 'import json,sys; print(json.load(sys.stdin)["packages"][0]["version"])')"
test -f skills/relay-knowledge-cli/README.md
test -f skills/relay-knowledge-cli/SKILL.md
test -f skills/relay-knowledge-cli/agents/openai.yaml
test -f skills/relay-knowledge-cli/references/cli-workflows.md
test -f skills/relay-knowledge-cli/references/knowledge-map-workflows.md
grep -q 'Relay Knowledge CLI Skill' skills/relay-knowledge-cli/README.md
grep -q 'assets/linux-x86_64/relay-knowledge' skills/relay-knowledge-cli/README.md
grep -q 'does not configure MCP' skills/relay-knowledge-cli/README.md
grep -q '^name: relay-knowledge-cli$' skills/relay-knowledge-cli/SKILL.md
python3 tools/release/update_skill_metadata_version.py --self-test --check \
skills/relay-knowledge-cli/SKILL.md "$manifest_version"
grep -q 'relay-knowledge version check --format json' skills/relay-knowledge-cli/SKILL.md
grep -q 'assets/linux-x86_64/relay-knowledge' skills/relay-knowledge-cli/SKILL.md
grep -q 'Prefer the bundled `assets` binary' skills/relay-knowledge-cli/SKILL.md
grep -q 'Version comparisons are diagnostic only' skills/relay-knowledge-cli/SKILL.md
grep -q 'HTTPS_PROXY' skills/relay-knowledge-cli/references/cli-workflows.md
grep -q 'repo query' skills/relay-knowledge-cli/SKILL.md
grep -q 'Repository Knowledge Bootstrap' skills/relay-knowledge-cli/SKILL.md
grep -q 'Spec-Grounded Incremental Loop' skills/relay-knowledge-cli/SKILL.md
grep -q 'Do not use this skill for MCP' skills/relay-knowledge-cli/SKILL.md
grep -q 'default_prompt: "Use \$relay-knowledge-cli' skills/relay-knowledge-cli/agents/openai.yaml
- name: Publish dry run
run: cargo publish --dry-run
- name: Verify repository is unchanged after checks
run: git diff --exit-code
build:
name: Build ${{ matrix.target }}
needs: verify
strategy:
fail-fast: false
matrix:
include:
- target: x86_64-unknown-linux-gnu
os: ubuntu-latest
archive: tar.gz
linux_gnu: true
linux_glibc_max: '2.31'
linux_linker: ''
- target: aarch64-unknown-linux-gnu
os: ubuntu-latest
archive: tar.gz
linux_gnu: true
linux_glibc_max: '2.31'
linux_linker: aarch64-linux-gnu-gcc
- target: x86_64-apple-darwin
os: macos-15-intel
archive: tar.gz
- target: aarch64-apple-darwin
os: macos-14
archive: tar.gz
- target: x86_64-pc-windows-msvc
os: windows-latest
archive: zip
msvc_arch: amd64
- target: aarch64-pc-windows-msvc
os: windows-latest
archive: zip
msvc_arch: amd64_arm64
runs-on: ${{ matrix.os }}
steps:
- name: Checkout repository
uses: actions/checkout@v6
- name: Install Rust toolchain
if: matrix.linux_gnu != true
run: rustup toolchain install stable --profile minimal --target ${{ matrix.target }}
- name: Set up MSVC toolchain
if: runner.os == 'Windows'
uses: ilammy/msvc-dev-cmd@v1
with:
arch: ${{ matrix.msvc_arch }}
- name: Build release binary
if: matrix.linux_gnu != true
run: cargo build --release --locked --target ${{ matrix.target }}
- name: Build Linux GNU release binary on glibc 2.31
if: matrix.linux_gnu == true
shell: bash
env:
TARGET: ${{ matrix.target }}
LINKER: ${{ matrix.linux_linker }}
run: |
set -euo pipefail
docker run --rm \
-v "$PWD:/workspace" \
-w /workspace \
-e CARGO_TERM_COLOR \
-e TARGET \
-e LINKER \
ubuntu:20.04 \
bash -lc '
set -euo pipefail
export DEBIAN_FRONTEND=noninteractive
apt-get update
apt-get install -y --no-install-recommends \
build-essential ca-certificates curl git pkg-config
if [[ -n "${LINKER:-}" ]]; then
apt-get install -y --no-install-recommends \
gcc-aarch64-linux-gnu libc6-dev-arm64-cross
fi
curl --proto "=https" --tlsv1.2 -sSf https://sh.rustup.rs \
| sh -s -- -y --profile minimal --default-toolchain stable
. "$HOME/.cargo/env"
rustup target add "$TARGET"
if [[ -n "${LINKER:-}" ]]; then
export CARGO_TARGET_AARCH64_UNKNOWN_LINUX_GNU_LINKER="$LINKER"
fi
cargo build --release --locked --target "$TARGET"
chown -R "$(stat -c "%u:%g" /workspace)" /workspace/target
'
- name: Verify Linux GNU glibc compatibility
if: matrix.linux_gnu == true
shell: bash
run: |
set -euo pipefail
python3 tools/release/check_linux_glibc_compat.py \
--max "${{ matrix.linux_glibc_max }}" \
"target/${{ matrix.target }}/release/${BIN_NAME}"
- name: Smoke test release binary
if: matrix.target == 'x86_64-unknown-linux-gnu' || matrix.target == 'x86_64-apple-darwin' || matrix.target == 'aarch64-apple-darwin'
shell: bash
run: |
set -euo pipefail
target/${{ matrix.target }}/release/${BIN_NAME} --version
target/${{ matrix.target }}/release/${BIN_NAME} status --format json
target/${{ matrix.target }}/release/${BIN_NAME} service doctor --format json
target/${{ matrix.target }}/release/${BIN_NAME} service plan upgrade --target-version "${{ needs.verify.outputs.version }}" --format json
target/${{ matrix.target }}/release/${BIN_NAME} service lifecycle install --dry-run --format json
- name: Smoke test Windows x64 release binary
if: matrix.target == 'x86_64-pc-windows-msvc'
shell: pwsh
run: |
$ErrorActionPreference = "Stop"
target\${{ matrix.target }}\release\${{ env.BIN_NAME }}.exe --version
target\${{ matrix.target }}\release\${{ env.BIN_NAME }}.exe status --format json
target\${{ matrix.target }}\release\${{ env.BIN_NAME }}.exe service doctor --format json
target\${{ matrix.target }}\release\${{ env.BIN_NAME }}.exe service plan upgrade --target-version "${{ needs.verify.outputs.version }}" --format json
target\${{ matrix.target }}\release\${{ env.BIN_NAME }}.exe service lifecycle install --dry-run --format json
- name: Prepare archive directory
shell: bash
run: |
set -euo pipefail
package="${BIN_NAME}-${{ needs.verify.outputs.tag }}-${{ matrix.target }}"
mkdir -p "dist/$package"
if [[ "${{ runner.os }}" == "Windows" ]]; then
cp "target/${{ matrix.target }}/release/${BIN_NAME}.exe" "dist/$package/"
else
cp "target/${{ matrix.target }}/release/${BIN_NAME}" "dist/$package/"
fi
cp README.md LICENSE "dist/$package/"
- name: Create tar archive
if: matrix.archive == 'tar.gz'
shell: bash
run: |
set -euo pipefail
package="${BIN_NAME}-${{ needs.verify.outputs.tag }}-${{ matrix.target }}"
tar -C dist -czf "dist/$package.tar.gz" "$package"
- name: Create zip archive
if: matrix.archive == 'zip'
shell: pwsh
run: |
$ErrorActionPreference = "Stop"
$package = "${{ env.BIN_NAME }}-${{ needs.verify.outputs.tag }}-${{ matrix.target }}"
Compress-Archive -Path "dist\$package" -DestinationPath "dist\$package.zip" -Force
- name: Upload release archive
uses: actions/upload-artifact@v6
with:
name: ${{ env.BIN_NAME }}-${{ needs.verify.outputs.tag }}-${{ matrix.target }}
path: dist/${{ env.BIN_NAME }}-${{ needs.verify.outputs.tag }}-${{ matrix.target }}.${{ matrix.archive }}
if-no-files-found: error
skill:
name: Package CLI skill
needs:
- verify
- build
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v6
- name: Download Linux x64 release archive
uses: actions/download-artifact@v7
with:
name: ${{ env.BIN_NAME }}-${{ needs.verify.outputs.tag }}-x86_64-unknown-linux-gnu
path: skill-artifacts/linux-x86_64
- name: Download Windows x64 release archive
uses: actions/download-artifact@v7
with:
name: ${{ env.BIN_NAME }}-${{ needs.verify.outputs.tag }}-x86_64-pc-windows-msvc
path: skill-artifacts/windows-x86_64
- name: Validate CLI skill version inputs
shell: bash
run: |
set -euo pipefail
manifest_version="$(cargo metadata --no-deps --format-version 1 \
| python3 -c 'import json,sys; print(json.load(sys.stdin)["packages"][0]["version"])')"
if [[ "$manifest_version" != "${{ needs.verify.outputs.version }}" ]]; then
echo "Skill release version must follow Cargo.toml: $manifest_version != ${{ needs.verify.outputs.version }}" >&2
exit 1
fi
- name: Create skill archive and verify CLI skill Linux asset glibc compatibility
shell: bash
run: |
set -euo pipefail
package="${BIN_NAME}-cli-skill-${{ needs.verify.outputs.tag }}"
linux_archive="skill-artifacts/linux-x86_64/${BIN_NAME}-${{ needs.verify.outputs.tag }}-x86_64-unknown-linux-gnu.tar.gz"
windows_archive="skill-artifacts/windows-x86_64/${BIN_NAME}-${{ needs.verify.outputs.tag }}-x86_64-pc-windows-msvc.zip"
mkdir -p "dist/$package"
cp -R skills/relay-knowledge-cli/. "dist/$package/"
test -f "dist/$package/README.md"
python3 tools/release/update_skill_metadata_version.py \
"dist/$package/SKILL.md" "${{ needs.verify.outputs.version }}"
mkdir -p "dist/$package/assets/linux-x86_64" "dist/$package/assets/windows-x86_64"
tar -xzf "$linux_archive" -C skill-artifacts/linux-x86_64
cp "skill-artifacts/linux-x86_64/${BIN_NAME}-${{ needs.verify.outputs.tag }}-x86_64-unknown-linux-gnu/${BIN_NAME}" \
"dist/$package/assets/linux-x86_64/${BIN_NAME}"
python3 - "$windows_archive" "skill-artifacts/windows-x86_64" <<'PY'
import sys
import zipfile
with zipfile.ZipFile(sys.argv[1]) as archive:
archive.extractall(sys.argv[2])
PY
cp "skill-artifacts/windows-x86_64/${BIN_NAME}-${{ needs.verify.outputs.tag }}-x86_64-pc-windows-msvc/${BIN_NAME}.exe" \
"dist/$package/assets/windows-x86_64/${BIN_NAME}.exe"
chmod 0755 "dist/$package/assets/linux-x86_64/${BIN_NAME}"
chmod 0755 "dist/$package/assets/windows-x86_64/${BIN_NAME}.exe"
test -x "dist/$package/assets/linux-x86_64/${BIN_NAME}"
test -f "dist/$package/assets/windows-x86_64/${BIN_NAME}.exe"
python3 tools/release/check_linux_glibc_compat.py \
--max 2.31 \
"dist/$package/assets/linux-x86_64/${BIN_NAME}"
python3 tools/release/update_skill_metadata_version.py --check \
"dist/$package/SKILL.md" "${{ needs.verify.outputs.version }}"
tar -C dist -czf "dist/$package.tar.gz" "$package"
tar -tzf "dist/$package.tar.gz" | grep -Fx "$package/README.md" >/dev/null
- name: Upload skill archive
uses: actions/upload-artifact@v6
with:
name: ${{ env.BIN_NAME }}-cli-skill-${{ needs.verify.outputs.tag }}
path: dist/${{ env.BIN_NAME }}-cli-skill-${{ needs.verify.outputs.tag }}.tar.gz
if-no-files-found: error
publish-crate:
name: Publish crate
if: github.event_name == 'push' && needs.verify.outputs.publish_crate == 'true'
needs:
- verify
- build
runs-on: ubuntu-latest
permissions:
contents: read
steps:
- name: Checkout repository
uses: actions/checkout@v6
- name: Install Rust toolchain
run: rustup toolchain install stable --profile minimal
- name: Publish crate to crates.io
env:
CARGO_REGISTRY_TOKEN: ${{ secrets.CARGO_REGISTRY_TOKEN }}
shell: bash
run: |
set -euo pipefail
version="${{ needs.verify.outputs.version }}"
published="$(python3 - "$version" <<'PY'
import json
import sys
import urllib.error
import urllib.request
version = sys.argv[1]
try:
with urllib.request.urlopen("https://crates.io/api/v1/crates/relay-knowledge", timeout=30) as response:
payload = json.load(response)
except urllib.error.HTTPError as error:
if error.code == 404:
print(False)
raise SystemExit(0)
raise
print(any(item["num"] == version for item in payload["versions"]))
PY
)"
if [[ "$published" == "True" ]]; then
echo "relay-knowledge $version is already published on crates.io; skipping."
exit 0
fi
cargo publish
release:
name: Publish GitHub release
if: always() && github.event_name == 'push' && needs.verify.result == 'success' && needs.build.result == 'success' && needs.skill.result == 'success' && (needs.publish-crate.result == 'success' || needs.publish-crate.result == 'skipped')
needs:
- verify
- build
- skill
- publish-crate
runs-on: ubuntu-latest
permissions:
contents: write
id-token: write
attestations: write
steps:
- name: Checkout repository
uses: actions/checkout@v6
- name: Download release archives
uses: actions/download-artifact@v7
with:
path: dist
merge-multiple: true
- name: Generate checksums
shell: bash
run: |
set -euo pipefail
cd dist
sha256sum * > checksums.txt
cat checksums.txt
- name: Generate release notes
shell: bash
run: |
set -euo pipefail
cat > release-notes.md <<'NOTES'
## Install
Download the archive for your platform from this release, verify it with `checksums.txt`, and place the `relay-knowledge` binary on your PATH.
Rust users can install from crates.io after the crate is published:
```bash
cargo install relay-knowledge
```
## CLI Skill
This release includes `relay-knowledge-cli-skill-${{ needs.verify.outputs.tag }}.tar.gz`, a ClawHub-compatible skill that teaches LLM agents to use the `relay-knowledge` CLI for local graph and code-repository workflows. The skill package includes root-level `README.md` guidance plus Linux x64 and Windows x64 binaries under `assets/`; agents prefer the matching bundled asset for the current operating system, CPU, and active command runner when `version --format json` succeeds, and use PATH only as a fallback or when explicitly requested. Windows `.exe` asset examples stay in PowerShell or cmd.exe instructions, not bash/POSIX command blocks. It is intentionally separate from MCP protocol access.
Registry maintainers can publish the same generated skill layout with:
```bash
clawhub publish skills/relay-knowledge-cli --slug relay-knowledge-cli --name "Relay Knowledge CLI" --version ${{ needs.verify.outputs.version }}
```
## Verification
```bash
sha256sum -c checksums.txt
gh attestation verify <artifact> -R coolplayagent/relay-knowledge
relay-knowledge --version
relay-knowledge status --format json
relay-knowledge service doctor --format json
```
## Platform Artifacts
- Linux x64: `x86_64-unknown-linux-gnu` built on the glibc 2.31 baseline
- Linux ARM64: `aarch64-unknown-linux-gnu` built on the glibc 2.31 baseline
- macOS Intel: `x86_64-apple-darwin`
- macOS Apple Silicon: `aarch64-apple-darwin`
- Windows x64: `x86_64-pc-windows-msvc`
- Windows ARM64: `aarch64-pc-windows-msvc`
Windows ARM64 is validated as a cross-built release artifact in this workflow. Native Windows ARM64 smoke tests require a future ARM64 Windows runner.
NOTES
- name: Attest release archives
uses: actions/attest@v4
with:
subject-checksums: dist/checksums.txt
- name: Create GitHub release
env:
GH_TOKEN: ${{ github.token }}
shell: bash
run: |
set -euo pipefail
tag="${{ needs.verify.outputs.tag }}"
prerelease_args=()
if [[ "${{ needs.verify.outputs.prerelease }}" == "true" ]]; then
prerelease_args+=(--prerelease)
fi
if gh release view "$tag" >/dev/null 2>&1; then
gh release edit "$tag" \
--title "$tag" \
--notes-file release-notes.md \
"${prerelease_args[@]}"
gh release upload "$tag" --clobber dist/*
exit 0
fi
gh release create "$tag" \
--title "$tag" \
--notes-file release-notes.md \
"${prerelease_args[@]}" \
dist/*
publish-skill:
name: Publish ClawHub skill
if: github.event_name == 'push'
needs:
- verify
- release
runs-on: ubuntu-latest
permissions:
contents: read
steps:
- name: Checkout repository
uses: actions/checkout@v6
- name: Check ClawHub token
id: clawhub-token
env:
CLAWHUB_TOKEN: ${{ secrets.CLAWHUB_TOKEN }}
shell: bash
run: |
set -euo pipefail
if [[ -z "${CLAWHUB_TOKEN:-}" ]]; then
echo "publish=false" >> "$GITHUB_OUTPUT"
echo "CLAWHUB_TOKEN is not configured; skipping ClawHub publish."
exit 0
fi
echo "publish=true" >> "$GITHUB_OUTPUT"
- name: Set up Node
if: steps.clawhub-token.outputs.publish == 'true'
uses: actions/setup-node@v4
with:
node-version: '24'
- name: Download Linux x64 release archive
if: steps.clawhub-token.outputs.publish == 'true'
uses: actions/download-artifact@v7
with:
name: ${{ env.BIN_NAME }}-${{ needs.verify.outputs.tag }}-x86_64-unknown-linux-gnu
path: skill-artifacts/linux-x86_64
- name: Download Windows x64 release archive
if: steps.clawhub-token.outputs.publish == 'true'
uses: actions/download-artifact@v7
with:
name: ${{ env.BIN_NAME }}-${{ needs.verify.outputs.tag }}-x86_64-pc-windows-msvc
path: skill-artifacts/windows-x86_64
- name: Install CLI skill asset binaries and verify Linux glibc compatibility
if: steps.clawhub-token.outputs.publish == 'true'
shell: bash
run: |
set -euo pipefail
linux_archive="skill-artifacts/linux-x86_64/${BIN_NAME}-${{ needs.verify.outputs.tag }}-x86_64-unknown-linux-gnu.tar.gz"
windows_archive="skill-artifacts/windows-x86_64/${BIN_NAME}-${{ needs.verify.outputs.tag }}-x86_64-pc-windows-msvc.zip"
mkdir -p "skills/relay-knowledge-cli/assets/linux-x86_64" "skills/relay-knowledge-cli/assets/windows-x86_64"
tar -xzf "$linux_archive" -C skill-artifacts/linux-x86_64
cp "skill-artifacts/linux-x86_64/${BIN_NAME}-${{ needs.verify.outputs.tag }}-x86_64-unknown-linux-gnu/${BIN_NAME}" \
"skills/relay-knowledge-cli/assets/linux-x86_64/${BIN_NAME}"
python3 - "$windows_archive" "skill-artifacts/windows-x86_64" <<'PY'
import sys
import zipfile
with zipfile.ZipFile(sys.argv[1]) as archive:
archive.extractall(sys.argv[2])
PY
cp "skill-artifacts/windows-x86_64/${BIN_NAME}-${{ needs.verify.outputs.tag }}-x86_64-pc-windows-msvc/${BIN_NAME}.exe" \
"skills/relay-knowledge-cli/assets/windows-x86_64/${BIN_NAME}.exe"
chmod 0755 "skills/relay-knowledge-cli/assets/linux-x86_64/${BIN_NAME}"
chmod 0755 "skills/relay-knowledge-cli/assets/windows-x86_64/${BIN_NAME}.exe"
test -x "skills/relay-knowledge-cli/assets/linux-x86_64/${BIN_NAME}"
test -f "skills/relay-knowledge-cli/assets/windows-x86_64/${BIN_NAME}.exe"
python3 tools/release/check_linux_glibc_compat.py \
--max 2.31 \
"skills/relay-knowledge-cli/assets/linux-x86_64/${BIN_NAME}"
python3 tools/release/update_skill_metadata_version.py \
skills/relay-knowledge-cli/SKILL.md "${{ needs.verify.outputs.version }}"
python3 tools/release/update_skill_metadata_version.py --check \
skills/relay-knowledge-cli/SKILL.md "${{ needs.verify.outputs.version }}"
- name: Install ClawHub CLI
if: steps.clawhub-token.outputs.publish == 'true'
run: npm install -g clawhub
- name: Publish skill
if: steps.clawhub-token.outputs.publish == 'true'
env:
CLAWHUB_TOKEN: ${{ secrets.CLAWHUB_TOKEN }}
shell: bash
run: |
set -euo pipefail
tags="latest"
if [[ "${{ needs.verify.outputs.prerelease }}" == "true" ]]; then
tags="prerelease"
fi
clawhub --no-input login --token "$CLAWHUB_TOKEN"
clawhub publish skills/relay-knowledge-cli \
--slug relay-knowledge-cli \
--name "Relay Knowledge CLI" \
--version "${{ needs.verify.outputs.version }}" \
--changelog "relay-knowledge ${{ needs.verify.outputs.tag }}" \
--tags "$tags"