Skip to content

chore(release): bump version to 0.3.2 #18

chore(release): bump version to 0.3.2

chore(release): bump version to 0.3.2 #18

Workflow file for this run

name: Release
on:
push:
tags:
- 'v[0-9]+.[0-9]+.[0-9]+'
- 'v[0-9]+.[0-9]+.[0-9]+-alpha.[0-9]+'
- 'v[0-9]+.[0-9]+.[0-9]+-beta.[0-9]+'
- 'v[0-9]+.[0-9]+.[0-9]+-rc.[0-9]+'
workflow_dispatch:
inputs:
tag:
description: 'Tag to release (e.g., v0.2.0)'
required: true
type: string
env:
CARGO_TERM_COLOR: always
RUST_BACKTRACE: 1
CARGO_INCREMENTAL: 0
permissions:
contents: write
id-token: write # Required for cosign OIDC + SLSA provenance
packages: write
attestations: write
concurrency:
group: release-${{ github.ref }}
cancel-in-progress: false
jobs:
# ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
# Pre-flight: validate tag matches Cargo.toml version
# ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
preflight:
name: Pre-flight checks
runs-on: ubuntu-latest
outputs:
version: ${{ steps.version.outputs.version }}
version_bare: ${{ steps.version.outputs.version_bare }}
is_prerelease: ${{ steps.version.outputs.is_prerelease }}
steps:
- uses: actions/checkout@v4
- name: Resolve version
id: version
run: |
if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then
TAG="${{ github.event.inputs.tag }}"
else
TAG="${GITHUB_REF#refs/tags/}"
fi
BARE="${TAG#v}"
echo "version=${TAG}" >> $GITHUB_OUTPUT
echo "version_bare=${BARE}" >> $GITHUB_OUTPUT
if echo "$TAG" | grep -qE '(alpha|beta|rc)'; then
echo "is_prerelease=true" >> $GITHUB_OUTPUT
else
echo "is_prerelease=false" >> $GITHUB_OUTPUT
fi
echo "Tag: ${TAG} (bare: ${BARE})"
- name: Validate tag == Cargo.toml version
run: |
CARGO_VERSION="$(grep '^version' Cargo.toml | head -1 | sed 's/.*"\(.*\)".*/\1/')"
TAG_VERSION="${{ steps.version.outputs.version_bare }}"
echo "Cargo.toml version: ${CARGO_VERSION}"
echo "Tag version: ${TAG_VERSION}"
if [ "$CARGO_VERSION" != "$TAG_VERSION" ]; then
echo "ERROR: Cargo.toml version (${CARGO_VERSION}) does not match tag (${TAG_VERSION})"
exit 1
fi
echo "✓ Version match confirmed"
# ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
# Build matrix — 6 targets
# ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
build:
name: Build ${{ matrix.target }}
needs: preflight
runs-on: ${{ matrix.os }}
strategy:
fail-fast: false
matrix:
include:
# Linux x86_64 — dynamic glibc (primary Linux artifact)
- os: ubuntu-latest
target: x86_64-unknown-linux-gnu
use-cross: true
archive: tar.gz
features: headless,vendored-openssl
# Linux x86_64 — static musl (Alpine/Docker)
- os: ubuntu-latest
target: x86_64-unknown-linux-musl
use-cross: true
archive: tar.gz
features: headless,vendored-openssl
# Linux ARM64 glibc
- os: ubuntu-latest
target: aarch64-unknown-linux-gnu
use-cross: true
archive: tar.gz
features: headless,vendored-openssl
# Linux ARM64 musl (for Alpine/Docker)
- os: ubuntu-latest
target: aarch64-unknown-linux-musl
use-cross: true
archive: tar.gz
features: vendored-openssl
# macOS — single runner builds BOTH targets (ARM64 native + Intel cross)
# macos-latest is ARM64 (M-series); x86_64 cross-compile is fully supported
- os: macos-latest
target: aarch64-apple-darwin
use-cross: false
archive: tar.gz
features: tui,vendored-openssl
extra-target: x86_64-apple-darwin
# Windows x86_64 — native runner, MSVC target (prebuilt ORT binaries available)
# vendored-openssl NOT used on Windows: system OpenSSL set via OPENSSL_DIR
- os: windows-latest
target: x86_64-pc-windows-msvc
use-cross: false
archive: zip
features: headless
steps:
- uses: actions/checkout@v4
- name: Setup Zuclubit (momoto-ui) path
shell: bash
run: |
# workspace Cargo.toml references ../Zuclubit/momoto-ui/momoto/crates/momoto-{core,metrics,intelligence}
# These are optional path deps (color-science feature) but cargo metadata resolves them always.
PARENT_DIR="$(cd .. && pwd)"
ZUCLUBIT_DIR="${PARENT_DIR}/Zuclubit"
MOMOTO_DIR="${ZUCLUBIT_DIR}/momoto-ui/momoto/crates"
# Try authenticated clone (succeeds if MOMOTO_TOKEN secret is configured)
CLONE_TOKEN="${{ secrets.MOMOTO_TOKEN }}"
if [ -n "$CLONE_TOKEN" ]; then
git clone --depth=1 \
"https://x-access-token:${CLONE_TOKEN}@github.com/cuervo-ai/momoto-ui.git" \
"${ZUCLUBIT_DIR}/momoto-ui" 2>/dev/null && echo "✓ momoto-ui cloned" && exit 0 || true
fi
# Fallback: minimal cargo-metadata stubs (color-science disabled → stubs never compiled)
echo "INFO: creating momoto stub crates for cargo metadata resolution"
for CRATE in momoto-core momoto-metrics momoto-intelligence; do
mkdir -p "${MOMOTO_DIR}/${CRATE}/src"
printf '[package]\nname = "%s"\nversion = "0.1.0"\nedition = "2021"\npublish = false\n' \
"${CRATE}" > "${MOMOTO_DIR}/${CRATE}/Cargo.toml"
echo "// stub" > "${MOMOTO_DIR}/${CRATE}/src/lib.rs"
done
echo "✓ momoto stubs created"
- name: Install Rust toolchain
uses: dtolnay/rust-toolchain@stable
with:
targets: >-
${{ matrix.target }}
${{ matrix.extra-target || '' }}
- name: Install cross
if: matrix.use-cross == true
run: cargo install cross --git https://github.com/cross-rs/cross --locked
- name: Install cargo-zigbuild + zig (Windows cross from Linux)
if: matrix.use-zigbuild == true && runner.os == 'Linux'
run: |
pip install ziglang --quiet
cargo install cargo-zigbuild --locked
# mingw-w64 for gnu target linking
sudo apt-get install -y gcc-mingw-w64-x86-64 --no-install-recommends -q
- uses: Swatinem/rust-cache@v2
with:
key: ${{ matrix.target }}
- name: Set build metadata
shell: bash
run: |
echo "HALCON_GIT_HASH=$(git rev-parse --short HEAD)" >> $GITHUB_ENV
echo "HALCON_BUILD_DATE=$(date -u +%Y-%m-%d)" >> $GITHUB_ENV
echo "HALCON_TARGET=${{ matrix.target }}" >> $GITHUB_ENV
- name: Build (cross — Linux targets)
if: matrix.use-cross == true
shell: bash
env:
ORT_STRATEGY: ${{ contains(matrix.target, 'musl') && 'compile' || 'download' }}
LIBGIT2_SYS_USE_PKG_CONFIG: "0"
LIBGIT2_STATIC: "1"
# Allow pkg-config for cross builds (required for libdbus-sys on musl containers)
PKG_CONFIG_ALLOW_CROSS: "1"
run: |
FEATURES="${{ matrix.features }}"
if [ -n "$FEATURES" ]; then
cross build --release --target ${{ matrix.target }} \
--no-default-features --features "$FEATURES" -p halcon-cli
else
cross build --release --target ${{ matrix.target }} \
--no-default-features -p halcon-cli
fi
- name: Build (zigbuild — Windows cross from Linux)
if: matrix.use-zigbuild == true && runner.os == 'Linux'
shell: bash
env:
ORT_STRATEGY: compile
LIBGIT2_SYS_USE_PKG_CONFIG: "0"
LIBGIT2_STATIC: "1"
run: |
FEATURES="${{ matrix.features }}"
if [ -n "$FEATURES" ]; then
cargo zigbuild --release --target ${{ matrix.target }} \
--no-default-features --features "$FEATURES" -p halcon-cli
else
cargo zigbuild --release --target ${{ matrix.target }} \
--no-default-features -p halcon-cli
fi
- name: Setup OpenSSL (Windows)
if: runner.os == 'Windows'
shell: pwsh
run: |
# Use pre-installed OpenSSL on the runner (avoids compiling from source)
$candidates = @(
"C:\Program Files\OpenSSL-Win64",
"C:\Program Files\OpenSSL",
"$env:VCPKG_ROOT\installed\x64-windows-static"
)
$found = $null
foreach ($dir in $candidates) {
if (Test-Path "$dir\include\openssl\ssl.h") {
$found = $dir; break
}
}
if (-not $found) {
Write-Host "OpenSSL not found in standard locations, installing via choco..."
choco install openssl -y --no-progress
$found = "C:\Program Files\OpenSSL-Win64"
}
Write-Host "Using OpenSSL at: $found"
"OPENSSL_DIR=$found" | Out-File -FilePath $env:GITHUB_ENV -Encoding utf8 -Append
"OPENSSL_STATIC=1" | Out-File -FilePath $env:GITHUB_ENV -Encoding utf8 -Append
- name: Build (cargo — Windows native MSVC)
if: runner.os == 'Windows'
shell: bash
env:
ORT_STRATEGY: download
LIBGIT2_SYS_USE_PKG_CONFIG: "0"
LIBGIT2_STATIC: "1"
run: |
FEATURES="${{ matrix.features }}"
if [ -n "$FEATURES" ]; then
cargo build --release --target ${{ matrix.target }} \
--no-default-features --features "$FEATURES" -p halcon-cli
else
cargo build --release --target ${{ matrix.target }} \
--no-default-features -p halcon-cli
fi
- name: Build (cargo — macOS, builds both targets)
if: matrix.use-cross == false && matrix.use-zigbuild != true
shell: bash
env:
ORT_STRATEGY: download
LIBGIT2_SYS_USE_PKG_CONFIG: "0"
LIBGIT2_STATIC: "1"
run: |
FEATURES="${{ matrix.features }}"
# Primary target
if [ -n "$FEATURES" ]; then
cargo build --release --target ${{ matrix.target }} \
--no-default-features --features "$FEATURES" -p halcon-cli
else
cargo build --release --target ${{ matrix.target }} \
--no-default-features -p halcon-cli
fi
# Extra target (macOS: build x86_64 on the same ARM64 runner)
EXTRA="${{ matrix.extra-target }}"
if [ -n "$EXTRA" ]; then
if [ -n "$FEATURES" ]; then
cargo build --release --target "$EXTRA" \
--no-default-features --features "$FEATURES" -p halcon-cli
else
cargo build --release --target "$EXTRA" \
--no-default-features -p halcon-cli
fi
fi
- name: Package (Unix — tar.gz, includes extra-target if present)
if: matrix.archive == 'tar.gz'
shell: bash
run: |
VERSION="${{ needs.preflight.outputs.version_bare }}"
package_target() {
local T="$1"
local ARTIFACT="halcon-${VERSION}-${T}"
local BIN="target/${T}/release/halcon"
mkdir -p "artifacts/${ARTIFACT}"
cp "$BIN" "artifacts/${ARTIFACT}/halcon"
cp README.md "artifacts/${ARTIFACT}/" 2>/dev/null || true
cp LICENSE "artifacts/${ARTIFACT}/" 2>/dev/null || true
tar czf "${ARTIFACT}.tar.gz" -C artifacts "${ARTIFACT}"
if command -v sha256sum &>/dev/null; then
sha256sum "${ARTIFACT}.tar.gz" | awk '{print $1}' > "${ARTIFACT}.tar.gz.sha256"
else
shasum -a 256 "${ARTIFACT}.tar.gz" | awk '{print $1}' > "${ARTIFACT}.tar.gz.sha256"
fi
echo "Packaged: ${ARTIFACT}.tar.gz"
}
package_target "${{ matrix.target }}"
EXTRA="${{ matrix.extra-target }}"
if [ -n "$EXTRA" ]; then
package_target "$EXTRA"
fi
# Set primary artifact env vars for the upload step
TARGET="${{ matrix.target }}"
ARTIFACT="halcon-${VERSION}-${TARGET}"
echo "ARTIFACT_ARCHIVE=${ARTIFACT}.tar.gz" >> $GITHUB_ENV
echo "ARTIFACT_SHA256=${ARTIFACT}.tar.gz.sha256" >> $GITHUB_ENV
# Signal extra artifact to upload step
if [ -n "$EXTRA" ]; then
echo "EXTRA_ARTIFACT=halcon-${VERSION}-${EXTRA}.tar.gz" >> $GITHUB_ENV
echo "EXTRA_SHA256=halcon-${VERSION}-${EXTRA}.tar.gz.sha256" >> $GITHUB_ENV
fi
- name: Package (Windows zip — native MSVC)
if: matrix.archive == 'zip'
shell: bash
run: |
VERSION="${{ needs.preflight.outputs.version_bare }}"
TARGET="${{ matrix.target }}"
ARTIFACT="halcon-${VERSION}-${TARGET}"
BIN="target/${TARGET}/release/halcon.exe"
mkdir -p "artifacts/${ARTIFACT}"
cp "$BIN" "artifacts/${ARTIFACT}/halcon.exe"
cp README.md "artifacts/${ARTIFACT}/" 2>/dev/null || true
cp LICENSE "artifacts/${ARTIFACT}/" 2>/dev/null || true
cd artifacts && 7z a "../${ARTIFACT}.zip" "${ARTIFACT}"
cd ..
if command -v sha256sum &>/dev/null; then
sha256sum "${ARTIFACT}.zip" | awk '{print $1}' > "${ARTIFACT}.zip.sha256"
else
# certutil output varies by Windows version; strip header/footer lines and whitespace
certutil -hashfile "${ARTIFACT}.zip" SHA256 \
| grep -v "^CertUtil" | grep -v "^SHA256" | grep -v "^$" \
| head -1 | tr -d ' \r\n' > "${ARTIFACT}.zip.sha256"
fi
# Verify the hash file is non-empty (guards against silent failures)
[ -s "${ARTIFACT}.zip.sha256" ] || { echo "ERROR: SHA256 hash is empty for ${ARTIFACT}.zip" >&2; exit 1; }
echo "ARTIFACT_ARCHIVE=${ARTIFACT}.zip" >> $GITHUB_ENV
echo "ARTIFACT_SHA256=${ARTIFACT}.zip.sha256" >> $GITHUB_ENV
- name: Upload artifact (primary)
uses: actions/upload-artifact@v4
with:
name: ${{ env.ARTIFACT_ARCHIVE }}
path: |
${{ env.ARTIFACT_ARCHIVE }}
${{ env.ARTIFACT_SHA256 }}
retention-days: 7
- name: Upload artifact (extra macOS target)
if: env.EXTRA_ARTIFACT != ''
uses: actions/upload-artifact@v4
with:
name: ${{ env.EXTRA_ARTIFACT }}
path: |
${{ env.EXTRA_ARTIFACT }}
${{ env.EXTRA_SHA256 }}
retention-days: 7
# ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
# Sign, SBOM, SLSA provenance, publish
# ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
publish:
name: Sign, SBOM & Publish
needs: [preflight, build]
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Download all artifacts
uses: actions/download-artifact@v4
with:
path: dist/
merge-multiple: true
- name: Install tooling
run: |
# cosign
COSIGN_VER="v2.4.1"
curl -sSfL "https://github.com/sigstore/cosign/releases/download/${COSIGN_VER}/cosign-linux-amd64" \
-o /usr/local/bin/cosign
chmod +x /usr/local/bin/cosign
# syft (SBOM)
curl -sSfL https://raw.githubusercontent.com/anchore/syft/main/install.sh | sh -s -- -b /usr/local/bin
# Python (for manifest generation)
python3 --version
- name: Generate checksums.txt
run: |
cd dist/
> checksums.txt
for f in *.tar.gz *.zip; do
[ -f "$f" ] || continue
sha256sum "$f" >> checksums.txt
done
echo "==> checksums.txt:"
cat checksums.txt
- name: Sign archives with cosign (keyless)
run: |
cd dist/
for f in *.tar.gz *.zip; do
[ -f "$f" ] || continue
echo "Signing: $f"
cosign sign-blob \
--yes \
--output-signature "${f}.sig" \
--output-certificate "${f}.pem" \
"$f"
done
# Sign checksums file
cosign sign-blob \
--yes \
--output-signature "checksums.txt.sig" \
--output-certificate "checksums.txt.pem" \
checksums.txt
- name: Generate SBOM (syft)
run: |
syft dir:. \
--output spdx-json=dist/halcon-${{ needs.preflight.outputs.version_bare }}.sbom.spdx.json \
--output cyclonedx-json=dist/halcon-${{ needs.preflight.outputs.version_bare }}.sbom.cyclonedx.json
- name: Install Rust (for cargo metadata)
uses: dtolnay/rust-toolchain@stable
- name: Generate manifest.json
run: |
python3 - <<'EOF'
import json, os, hashlib, subprocess
from pathlib import Path
from datetime import datetime, timezone
version = os.environ.get("VERSION", "")
dist = Path("dist")
# Map Rust target triple → (os, arch, ext)
TARGET_META = {
"aarch64-apple-darwin": ("macos", "aarch64", "tar.gz"),
"x86_64-apple-darwin": ("macos", "x86_64", "tar.gz"),
"x86_64-unknown-linux-gnu": ("linux", "x86_64", "tar.gz"),
"x86_64-unknown-linux-musl": ("linux", "x86_64", "tar.gz"),
"aarch64-unknown-linux-gnu": ("linux", "aarch64", "tar.gz"),
"aarch64-unknown-linux-musl": ("linux", "aarch64", "tar.gz"),
"x86_64-pc-windows-msvc": ("windows", "x86_64", "zip"),
}
artifacts = []
for f in sorted(dist.glob("*.tar.gz")) + sorted(dist.glob("*.zip")):
if any(ext in f.name for ext in [".sig", ".pem", ".sbom"]):
continue
sha256 = hashlib.sha256(f.read_bytes()).hexdigest()
size = f.stat().st_size
stem = f.stem.replace(".tar", "")
target = stem.replace(f"halcon-{version}-", "")
meta = TARGET_META.get(target, ("unknown", "unknown", f.suffix.lstrip(".")))
artifacts.append({
"name": f.name,
"target": target,
"os": meta[0],
"arch": meta[1],
"ext": meta[2],
"sha256": sha256,
"size": size,
"url": f"https://releases.cli.cuervo.cloud/latest/{f.name}"
})
# Determine channel from tag: alpha/beta/rc → beta; else stable
is_prerelease = os.environ.get("IS_PRERELEASE", "false").lower() == "true"
channel = "beta" if is_prerelease else "stable"
# Extract release notes from GITHUB_RELEASE_NOTES env var (set by GH action)
# or from CHANGELOG.md if it exists — first ## vX.Y.Z section
release_notes = os.environ.get("RELEASE_NOTES", "")
if not release_notes:
changelog = Path("CHANGELOG.md")
if changelog.exists():
import re
text = changelog.read_text()
# Match from `## v{version}` to the next `## v` header
pattern = rf"(?s)##\s+v?{re.escape(version)}[^\n]*\n(.*?)(?=\n##\s+v|\Z)"
m = re.search(pattern, text)
if m:
release_notes = m.group(1).strip()[:2000] # cap at 2000 chars
# Minimum OS versions (informational, enforced by installer)
MIN_OS = {
"macos": "12.0", # Monterey — required for arm64 native
"linux": "glibc-2.17",
"windows": "10",
}
manifest = {
"version": version,
"channel": channel,
"published_at": datetime.now(timezone.utc).isoformat(),
"artifacts": artifacts,
"checksums_url": "https://releases.cli.cuervo.cloud/latest/checksums.txt",
"sbom_url": f"https://releases.cli.cuervo.cloud/latest/halcon-{version}.sbom.spdx.json",
"github_url": f"https://github.com/cuervo-ai/halcon-cli/releases/tag/v{version}",
"release_notes": release_notes or None,
"min_os": MIN_OS,
}
# Strip None values for a cleaner manifest
manifest = {k: v for k, v in manifest.items() if v is not None}
out = dist / "manifest.json"
out.write_text(json.dumps(manifest, indent=2))
print(json.dumps(manifest, indent=2))
EOF
env:
VERSION: ${{ needs.preflight.outputs.version_bare }}
IS_PRERELEASE: ${{ needs.preflight.outputs.is_prerelease }}
- name: Upload to Cloudflare R2 (versioned)
uses: cloudflare/wrangler-action@v3
with:
apiToken: ${{ secrets.CF_API_TOKEN }}
accountId: ${{ secrets.CF_ACCOUNT_ID }}
command: r2 object put cuervo-releases/${{ needs.preflight.outputs.version }}/
continue-on-error: true # Fallback: upload via aws cli below
- name: Upload to R2 (aws s3 compat)
continue-on-error: true # Don't fail release if R2 credentials not configured
run: |
pip install awscli --quiet
VERSION="${{ needs.preflight.outputs.version }}"
BARE="${{ needs.preflight.outputs.version_bare }}"
# Upload versioned copy
aws s3 sync dist/ "s3://cuervo-releases/${VERSION}/" \
--endpoint-url "https://${{ secrets.CF_ACCOUNT_ID }}.r2.cloudflarestorage.com" \
--checksum-algorithm SHA256 \
--no-progress
# Update latest/
aws s3 sync dist/ "s3://cuervo-releases/latest/" \
--endpoint-url "https://${{ secrets.CF_ACCOUNT_ID }}.r2.cloudflarestorage.com" \
--checksum-algorithm SHA256 \
--no-progress \
--delete
env:
AWS_ACCESS_KEY_ID: ${{ secrets.CF_R2_ACCESS_KEY_ID }}
AWS_SECRET_ACCESS_KEY: ${{ secrets.CF_R2_SECRET_ACCESS_KEY }}
AWS_DEFAULT_REGION: auto
- name: Create GitHub Release
uses: softprops/action-gh-release@v2
with:
tag_name: ${{ needs.preflight.outputs.version }}
name: Halcon CLI ${{ needs.preflight.outputs.version }}
prerelease: ${{ needs.preflight.outputs.is_prerelease == 'true' }}
generate_release_notes: true
body: |
## Halcon CLI ${{ needs.preflight.outputs.version }}
### Quick Install
**Linux / macOS:**
```bash
curl -sSfL https://halcon.cuervo.cloud/install.sh | sh
```
**Windows (PowerShell):**
```powershell
iwr -useb https://halcon.cuervo.cloud/install.ps1 | iex
```
**Homebrew:**
```bash
brew tap cuervo-ai/tap && brew install halcon
```
**Self-update (if already installed):**
```bash
halcon update
```
---
### What's New in v0.3.0
- **Declarative Sub-Agent Registry** — define specialized agents in `.halcon/agents/*.md`, discovered across session/project/user scopes
- **Lifecycle Hooks** — pre/post tool execution hooks via `.halcon/hooks/` for audit, telemetry, and custom workflows
- **Semantic Memory Vector Store** — TF-IDF based local vector search over MEMORY.md for long-term context retrieval
- **Halcon as MCP Server** — expose Halcon's agent capabilities to Claude Code and other MCP clients (`halcon mcp serve`)
- **MCP OAuth 2.1** — PKCE-based OAuth flow for authenticated MCP server connections
- **Compliance Audit Export** — export session audit logs as JSONL, CSV, or PDF with HMAC-SHA256 integrity chains (`halcon audit export`)
- **HybridIntentClassifier** — 6-layer intent classification with embedding, LLM deliberation, and adaptive learning
- **VS Code Extension** — JSON-RPC subprocess bridge for editor integration
- **Agent Scheduler** — cron-based scheduled agent tasks (`halcon schedule add`)
- **Cenzontle SSO** — OAuth 2.1 PKCE enterprise SSO integration
- **Runtime Events** — structured `RuntimeEvent` bus for IDE observability and replay
---
### Verify
All artifacts are signed with [cosign](https://sigstore.dev) keyless signing.
See [checksums.txt](https://releases.cli.cuervo.cloud/${{ needs.preflight.outputs.version }}/checksums.txt) for SHA-256 hashes.
files: |
dist/*.tar.gz
dist/*.zip
dist/*.sha256
dist/*.sig
dist/*.pem
dist/checksums.txt
dist/manifest.json
dist/*.sbom.*.json
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Build website
run: |
cd website
npm ci --prefer-offline 2>/dev/null || npm ci
npm run build
continue-on-error: true # Don't fail release if website build fails
- name: Deploy website to Cloudflare Pages (halcon-website)
uses: cloudflare/wrangler-action@v3
with:
apiToken: ${{ secrets.CF_API_TOKEN }}
accountId: ${{ secrets.CF_ACCOUNT_ID }}
command: pages deploy website/dist --project-name=halcon-website --commit-dirty=true
continue-on-error: true # Don't fail release if pages deploy fails
- name: Deploy releases Worker (halcon-releases)
uses: cloudflare/wrangler-action@v3
with:
apiToken: ${{ secrets.CF_API_TOKEN }}
accountId: ${{ secrets.CF_ACCOUNT_ID }}
workingDirectory: workers/releases
command: deploy
continue-on-error: true # Don't fail release if worker deploy fails
# ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
# Smoke tests post-release
# ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
smoke-test:
name: Smoke tests
needs: [preflight, publish]
runs-on: ubuntu-latest
steps:
- name: Verify manifest.json accessible
run: |
sleep 10 # allow CDN propagation
MANIFEST=$(curl -sSfL https://releases.cli.cuervo.cloud/latest/manifest.json 2>/dev/null || echo '{}')
VERSION=$(echo "$MANIFEST" | python3 -c "import sys,json; d=json.load(sys.stdin); print(d.get('version',''))" 2>/dev/null || echo '')
EXPECTED="${{ needs.preflight.outputs.version_bare }}"
if [ "$VERSION" != "$EXPECTED" ]; then
echo "WARN: manifest version '${VERSION}' != expected '${EXPECTED}'"
else
echo "✓ manifest.json: version=${VERSION}"
fi
continue-on-error: true
- name: Verify checksums.txt accessible
run: |
HTTP_CODE=$(curl -sSo /dev/null -w '%{http_code}' https://releases.cli.cuervo.cloud/latest/checksums.txt 2>/dev/null || echo '000')
if [ "$HTTP_CODE" = "200" ]; then
echo "✓ checksums.txt: HTTP 200"
else
echo "WARN: checksums.txt returned HTTP ${HTTP_CODE}"
fi
continue-on-error: true
- name: Test install script syntax and reachability
run: |
# Syntax-check local copy (always available)
bash -n website/public/install.sh && echo "✓ install.sh syntax OK"
# Verify install script is reachable over HTTPS (allow CDN propagation)
sleep 15
HTTP_CODE=$(curl -sSo /dev/null -w '%{http_code}' https://halcon.cuervo.cloud/install.sh 2>/dev/null || echo '000')
if [ "$HTTP_CODE" = "200" ]; then
echo "✓ halcon.cuervo.cloud/install.sh: HTTP 200"
else
echo "WARN: halcon.cuervo.cloud/install.sh returned HTTP ${HTTP_CODE}"
fi
# Verify releases Worker health
HTTP_WORKER=$(curl -sSo /dev/null -w '%{http_code}' https://releases.cli.cuervo.cloud/health 2>/dev/null || echo '000')
if [ "$HTTP_WORKER" = "200" ]; then
echo "✓ releases.cli.cuervo.cloud/health: HTTP 200"
else
echo "WARN: releases Worker returned HTTP ${HTTP_WORKER}"
fi
continue-on-error: true