veloci 0.3.3

Veloci Redactor: redact secrets and PII from text and structured files, with stable numbered redactions
Documentation
name: Publish extra packages (deb, scoop, plugin)

# Called by cargo-dist's release.yml as a post-announce job (see
# dist-workspace.toml), after the GitHub Release exists with all platform
# archives. It can't trigger on `release: published` because that release is
# created with GITHUB_TOKEN, and GITHUB_TOKEN events don't start workflows.
#
# workflow_dispatch backfills an existing release by tag.
on:
  workflow_call:
    inputs:
      plan:
        required: true
        type: string
  workflow_dispatch:
    inputs:
      tag:
        description: Release tag to build packages for (e.g. v0.3.0)
        required: true
        type: string

jobs:
  meta:
    runs-on: ubuntu-latest
    outputs:
      tag: ${{ steps.meta.outputs.tag }}
      prerelease: ${{ steps.meta.outputs.prerelease }}
    steps:
      - id: meta
        env:
          PLAN: ${{ inputs.plan }}
          TAG_INPUT: ${{ inputs.tag }}
          GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
        run: |
          set -euo pipefail
          if [ -n "$TAG_INPUT" ]; then
            tag="$TAG_INPUT"
            prerelease=$(gh release view "$tag" --repo "${{ github.repository }}" --json isPrerelease -q .isPrerelease)
          else
            tag=$(jq -r .announcement_tag <<<"$PLAN")
            prerelease=$(jq -r .announcement_is_prerelease <<<"$PLAN")
          fi
          echo "tag=$tag" >> "$GITHUB_OUTPUT"
          echo "prerelease=$prerelease" >> "$GITHUB_OUTPUT"

  deb:
    needs: meta
    if: ${{ needs.meta.outputs.prerelease != 'true' }}
    runs-on: ubuntu-latest
    permissions:
      contents: write
    strategy:
      fail-fast: false
      matrix:
        include:
          - target: x86_64-unknown-linux-gnu
            arch: amd64
          - target: aarch64-unknown-linux-gnu
            arch: arm64
    steps:
      - uses: actions/checkout@v4

      - name: Download release archive
        env:
          GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
        run: |
          mkdir dl
          gh release download "${{ needs.meta.outputs.tag }}" \
            --repo "${{ github.repository }}" \
            --pattern "veloci-cli-${{ matrix.target }}.tar.xz" \
            --dir dl

      - name: Extract binary
        run: |
          mkdir extracted
          # cargo-dist wraps archive contents in a veloci-cli-<target>/ directory.
          tar -xJf "dl/veloci-cli-${{ matrix.target }}.tar.xz" -C extracted --strip-components=1

      - name: Build .deb
        run: |
          TAG="${{ needs.meta.outputs.tag }}"
          VERSION="${TAG#v}"
          ./scripts/build-deb.sh veloci "$VERSION" "${{ matrix.arch }}" \
            extracted/veloci \
            "Command-line interface for Veloci Redactor: redact secrets and PII from text and structured files" \
            "https://github.com/phayes/velociredactor" \
            extracted

      - name: Upload .deb to release
        env:
          GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
        run: |
          gh release upload "${{ needs.meta.outputs.tag }}" \
            --repo "${{ github.repository }}" \
            veloci_*_${{ matrix.arch }}.deb

  scoop:
    needs: meta
    if: ${{ needs.meta.outputs.prerelease != 'true' }}
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - name: Download windows archive + checksum
        env:
          GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
        run: |
          mkdir dl
          gh release download "${{ needs.meta.outputs.tag }}" \
            --repo "${{ github.repository }}" \
            --pattern "veloci-cli-x86_64-pc-windows-msvc.zip*" \
            --dir dl

      - name: Generate manifest
        run: |
          TAG="${{ needs.meta.outputs.tag }}"
          VERSION="${TAG#v}"
          SHA=$(awk '{print $1}' dl/veloci-cli-x86_64-pc-windows-msvc.zip.sha256)
          ./scripts/gen-scoop-manifest.sh veloci "$VERSION" \
            "Command-line interface for Veloci Redactor: redact secrets and PII from text and structured files" \
            "https://github.com/phayes/velociredactor" \
            "MIT" \
            velociredactor "$TAG" \
            veloci-cli-x86_64-pc-windows-msvc.zip veloci "$SHA"

      - name: Publish to scoop-bucket
        env:
          GH_TOKEN: ${{ secrets.SCOOP_BUCKET_TOKEN }}
        run: |
          git clone "https://x-access-token:${GH_TOKEN}@github.com/phayes/scoop-bucket.git" bucket-repo
          cp veloci.json bucket-repo/bucket/veloci.json
          cd bucket-repo
          git config user.email "patrick.d.hayes@gmail.com"
          git config user.name "Patrick Hayes"
          git add bucket/veloci.json
          if git diff --cached --quiet; then
            echo "manifest unchanged, nothing to push"
            exit 0
          fi
          git commit -m "veloci ${{ needs.meta.outputs.tag }}"
          git push

  # cargo-dist's target list (aarch64-apple-darwin, x86_64-apple-darwin,
  # aarch64-unknown-linux-gnu, x86_64-unknown-linux-gnu, x86_64-pc-windows-msvc)
  # doesn't include Windows ARM64, so the plugin's binary for that platform is
  # built here rather than lifted from a cargo-dist release archive.
  plugin-winarm64:
    needs: meta
    if: ${{ needs.meta.outputs.prerelease != 'true' }}
    runs-on: windows-11-arm
    steps:
      - uses: actions/checkout@v4
      - name: Install Rust
        uses: dtolnay/rust-toolchain@stable
        with:
          targets: aarch64-pc-windows-msvc
      - name: Build aarch64-pc-windows-msvc (plugin only)
        run: cargo build --release --locked --package veloci-cli --target aarch64-pc-windows-msvc
        env:
          RUSTFLAGS: -C strip=symbols
      - uses: actions/upload-artifact@v4
        with:
          name: winarm64-binary
          path: target/aarch64-pc-windows-msvc/release/veloci.exe

  plugin:
    needs: [meta, plugin-winarm64]
    if: ${{ needs.meta.outputs.prerelease != 'true' }}
    runs-on: ubuntu-latest
    permissions:
      contents: write
    steps:
      - uses: actions/checkout@v4

      - uses: actions/download-artifact@v4
        with:
          name: winarm64-binary
          path: winarm64

      - name: Download release archives
        env:
          GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
        run: |
          set -euo pipefail
          mkdir dl
          for asset in \
            veloci-cli-aarch64-apple-darwin.tar.xz \
            veloci-cli-x86_64-apple-darwin.tar.xz \
            veloci-cli-aarch64-unknown-linux-gnu.tar.xz \
            veloci-cli-x86_64-unknown-linux-gnu.tar.xz \
            veloci-cli-x86_64-pc-windows-msvc.zip; do
            gh release download "${{ needs.meta.outputs.tag }}" \
              --repo "${{ github.repository }}" \
              --pattern "$asset" \
              --dir dl
          done

      - name: Assemble libexec binaries
        shell: bash
        run: |
          set -euo pipefail
          mkdir -p plugin/libexec

          for target in aarch64-apple-darwin x86_64-apple-darwin aarch64-unknown-linux-gnu x86_64-unknown-linux-gnu; do
            extract="extracted-${target}"
            mkdir -p "$extract"
            tar -xJf "dl/veloci-cli-${target}.tar.xz" -C "$extract" --strip-components=1
            cp "$extract/veloci" "plugin/libexec/veloci-${target}"
          done

          # Unlike the tarballs, cargo-dist's Windows zip has no wrapping directory.
          extract="extracted-x86_64-pc-windows-msvc"
          mkdir -p "$extract"
          unzip -q "dl/veloci-cli-x86_64-pc-windows-msvc.zip" -d "$extract"
          cp "$extract/veloci.exe" "plugin/libexec/veloci-x86_64-pc-windows-msvc.exe"

          cp winarm64/veloci.exe "plugin/libexec/veloci-aarch64-pc-windows-msvc.exe"

      - name: Assemble plugin
        shell: bash
        run: |
          set -euo pipefail
          version="${{ needs.meta.outputs.tag }}"
          version="${version#v}"

          # Artifacts lose the executable bit; the launcher's is kept by git.
          chmod +x plugin/bin/veloci plugin/libexec/*
          for target in \
            aarch64-apple-darwin x86_64-apple-darwin \
            aarch64-unknown-linux-gnu x86_64-unknown-linux-gnu \
            aarch64-pc-windows-msvc.exe x86_64-pc-windows-msvc.exe; do
            test -f "plugin/libexec/veloci-${target}" || { echo "missing veloci-${target}" >&2; exit 1; }
          done

          # Claude Code detects updates by plugin.json's version.
          jq --arg v "$version" '.version = $v' plugin/.claude-plugin/plugin.json > plugin.json.tmp
          mv plugin.json.tmp plugin/.claude-plugin/plugin.json

          plugin/bin/veloci --version

      - name: Validate plugin
        run: npx --yes @anthropic-ai/claude-code plugin validate ./plugin

      - name: Package plugin
        run: |
          set -euo pipefail
          mkdir -p dist
          (cd plugin && zip -r -X ../dist/veloci-plugin.zip .)
          (cd dist && sha256sum veloci-plugin.zip > veloci-plugin.zip.sha256)

      - uses: actions/upload-artifact@v4
        with:
          name: veloci-plugin
          path: dist/

  # Windows machines may have no bash, so the plugin has to work there through
  # bin/veloci.cmd alone: both the hook's command and the skills' `veloci`.
  plugin-windows:
    needs: plugin
    strategy:
      matrix:
        runner: [windows-latest, windows-11-arm]
    runs-on: ${{ matrix.runner }}
    steps:
      - uses: actions/download-artifact@v4
        with:
          name: veloci-plugin
          path: dist

      - name: Unpack plugin and write a hook input
        shell: pwsh
        run: |
          Expand-Archive dist/veloci-plugin.zip -DestinationPath plugin
          New-Item -ItemType Directory proj | Out-Null
          Set-Content proj/.env 'TOKEN=1'
          @{
            tool_name  = 'Read'
            tool_input = @{ file_path = '.env' }
            cwd        = (Resolve-Path proj).Path
          } | ConvertTo-Json -Compress | Set-Content hook-input.json

      - name: Run the launcher and hook from cmd, without bash
        shell: cmd
        run: |
          set "PATH=%SystemRoot%\System32;%SystemRoot%"
          where bash >nul 2>&1 && (echo bash is on PATH & exit /b 1)
          set "PATH=%GITHUB_WORKSPACE%\plugin\bin;%PATH%"
          call veloci --version || exit /b 1
          cd proj
          call veloci agent init --protect .env --enforce || exit /b 1
          rem The hook's command as hooks.json writes it: an extensionless path.
          cmd /d /s /c ""%GITHUB_WORKSPACE%\plugin/bin/veloci" agent hook" < ..\hook-input.json > ..\hook-output.json || exit /b 1

      - name: Check the hook denied the read, and run veloci from PowerShell
        shell: pwsh
        run: |
          $decision = (Get-Content hook-output.json | ConvertFrom-Json).hookSpecificOutput.permissionDecision
          if ($decision -ne 'deny') { Get-Content hook-output.json; throw "hook did not deny the read" }
          $env:PATH = "$env:GITHUB_WORKSPACE\plugin\bin;$env:SystemRoot\System32;$env:SystemRoot"
          veloci --version
          if ($LASTEXITCODE -ne 0) { throw "veloci exited $LASTEXITCODE" }

  plugin-publish:
    needs: [meta, plugin, plugin-windows]
    runs-on: ubuntu-latest
    permissions:
      contents: write
    steps:
      - uses: actions/download-artifact@v4
        with:
          name: veloci-plugin
          path: dist

      - name: Upload plugin to release
        env:
          GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
        run: |
          gh release upload "${{ needs.meta.outputs.tag }}" \
            --repo "${{ github.repository }}" \
            dist/veloci-plugin.zip dist/veloci-plugin.zip.sha256