vetto 0.2.6

Daemon-less sandbox + security layer for AI coding agents (Landlock/Seatbelt, TUI statusline, post-session audit reports)
Documentation
name: release

on:
  push:
    tags: ["v*"]
  workflow_dispatch:
    inputs:
      tag:
        description: Existing v* tag to release
        required: true
        type: string

permissions:
  contents: read

env:
  CARGO_TERM_COLOR: always
  GH_REPO: ${{ github.repository }}
  RELEASE_TAG: ${{ inputs.tag || github.ref_name }}

jobs:
  prepare-release:
    name: prepare draft GitHub release
    runs-on: ubuntu-latest
    permissions:
      contents: write
    steps:
      - name: Create or verify the draft release
        env:
          GH_TOKEN: ${{ github.token }}
        shell: bash
        run: |
          set -euo pipefail
          [[ "$RELEASE_TAG" == v* ]]

          if gh release view "$RELEASE_TAG" >/dev/null 2>&1; then
            is_draft="$(gh release view "$RELEASE_TAG" --json isDraft --jq '.isDraft')"
            if [[ "$is_draft" != "true" ]]; then
              echo "release $RELEASE_TAG is already published" >&2
              exit 1
            fi
          else
            prerelease_args=()
            if [[ "$RELEASE_TAG" == *-* ]]; then
              prerelease_args+=(--prerelease)
            fi
            gh release create "$RELEASE_TAG" \
              --draft \
              "${prerelease_args[@]}" \
              --generate-notes \
              --title "vetto ${RELEASE_TAG#v}" \
              --verify-tag
          fi

  build:
    name: build ${{ matrix.target }}
    needs: prepare-release
    runs-on: ${{ matrix.os }}
    permissions:
      contents: write
    strategy:
      fail-fast: false
      matrix:
        include:
          - os: ubuntu-22.04
            target: x86_64-unknown-linux-gnu
            artifact: vetto-linux-x86_64
            archive: vetto-linux-x86_64.tar.gz
            binary: vetto
          - os: ubuntu-22.04
            target: aarch64-unknown-linux-gnu
            artifact: vetto-linux-aarch64
            archive: vetto-linux-aarch64.tar.gz
            binary: vetto
          - os: macos-15
            target: aarch64-apple-darwin
            artifact: vetto-macos-aarch64
            archive: vetto-macos-aarch64.tar.gz
            binary: vetto
          - os: macos-15-intel
            target: x86_64-apple-darwin
            artifact: vetto-macos-x86_64
            archive: vetto-macos-x86_64.tar.gz
            binary: vetto
          - os: windows-latest
            target: x86_64-pc-windows-msvc
            artifact: vetto-windows-x86_64
            archive: vetto-windows-x86_64.zip
            binary: vetto.exe

    steps:
      - uses: actions/checkout@v5
        with:
          ref: ${{ env.RELEASE_TAG }}

      - uses: dtolnay/rust-toolchain@stable
        with:
          targets: ${{ matrix.target }}

      - uses: Swatinem/rust-cache@v2

      - name: Verify tag and manifest versions
        shell: bash
        run: |
          set -euo pipefail
          expected_version="${RELEASE_TAG#v}"
          cargo_version="$(sed -n 's/^version = "\([^"]*\)"$/\1/p' Cargo.toml | head -n 1)"
          npm_version="$(sed -n 's/^[[:space:]]*"version": "\([^"]*\)",$/\1/p' npm/package.json | head -n 1)"
          test -n "$cargo_version"
          test "$expected_version" = "$cargo_version"
          test "$expected_version" = "$npm_version"

      - name: Install the ARM64 Linux linker
        if: matrix.target == 'aarch64-unknown-linux-gnu'
        run: sudo apt-get update && sudo apt-get install --yes gcc-aarch64-linux-gnu

      - name: Release build
        env:
          CARGO_TARGET_AARCH64_UNKNOWN_LINUX_GNU_LINKER: aarch64-linux-gnu-gcc
        run: cargo build --locked --release --target ${{ matrix.target }}

      - name: Sign Windows binary (Authenticode)
        if: runner.os == 'Windows'
        env:
          SIGNING_CERT_PFX: ${{ secrets.SIGNING_CERT_PFX }}
          SIGNING_CERT_PASSWORD: ${{ secrets.SIGNING_CERT_PASSWORD }}
        shell: pwsh
        run: |
          ./packaging/windows/sign.ps1 -TargetPath "target/${{ matrix.target }}/release/${{ matrix.binary }}"

      - name: Build macOS installer package (.pkg)
        if: runner.os == 'macOS'
        env:
          DEVELOPER_ID_APPLICATION: ${{ secrets.DEVELOPER_ID_APPLICATION }}
          DEVELOPER_ID_INSTALLER: ${{ secrets.DEVELOPER_ID_INSTALLER }}
          APPLE_ID: ${{ secrets.APPLE_ID }}
          APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }}
          APP_SPECIFIC_PASSWORD: ${{ secrets.APP_SPECIFIC_PASSWORD }}
        shell: bash
        run: |
          ./packaging/macos/build_pkg.sh "${RELEASE_TAG#v}" "${{ matrix.target }}" || true

      - name: Package release archive
        if: runner.os != 'Windows'
        shell: bash
        run: |
          set -euo pipefail
          binary="target/${{ matrix.target }}/release/${{ matrix.binary }}"
          test -f "$binary"
          mkdir -p release-assets
          tar -czf "release-assets/${{ matrix.archive }}" \
            -C "$(dirname "$binary")" "$(basename "$binary")"
          if command -v sha256sum >/dev/null 2>&1; then
            (cd release-assets && sha256sum "${{ matrix.archive }}" \
              > "${{ matrix.archive }}.sha256")
          else
            (cd release-assets && shasum -a 256 "${{ matrix.archive }}" \
              > "${{ matrix.archive }}.sha256")
          fi

      - name: Package Windows release archive
        if: runner.os == 'Windows'
        shell: pwsh
        run: |
          $binary = "target/${{ matrix.target }}/release/${{ matrix.binary }}"
          if (-not (Test-Path -LiteralPath $binary -PathType Leaf)) {
            throw "release binary not found: $binary"
          }
          New-Item -ItemType Directory -Force -Path release-assets | Out-Null
          $archive = "release-assets/${{ matrix.archive }}"
          Compress-Archive -LiteralPath $binary -DestinationPath $archive
          $hash = (Get-FileHash -Algorithm SHA256 -LiteralPath $archive).Hash.ToLowerInvariant()
          "$hash  ${{ matrix.archive }}" | Set-Content -Encoding ascii "$archive.sha256"

      - name: Upload assets directly to the draft release
        env:
          GH_TOKEN: ${{ github.token }}
        shell: bash
        run: gh release upload "$RELEASE_TAG" release-assets/* --clobber

  npm-package:
    name: assemble npm package
    needs: build
    runs-on: ubuntu-latest
    permissions:
      contents: write
    steps:
      - uses: actions/checkout@v5
        with:
          ref: ${{ env.RELEASE_TAG }}

      - name: Download native release archives
        env:
          GH_TOKEN: ${{ github.token }}
        shell: bash
        run: |
          set -euo pipefail
          gh release download "$RELEASE_TAG" \
            --pattern 'vetto-linux-*' \
            --pattern 'vetto-macos-*' \
            --pattern 'vetto-windows-*'

      - uses: actions/setup-node@v4
        with:
          node-version: 24

      - name: Assemble and validate npm package
        shell: bash
        run: |
          set -euo pipefail

          expected_version="${RELEASE_TAG#v}"
          package_version="$(node -p "require('./npm/package.json').version")"
          test "$expected_version" = "$package_version"

          rm -rf npm-stage npm-dist
          mkdir -p npm-stage npm-dist
          cp -R npm/. npm-stage/
          rm -f npm-stage/.npmignore npm-stage/.gitignore
          rm -rf npm-stage/native

          for checksum in vetto-*.sha256; do
            sha256sum -c "$checksum"
          done

          extract_binary() {
            local archive="$1"
            local target="$2"
            mkdir -p "npm-stage/native/$target"
            tar -xzf "$archive.tar.gz" -C "npm-stage/native/$target"
          }

          extract_binary vetto-linux-x86_64 linux-x64
          extract_binary vetto-linux-aarch64 linux-arm64
          extract_binary vetto-macos-x86_64 darwin-x64
          extract_binary vetto-macos-aarch64 darwin-arm64
          mkdir -p npm-stage/native/win32-x64
          unzip -q vetto-windows-x86_64.zip -d npm-stage/native/win32-x64

          chmod +x npm-stage/bin/vetto.js
          chmod +x npm-stage/native/linux-x64/vetto
          chmod +x npm-stage/native/linux-arm64/vetto
          chmod +x npm-stage/native/darwin-x64/vetto
          chmod +x npm-stage/native/darwin-arm64/vetto

          npm test --prefix npm-stage
          npm pack ./npm-stage --pack-destination ./npm-dist
          package_tarball="$(find npm-dist -maxdepth 1 -type f -name '*.tgz' -print -quit)"
          test -n "$package_tarball"
          mv "$package_tarball" npm-dist/vetto-npm-${package_version}.tgz
          if command -v sha256sum >/dev/null 2>&1; then
            (cd npm-dist && sha256sum "vetto-npm-${package_version}.tgz" \
              > "vetto-npm-${package_version}.tgz.sha256")
          else
            (cd npm-dist && shasum -a 256 "vetto-npm-${package_version}.tgz" \
              > "vetto-npm-${package_version}.tgz.sha256")
          fi

          package_listing="$(mktemp)"
          tar -tzf npm-dist/vetto-npm-${package_version}.tgz > "$package_listing"
          for path in \
            package/bin/vetto.js \
            package/native/linux-x64/vetto \
            package/native/linux-arm64/vetto \
            package/native/darwin-x64/vetto \
            package/native/darwin-arm64/vetto \
            package/native/win32-x64/vetto.exe; do
            grep -Fqx "$path" "$package_listing"
          done

      - name: Upload the npm package directly to the draft release
        env:
          GH_TOKEN: ${{ github.token }}
        shell: bash
        run: gh release upload "$RELEASE_TAG" npm-dist/* --clobber

  npm-smoke:
    name: npm smoke (${{ matrix.os }})
    needs: npm-package
    runs-on: ${{ matrix.os }}
    permissions:
      contents: write
    strategy:
      fail-fast: false
      matrix:
        os: [ubuntu-latest, macos-15, macos-15-intel, windows-latest]
    steps:
      - name: Download npm package artifact
        env:
          GH_TOKEN: ${{ github.token }}
        shell: bash
        run: |
          set -euo pipefail
          mkdir npm-dist
          gh release download "$RELEASE_TAG" \
            --dir npm-dist \
            --pattern 'vetto-npm-*.tgz'

      - uses: actions/setup-node@v4
        with:
          node-version: 24

      - name: Install and launch the packaged CLI
        shell: bash
        run: |
          set -euo pipefail
          mkdir smoke && cd smoke
          npm init --yes >/dev/null 2>&1
          package_tarball="$(find ../npm-dist -maxdepth 1 -type f -name 'vetto-npm-*.tgz' -print -quit)"
          test -n "$package_tarball"
          npm install --ignore-scripts "$package_tarball"
          node <<'NODE'
          const { spawnSync } = require("child_process");
          const windows = process.platform === "win32";
          const command = windows
            ? (process.env.ComSpec || "cmd.exe")
            : "node_modules/.bin/vetto";
          const args = windows
            ? ["/d", "/c", "node_modules\\.bin\\vetto.cmd", "--version"]
            : ["--version"];
          const result = spawnSync(command, args, {
            stdio: "inherit",
          });
          if (result.error) throw result.error;
          process.exit(result.status === null ? 1 : result.status);
          NODE

  github-release:
    name: verify draft GitHub release
    needs: [build, npm-package, npm-smoke]
    runs-on: ubuntu-latest
    permissions:
      contents: write
    steps:
      - name: Verify the complete draft asset set
        env:
          GH_TOKEN: ${{ github.token }}
        shell: bash
        run: |
          set -euo pipefail
          test "$(gh release view "$RELEASE_TAG" --json isDraft --jq '.isDraft')" = true

          asset_names="$(gh release view "$RELEASE_TAG" --json assets --jq '.assets[].name')"
          for asset in \
            vetto-linux-x86_64.tar.gz \
            vetto-linux-x86_64.tar.gz.sha256 \
            vetto-linux-aarch64.tar.gz \
            vetto-linux-aarch64.tar.gz.sha256 \
            vetto-macos-x86_64.tar.gz \
            vetto-macos-x86_64.tar.gz.sha256 \
            vetto-macos-aarch64.tar.gz \
            vetto-macos-aarch64.tar.gz.sha256 \
            vetto-windows-x86_64.zip \
            vetto-windows-x86_64.zip.sha256 \
            "vetto-npm-${RELEASE_TAG#v}.tgz" \
            "vetto-npm-${RELEASE_TAG#v}.tgz.sha256"; do
            grep -Fqx "$asset" <<<"$asset_names"
          done