chore(release): bump version to 0.3.2 #18
Workflow file for this run
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 | |
| 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 |