Skip to content

Release KafClaw

Release KafClaw #81

Workflow file for this run

name: Release KafClaw
on:
push:
tags:
- "v*"
- "edge-*"
schedule:
- cron: "0 2 * * *"
workflow_dispatch:
inputs:
channel:
description: "Release channel"
required: true
default: edge
type: choice
options:
- edge
- stable
version_tag:
description: "Optional tag override (e.g. v1.2.3 or edge-20260217)"
required: false
type: string
permissions:
contents: write
id-token: write
attestations: write
jobs:
prepare:
runs-on: ubuntu-latest
outputs:
should_release: ${{ steps.meta.outputs.should_release }}
channel: ${{ steps.meta.outputs.channel }}
version_tag: ${{ steps.meta.outputs.version_tag }}
prerelease: ${{ steps.meta.outputs.prerelease }}
release_name: ${{ steps.meta.outputs.release_name }}
steps:
- name: Checkout
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Determine release metadata
id: meta
shell: bash
run: |
set -euo pipefail
event="${GITHUB_EVENT_NAME}"
ref_tag="${GITHUB_REF#refs/tags/}"
input_channel="${{ github.event.inputs.channel || '' }}"
input_tag="${{ github.event.inputs.version_tag || '' }}"
should_release="true"
channel="edge"
prerelease="true"
version_tag=""
if [[ "$event" == "push" ]]; then
version_tag="$ref_tag"
if [[ "$ref_tag" == v* ]]; then
channel="stable"
prerelease="false"
else
channel="edge"
prerelease="true"
fi
elif [[ "$event" == "workflow_dispatch" ]]; then
if [[ "$input_channel" == "stable" ]]; then
channel="stable"
prerelease="false"
else
channel="edge"
prerelease="true"
fi
if [[ -n "$input_tag" ]]; then
version_tag="$input_tag"
elif [[ "$channel" == "stable" ]]; then
version_tag="v0.0.0-manual-${GITHUB_SHA:0:7}"
else
version_tag="edge-$(date -u +%Y%m%d)-${GITHUB_SHA:0:7}"
fi
else
latest_edge_tag="$(git for-each-ref --sort=-creatordate --format='%(refname:strip=2)' refs/tags/edge-* | head -n1)"
if [[ -n "$latest_edge_tag" ]]; then
latest_edge_sha="$(git rev-list -n 1 "$latest_edge_tag")"
if [[ "$latest_edge_sha" == "$GITHUB_SHA" ]]; then
should_release="false"
fi
fi
channel="edge"
prerelease="true"
version_tag="edge-$(date -u +%Y%m%d)-${GITHUB_SHA:0:7}"
fi
echo "should_release=$should_release" >> "$GITHUB_OUTPUT"
echo "channel=$channel" >> "$GITHUB_OUTPUT"
echo "version_tag=$version_tag" >> "$GITHUB_OUTPUT"
echo "prerelease=$prerelease" >> "$GITHUB_OUTPUT"
echo "release_name=KafClaw $version_tag ($channel)" >> "$GITHUB_OUTPUT"
# -------------------------------------------------------------------
# Job 1: Cross-compile Go CLI binaries (release artifacts)
# Includes: macOS, Windows, Linux amd64/arm64, Raspberry Pi armv7/armv6
# -------------------------------------------------------------------
build-go:
needs: prepare
if: needs.prepare.outputs.should_release == 'true'
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
include:
- goos: darwin
goarch: arm64
artifact_suffix: darwin-arm64
- goos: darwin
goarch: amd64
artifact_suffix: darwin-amd64
- goos: windows
goarch: amd64
artifact_suffix: windows-amd64
ext: .exe
- goos: windows
goarch: arm64
artifact_suffix: windows-arm64
ext: .exe
- goos: linux
goarch: amd64
artifact_suffix: linux-amd64
- goos: linux
goarch: arm64
artifact_suffix: linux-arm64
- goos: linux
goarch: arm
goarm: "7"
artifact_suffix: linux-armv7
- goos: linux
goarch: arm
goarm: "6"
artifact_suffix: linux-armv6
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Setup Go
uses: actions/setup-go@v5
with:
go-version: "1.24.13"
cache-dependency-path: go.sum
- name: Validate bundled skills artifacts
shell: bash
run: bash scripts/check_bundled_skills.sh
- name: Build hardened binary
shell: bash
env:
GOOS: ${{ matrix.goos }}
GOARCH: ${{ matrix.goarch }}
GOARM: ${{ matrix.goarm }}
CGO_ENABLED: "0"
run: |
set -euo pipefail
mkdir -p dist
BIN_NAME="kafclaw-${{ matrix.artifact_suffix }}${{ matrix.ext }}"
LDFLAGS="-s -w -buildid= -X github.com/KafClaw/KafClaw/internal/cli.version=${{ needs.prepare.outputs.version_tag }}"
go build -trimpath -ldflags "$LDFLAGS" -o "dist/${BIN_NAME}" ./cmd/kafclaw
if [[ "${{ matrix.goos }}" != "windows" ]]; then
chmod +x "dist/${BIN_NAME}"
fi
- name: Smoke test (where executable on runner)
shell: bash
run: |
set -euo pipefail
BIN_NAME="kafclaw-${{ matrix.artifact_suffix }}${{ matrix.ext }}"
if [[ "${{ matrix.goos }}" == "linux" && "${{ matrix.goarch }}" == "amd64" ]]; then
./dist/${BIN_NAME} --help >/dev/null
else
echo "Skipping smoke test for cross-arch target ${{ matrix.goos }}/${{ matrix.goarch }}"
fi
- name: Build Linux systemd install bundle
if: matrix.goos == 'linux'
shell: bash
run: |
set -euo pipefail
BIN_NAME="kafclaw-${{ matrix.artifact_suffix }}${{ matrix.ext }}"
BUNDLE_DIR="dist/kafclaw-${{ matrix.artifact_suffix }}-bundle"
mkdir -p "$BUNDLE_DIR/bin" "$BUNDLE_DIR/systemd" "$BUNDLE_DIR/env" "$BUNDLE_DIR/skills" "$BUNDLE_DIR/docs/skills"
cp "dist/${BIN_NAME}" "$BUNDLE_DIR/bin/kafclaw"
cp -R skills/. "$BUNDLE_DIR/skills/"
cp -R docs/skills/. "$BUNDLE_DIR/docs/skills/"
cat > "$BUNDLE_DIR/systemd/kafclaw.service" <<'UNIT'
[Unit]
Description=KafClaw Gateway
After=network-online.target
Wants=network-online.target
[Service]
Type=simple
User=kafclaw
Group=kafclaw
WorkingDirectory=/home/kafclaw
ExecStart=/usr/local/bin/kafclaw gateway
EnvironmentFile=/home/kafclaw/.config/kafclaw/env
Restart=always
RestartSec=5
[Install]
WantedBy=multi-user.target
UNIT
cat > "$BUNDLE_DIR/env/kafclaw.env" <<'ENVFILE'
# KafClaw runtime environment
MIKROBOT_GATEWAY_AUTH_TOKEN=
KAFCLAW_CONFIG=/home/kafclaw/.kafclaw/config.json
KAFCLAW_HOME=/home/kafclaw
HOME=/home/kafclaw
PATH=/home/kafclaw/.local/bin:/usr/local/bin:/usr/bin:/bin
ENVFILE
cat > "$BUNDLE_DIR/install.sh" <<'INSTALL'
#!/usr/bin/env bash
set -euo pipefail
install -m 0755 bin/kafclaw /usr/local/bin/kafclaw
install -d -m 0755 /etc/systemd/system
install -m 0644 systemd/kafclaw.service /etc/systemd/system/kafclaw.service
install -d -m 0700 /home/kafclaw/.config/kafclaw
if [[ ! -f /home/kafclaw/.config/kafclaw/env ]]; then
install -m 0600 env/kafclaw.env /home/kafclaw/.config/kafclaw/env
fi
systemctl daemon-reload
echo "Installed. Run: systemctl enable --now kafclaw"
INSTALL
chmod +x "$BUNDLE_DIR/install.sh"
tar -C dist -czf "dist/kafclaw-${{ matrix.artifact_suffix }}-bundle.tar.gz" "kafclaw-${{ matrix.artifact_suffix }}-bundle"
if [[ "${{ matrix.goarch }}" == "amd64" ]]; then
tar -czf "dist/kafclaw-skills-bundle.tar.gz" skills docs/skills
fi
- name: Upload artifact
uses: actions/upload-artifact@v4
with:
name: kafclaw-${{ matrix.artifact_suffix }}
path: dist/*
# -------------------------------------------------------------------
# Job 2: Platform smoke tests on native runners (where possible)
# -------------------------------------------------------------------
smoke-native:
needs: [prepare, build-go]
if: needs.prepare.outputs.should_release == 'true'
strategy:
fail-fast: false
matrix:
include:
- runner: ubuntu-latest
artifact: kafclaw-linux-amd64
binary: kafclaw-linux-amd64
- runner: windows-latest
artifact: kafclaw-windows-amd64
binary: kafclaw-windows-amd64.exe
- runner: macos-latest
artifact: kafclaw-darwin-arm64
binary: kafclaw-darwin-arm64
runs-on: ${{ matrix.runner }}
steps:
- name: Download artifact
uses: actions/download-artifact@v4
with:
name: ${{ matrix.artifact }}
path: dist
- name: Smoke test (Unix)
if: runner.os != 'Windows'
shell: bash
run: |
set -euo pipefail
chmod +x "dist/${{ matrix.binary }}"
"dist/${{ matrix.binary }}" --help >/dev/null
- name: Smoke test (Windows)
if: runner.os == 'Windows'
shell: pwsh
run: |
& "dist/${{ matrix.binary }}" --help | Out-Null
# -------------------------------------------------------------------
# Job 3: Electron packaging
# -------------------------------------------------------------------
build-electron:
needs: prepare
if: needs.prepare.outputs.should_release == 'true'
strategy:
fail-fast: false
matrix:
include:
- os: macos-latest
platform: mac
go_goos: darwin
go_goarch: arm64
- os: ubuntu-latest
platform: linux
go_goos: linux
go_goarch: amd64
runs-on: ${{ matrix.os }}
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Setup Go
uses: actions/setup-go@v5
with:
go-version: "1.24.13"
cache-dependency-path: go.sum
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: "20"
- name: Build Go binary for target platform
env:
GOOS: ${{ matrix.go_goos }}
GOARCH: ${{ matrix.go_goarch }}
CGO_ENABLED: "0"
run: |
go build -trimpath -ldflags "-s -w -buildid= -X github.com/KafClaw/KafClaw/internal/cli.version=${{ needs.prepare.outputs.version_tag }}" -o kafclaw ./cmd/kafclaw
chmod +x kafclaw
- name: Install Electron dependencies
working-directory: electron
run: npm ci
- name: Build Electron app
working-directory: electron
run: npm run build
- name: Package Electron app
working-directory: electron
run: npx electron-builder --${{ matrix.platform }} --publish never
- name: Upload Electron artifacts
uses: actions/upload-artifact@v4
with:
name: electron-${{ matrix.platform }}
path: |
electron/release/*.dmg
electron/release/*.AppImage
# -------------------------------------------------------------------
# Job 4: Publish release, checksums, signatures, SBOM, provenance
# -------------------------------------------------------------------
release:
needs: [prepare, build-go, smoke-native, build-electron]
if: needs.prepare.outputs.should_release == 'true'
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Download all artifacts
uses: actions/download-artifact@v4
with:
path: artifacts
- name: Prepare release files
shell: bash
run: |
set -euo pipefail
mkdir -p release_files
find artifacts -type f \( -name "kafclaw-*" -o -name "*.exe" -o -name "*.tar.gz" -o -name "*.dmg" -o -name "*.AppImage" \) -exec cp {} release_files/ \;
(cd release_files && sha256sum * > SHA256SUMS)
echo "Release files:"
ls -lh release_files/
- name: Install cosign
uses: sigstore/cosign-installer@v3
- name: Sign release artifacts (keyless)
shell: bash
run: |
set -euo pipefail
shopt -s nullglob
for f in release_files/*; do
base="$(basename "$f")"
if [[ "$base" == "SHA256SUMS" || "$base" == *.sig || "$base" == *.pem ]]; then
continue
fi
cosign sign-blob --yes \
--output-signature "${f}.sig" \
--output-certificate "${f}.pem" \
"$f"
done
- name: Generate SBOM (SPDX JSON)
uses: anchore/sbom-action@v0
with:
path: release_files
format: spdx-json
output-file: release_files/sbom.spdx.json
- name: Build provenance attestation
uses: actions/attest-build-provenance@v2
with:
subject-path: release_files/*
- name: Create GitHub Release
shell: bash
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
set -euo pipefail
args=(
release create
"${{ needs.prepare.outputs.version_tag }}"
--repo "${GITHUB_REPOSITORY}"
--target "${GITHUB_SHA}"
--title "${{ needs.prepare.outputs.release_name }}"
--generate-notes
)
if [[ "${{ needs.prepare.outputs.prerelease }}" == "true" ]]; then
args+=(--prerelease)
fi
shopt -s nullglob
files=(release_files/*)
if [[ ${#files[@]} -eq 0 ]]; then
echo "No release files found"
exit 1
fi
args+=("${files[@]}")
gh "${args[@]}"