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.0.7
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: 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/SKILL.md
test -f skills/relay-knowledge-cli/agents/openai.yaml
test -f skills/relay-knowledge-cli/references/cli-workflows.md
grep -q '^name: relay-knowledge-cli$' skills/relay-knowledge-cli/SKILL.md
python3 tools/release/update_skill_metadata_version.py --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 'newest semver' 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 '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
cross: false
- target: aarch64-unknown-linux-gnu
os: ubuntu-latest
archive: tar.gz
cross: true
- target: x86_64-apple-darwin
os: macos-15-intel
archive: tar.gz
cross: false
- target: aarch64-apple-darwin
os: macos-14
archive: tar.gz
cross: false
- target: x86_64-pc-windows-msvc
os: windows-latest
archive: zip
cross: false
msvc_arch: amd64
- target: aarch64-pc-windows-msvc
os: windows-latest
archive: zip
cross: false
msvc_arch: amd64_arm64
runs-on: ${{ matrix.os }}
steps:
- name: Checkout repository
uses: actions/checkout@v6
- name: Install Rust toolchain
run: rustup toolchain install stable --profile minimal --target ${{ matrix.target }}
- name: Set up Linux cross toolchain
if: matrix.cross == true
uses: taiki-e/setup-cross-toolchain-action@v1
with:
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
run: cargo build --release --locked --target ${{ matrix.target }}
- 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
- 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
- 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
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/"
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/update_skill_metadata_version.py --check \
"dist/$package/SKILL.md" "${{ needs.verify.outputs.version }}"
tar -C dist -czf "dist/$package.tar.gz" "$package"
- 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 Linux x64 and Windows x64 binaries under `assets/` and selects the newest semver version when both PATH and bundled binaries are available. 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`
- Linux ARM64: `aarch64-unknown-linux-gnu`
- 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
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/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"