fix(ci): remove --locked from builds (path deps version mismatch with… #5
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 | |
| # Linux x86_64 — static musl (Alpine/Docker) | |
| - os: ubuntu-latest | |
| target: x86_64-unknown-linux-musl | |
| use-cross: true | |
| archive: tar.gz | |
| features: headless | |
| # Linux ARM64 | |
| - os: ubuntu-latest | |
| target: aarch64-unknown-linux-gnu | |
| use-cross: true | |
| archive: tar.gz | |
| features: headless | |
| # 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: 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 }} | |
| - 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 | |
| env: | |
| # ORT: musl has no prebuilt ONNX Runtime → compile from source inside cross container | |
| # GNU: use prebuilt binaries (faster) | |
| ORT_STRATEGY: ${{ contains(matrix.target, 'musl') && 'compile' || 'download' }} | |
| LIBGIT2_SYS_USE_PKG_CONFIG: "0" | |
| LIBGIT2_STATIC: "1" | |
| ZSTD_SYS_USE_PKG_CONFIG: "0" | |
| 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 (cargo) | |
| if: matrix.use-cross == false | |
| 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: 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") | |
| # 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}" | |
| }) | |
| 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: 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 (Cloudflare Pages) | |
| uses: cloudflare/pages-action@v1 | |
| with: | |
| apiToken: ${{ secrets.CF_API_TOKEN }} | |
| accountId: ${{ secrets.CF_ACCOUNT_ID }} | |
| projectName: halcon-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 |