Release KafClaw #72
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
| 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[@]}" |