Skip to content

feat: complete rename cuervo → halcon + installer audit fixes #1

feat: complete rename cuervo → halcon + installer audit fixes

feat: complete rename cuervo → halcon + installer audit fixes #1

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 — static musl (primary Linux artifact)
- os: ubuntu-latest
target: x86_64-unknown-linux-musl
use-cross: true
archive: tar.gz
features: tui
# Linux ARM64
- os: ubuntu-latest
target: aarch64-unknown-linux-gnu
use-cross: true
archive: tar.gz
features: ""
# macOS Intel
- os: macos-13
target: x86_64-apple-darwin
use-cross: false
archive: tar.gz
features: tui
# macOS Apple Silicon
- os: macos-latest
target: aarch64-apple-darwin
use-cross: false
archive: tar.gz
features: tui
# Windows x86_64
- os: windows-latest
target: x86_64-pc-windows-msvc
use-cross: false
archive: zip
features: ""
# Linux ARM64 musl (for Alpine/Docker)
- os: ubuntu-latest
target: aarch64-unknown-linux-musl
use-cross: true
archive: tar.gz
features: ""
steps:
- uses: actions/checkout@v4
- name: Install Rust toolchain
uses: dtolnay/rust-toolchain@stable
with:
targets: ${{ matrix.target }}
- name: Install cross
if: matrix.use-cross == true
run: cargo install cross --git https://github.com/cross-rs/cross --locked
- 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)
if: matrix.use-cross == true
shell: bash
run: |
FEATURES="${{ matrix.features }}"
if [ -n "$FEATURES" ]; then
cross build --release --target ${{ matrix.target }} --features "$FEATURES" --locked -p halcon-cli
else
cross build --release --target ${{ matrix.target }} --no-default-features --locked -p halcon-cli
fi
- name: Build (cargo)
if: matrix.use-cross == false
shell: bash
run: |
FEATURES="${{ matrix.features }}"
if [ -n "$FEATURES" ]; then
cargo build --release --target ${{ matrix.target }} --features "$FEATURES" --locked -p halcon-cli
else
cargo build --release --target ${{ matrix.target }} --no-default-features --locked -p halcon-cli
fi
- name: Package (Unix)
if: matrix.archive == 'tar.gz'
shell: bash
run: |
VERSION="${{ needs.preflight.outputs.version_bare }}"
TARGET="${{ matrix.target }}"
ARTIFACT="halcon-${VERSION}-${TARGET}"
BIN="target/${TARGET}/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}"
# SHA-256
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 "ARTIFACT_ARCHIVE=${ARTIFACT}.tar.gz" >> $GITHUB_ENV
echo "ARTIFACT_SHA256=${ARTIFACT}.tar.gz.sha256" >> $GITHUB_ENV
- name: Package (Windows)
if: matrix.archive == 'zip'
shell: pwsh
run: |
$Version = "${{ needs.preflight.outputs.version_bare }}"
$Target = "${{ matrix.target }}"
$Artifact = "halcon-${Version}-${Target}"
$Bin = "target/${Target}/release/halcon.exe"
New-Item -ItemType Directory -Path "artifacts/${Artifact}" -Force | Out-Null
Copy-Item $Bin "artifacts/${Artifact}/halcon.exe"
if (Test-Path "README.md") { Copy-Item "README.md" "artifacts/${Artifact}/" }
if (Test-Path "LICENSE") { Copy-Item "LICENSE" "artifacts/${Artifact}/" }
Compress-Archive -Path "artifacts/${Artifact}" -DestinationPath "${Artifact}.zip" -Force
$Hash = (Get-FileHash -Path "${Artifact}.zip" -Algorithm SHA256).Hash.ToLower()
$Hash | Out-File -FilePath "${Artifact}.zip.sha256" -Encoding ASCII -NoNewline
echo "ARTIFACT_ARCHIVE=${Artifact}.zip" | Out-File -FilePath $env:GITHUB_ENV -Append -Encoding utf8
echo "ARTIFACT_SHA256=${Artifact}.zip.sha256" | Out-File -FilePath $env:GITHUB_ENV -Append -Encoding utf8
- name: Upload artifact
uses: actions/upload-artifact@v4
with:
name: ${{ env.ARTIFACT_ARCHIVE }}
path: |
${{ env.ARTIFACT_ARCHIVE }}
${{ env.ARTIFACT_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")
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
target = f.stem.replace(f"halcon-{version}-", "").replace(".tar", "").replace(".zip", "")
artifacts.append({
"name": f.name,
"target": target,
"sha256": sha256,
"size": size,
"url": f"https://releases.cli.cuervo.cloud/latest/{f.name}"
})
manifest = {
"version": version,
"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",
}
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 }}
- 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)
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://cli.cuervo.cloud/install.sh | sh
```
**Windows (PowerShell):**
```powershell
iwr -useb https://cli.cuervo.cloud/install.ps1 | iex
```
**Homebrew:**
```bash
brew tap cuervo-ai/tap && brew install halcon
```
**Self-update (if already installed):**
```bash
halcon update
```
---
### 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: Deploy website (Cloudflare Pages)
uses: cloudflare/pages-action@v1
with:
apiToken: ${{ secrets.CF_API_TOKEN }}
accountId: ${{ secrets.CF_ACCOUNT_ID }}
projectName: cuervo-website
directory: website/dist
gitHubToken: ${{ secrets.GITHUB_TOKEN }}
continue-on-error: true # Don't fail release if pages 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
run: |
# Download and syntax-check install.sh
curl -sSfL https://cli.cuervo.cloud/install.sh -o /tmp/install.sh 2>/dev/null || true
if [ -f /tmp/install.sh ]; then
bash -n /tmp/install.sh && echo "✓ install.sh syntax OK" || echo "WARN: install.sh syntax error"
fi
continue-on-error: true