tokensave 7.9.0

Code intelligence tool that builds a semantic knowledge graph from Rust, Go, Java, Scala, TypeScript, Python, C, C++, Kotlin, C#, Swift, and many more codebases
name: Release

on:
  release:
    types: [published]
  workflow_dispatch:

# Skip beta/prerelease tags — handled by release-beta.yml

permissions:
  contents: write

env:
  CARGO_TERM_COLOR: always

jobs:
  build:
    name: Build ${{ matrix.name }}
    if: github.event_name == 'workflow_dispatch' || !github.event.release.prerelease
    runs-on: ${{ matrix.runner }}
    strategy:
      fail-fast: false
      matrix:
        include:
          - name: aarch64-macos
            runner: macos-14
            target: aarch64-apple-darwin
            bottle_tag: arm64_sonoma
            archive: tar.gz
          - name: x86_64-linux
            runner: ubuntu-22.04
            target: x86_64-unknown-linux-gnu
            bottle_tag: x86_64_linux
            archive: tar.gz
          - name: aarch64-linux
            runner: ubuntu-22.04-arm
            target: aarch64-unknown-linux-gnu
            archive: tar.gz
          - name: x86_64-windows
            runner: windows-latest
            target: x86_64-pc-windows-msvc
            archive: zip
          - name: aarch64-windows
            runner: windows-11-arm
            target: aarch64-pc-windows-msvc
            archive: zip

    steps:
      - uses: actions/checkout@v5

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

      - name: Build release binary
        run: cargo build --release --target ${{ matrix.target }}

      - name: Get version
        id: version
        shell: bash
        run: |
          VERSION="${GITHUB_REF_NAME#v}"
          echo "version=$VERSION" >> "$GITHUB_OUTPUT"

      # --- Binary archive (all platforms) ---

      - name: Package binary (unix)
        if: matrix.archive == 'tar.gz'
        run: |
          cd target/${{ matrix.target }}/release
          tar czf ../../../tokensave-${{ github.ref_name }}-${{ matrix.name }}.tar.gz tokensave
          cd ../../..

      - name: Upload binary archive (unix)
        if: matrix.archive == 'tar.gz' && github.event_name == 'release'
        env:
          GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
        shell: bash
        run: gh release upload ${{ github.ref_name }} tokensave-${{ github.ref_name }}-${{ matrix.name }}.${{ matrix.archive }} --clobber

      # --- Windows: Authenticode signing via SignPath (build half) ---
      # Production signing uses a SignPath Foundation certificate, which requires manual
      # approval in the SignPath UI, so signing is split out into the sign-windows job.
      # Here we only upload the unsigned .exe (upload-artifact wraps the single file in a
      # ZIP) and record its artifact id so the signing job can submit it. This frees the
      # Windows runner immediately instead of blocking it while a human approves.

      - name: Upload unsigned binary for signing
        if: matrix.archive == 'zip' && github.event_name == 'release'
        id: upload-unsigned
        uses: actions/upload-artifact@v6
        with:
          # Per-target name: both Windows legs (x86_64 and aarch64) run this
          # step, so a shared name would collide and only one would survive.
          name: tokensave-windows-unsigned-${{ matrix.name }}
          path: target/${{ matrix.target }}/release/tokensave.exe
          if-no-files-found: error

      - name: Record unsigned artifact id
        if: matrix.archive == 'zip' && github.event_name == 'release'
        shell: bash
        run: echo "${{ steps.upload-unsigned.outputs.artifact-id }}" > unsigned-artifact-id.txt

      - name: Upload artifact id for signing job
        if: matrix.archive == 'zip' && github.event_name == 'release'
        uses: actions/upload-artifact@v6
        with:
          name: windows-unsigned-artifact-id-${{ matrix.name }}
          path: unsigned-artifact-id.txt
          if-no-files-found: error

      # --- Homebrew bottle (only for platforms with bottle_tag) ---

      - name: Package Homebrew bottle
        if: matrix.bottle_tag && github.event_name == 'release'
        run: |
          VERSION="${{ steps.version.outputs.version }}"
          BOTTLE_TAG="${{ matrix.bottle_tag }}"
          mkdir -p tokensave/${VERSION}/bin
          cp target/${{ matrix.target }}/release/tokensave tokensave/${VERSION}/bin/tokensave
          chmod +x tokensave/${VERSION}/bin/tokensave
          tar czf "tokensave-${VERSION}.${BOTTLE_TAG}.bottle.tar.gz" tokensave/

      - name: Upload bottle artifact
        if: matrix.bottle_tag && github.event_name == 'release'
        uses: actions/upload-artifact@v6
        with:
          name: bottle-${{ matrix.bottle_tag }}
          path: "tokensave-*.bottle.tar.gz"

  sign-windows:
    name: Sign Windows binary (${{ matrix.name }})
    if: github.event_name == 'release' && !github.event.release.prerelease && !cancelled()
    needs: build
    runs-on: ubuntu-latest
    permissions:
      contents: write   # upload the signed zip to the release
      actions: read     # let the SignPath action read the unsigned artifact
    strategy:
      # One signing request per Windows architecture. Independent so a slow
      # approval on one does not block the other; both must land for update-scoop.
      fail-fast: false
      matrix:
        name: [x86_64-windows, aarch64-windows]
    steps:
      - name: Download unsigned artifact id
        uses: actions/download-artifact@v7
        with:
          name: windows-unsigned-artifact-id-${{ matrix.name }}

      - name: Read unsigned artifact id
        id: meta
        run: echo "artifact-id=$(cat unsigned-artifact-id.txt)" >> "$GITHUB_OUTPUT"

      - name: Submit signing request to SignPath (waits for manual approval)
        # Production signing uses a SignPath Foundation certificate, which requires a
        # human to approve the request in the SignPath UI. This step submits the request
        # and blocks until it is approved + signed (or the timeout below elapses), then
        # downloads the signed .exe into ./signed. If approval is slow and this times
        # out, re-run this job — it submits a fresh request.
        uses: signpath/github-action-submit-signing-request@v2
        with:
          api-token: ${{ secrets.SIGNPATH_API_TOKEN }}
          organization-id: ${{ vars.SIGNPATH_ORGANIZATION_ID }}
          project-slug: tokensave
          # Production Foundation-cert policy (SignPath UI: Org > Projects > tokensave > Signing Policies).
          signing-policy-slug: release-signing
          github-artifact-id: ${{ steps.meta.outputs.artifact-id }}
          wait-for-completion: true
          wait-for-completion-timeout-in-seconds: 3600
          output-artifact-directory: signed

      - name: Package signed binary
        run: |
          cd signed
          zip "../tokensave-${GITHUB_REF_NAME}-${{ matrix.name }}.zip" tokensave.exe

      - name: Upload signed binary archive
        env:
          GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
        # This job has no checkout, so gh can't infer the repo from git context.
        run: gh release upload "${GITHUB_REF_NAME}" "tokensave-${GITHUB_REF_NAME}-${{ matrix.name }}.zip" --clobber --repo "$GITHUB_REPOSITORY"

  update-homebrew:
    name: Update Homebrew tap
    if: github.event_name == 'release' && !github.event.release.prerelease && !cancelled()
    needs: build
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v5

      - name: Get version
        id: version
        run: |
          VERSION="${GITHUB_REF_NAME#v}"
          echo "version=$VERSION" >> "$GITHUB_OUTPUT"

      - name: Download bottle artifacts
        uses: actions/download-artifact@v7
        with:
          path: bottles
          merge-multiple: true

      - name: Upload bottles to release
        env:
          GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
        run: |
          for f in bottles/tokensave-*.bottle.tar.gz; do
            gh release upload "${GITHUB_REF_NAME}" "$f" --clobber
          done

      - name: Download source tarball
        run: |
          VERSION="${{ steps.version.outputs.version }}"
          curl -sL "https://github.com/${{ github.repository }}/archive/refs/tags/${GITHUB_REF_NAME}.tar.gz" -o "source.tar.gz"

      - name: Compute SHA256 hashes
        id: hashes
        run: |
          VERSION="${{ steps.version.outputs.version }}"
          SOURCE_SHA=$(sha256sum source.tar.gz | awk '{print $1}')
          echo "source_sha=${SOURCE_SHA}" >> "$GITHUB_OUTPUT"

          for f in bottles/tokensave-*.bottle.tar.gz; do
            tag=$(basename "$f" | sed "s/tokensave-${VERSION}\.\(.*\)\.bottle\.tar\.gz/\1/")
            sha=$(sha256sum "$f" | awk '{print $1}')
            echo "${tag}_sha=${sha}" >> "$GITHUB_OUTPUT"
          done

      - name: Update formula
        env:
          TAP_GITHUB_TOKEN: ${{ secrets.TAP_GITHUB_TOKEN }}
        run: |
          VERSION="${{ steps.version.outputs.version }}"
          SOURCE_SHA="${{ steps.hashes.outputs.source_sha }}"
          ARM64_SONOMA_SHA="${{ steps.hashes.outputs.arm64_sonoma_sha }}"
          X86_64_LINUX_SHA="${{ steps.hashes.outputs.x86_64_linux_sha }}"

          git clone "https://x-access-token:${TAP_GITHUB_TOKEN}@github.com/aovestdipaperino/homebrew-tap.git" tap

          cat > tap/Formula/tokensave.rb << EOF
          class Tokensave < Formula
            desc "Code intelligence tool that builds semantic knowledge graphs from source code"
            homepage "https://github.com/aovestdipaperino/tokensave"
            url "https://github.com/aovestdipaperino/tokensave/archive/refs/tags/v${VERSION}.tar.gz"
            sha256 "${SOURCE_SHA}"
            license "MIT"

            bottle do
              root_url "https://github.com/aovestdipaperino/tokensave/releases/download/v${VERSION}"
              sha256 cellar: :any_skip_relocation, arm64_sonoma: "${ARM64_SONOMA_SHA}"
              sha256 cellar: :any_skip_relocation, x86_64_linux: "${X86_64_LINUX_SHA}"
            end

            depends_on "rust" => :build

            def install
              system "cargo", "install", *std_cargo_args
            end

            test do
              system "#{bin}/tokensave", "--help"
            end
          end
          EOF

          cd tap
          git config user.name "github-actions[bot]"
          git config user.email "github-actions[bot]@users.noreply.github.com"
          git add Formula/tokensave.rb
          git diff --cached --quiet || git commit -m "tokensave ${VERSION}"
          git push

  publish-crate:
    name: Publish to crates.io
    if: github.event_name == 'release' && !github.event.release.prerelease
    needs: build
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v5

      - uses: dtolnay/rust-toolchain@stable

      - name: Publish
        run: cargo publish
        env:
          CARGO_REGISTRY_TOKEN: ${{ secrets.CARGO_REGISTRY_TOKEN }}

  update-scoop:
    name: Update Scoop bucket
    if: github.event_name == 'release' && !github.event.release.prerelease && !cancelled()
    # Depends on sign-windows: it downloads the signed Windows zip from the release to hash it.
    needs: sign-windows
    runs-on: ubuntu-latest
    steps:
      - name: Get release info
        id: release
        env:
          GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
        run: |
          VERSION="${GITHUB_REF_NAME#v}"
          echo "version=${VERSION}" >> "$GITHUB_OUTPUT"
          echo "tag=${GITHUB_REF_NAME}" >> "$GITHUB_OUTPUT"

          gh release download "${GITHUB_REF_NAME}" \
            --repo "${{ github.repository }}" \
            --pattern "tokensave-${GITHUB_REF_NAME}-x86_64-windows.zip" \
            --pattern "tokensave-${GITHUB_REF_NAME}-aarch64-windows.zip" \
            --dir .
          # Fail loudly on a missing asset. `VAR=$(sha256sum missing)` would
          # otherwise mask the error from `set -e` and publish an empty hash
          # against a 404 URL (the WinARM regression this guards against).
          for arch in x86_64-windows aarch64-windows; do
            if [ ! -f "tokensave-${GITHUB_REF_NAME}-${arch}.zip" ]; then
              echo "::error::required release asset tokensave-${GITHUB_REF_NAME}-${arch}.zip is missing — aborting Scoop update"
              exit 1
            fi
          done
          SHA256=$(sha256sum "tokensave-${GITHUB_REF_NAME}-x86_64-windows.zip" | cut -d' ' -f1)
          echo "sha256_win64=${SHA256}" >> "$GITHUB_OUTPUT"
          SHA256_ARM=$(sha256sum "tokensave-${GITHUB_REF_NAME}-aarch64-windows.zip" | cut -d' ' -f1)
          echo "sha256_winarm64=${SHA256_ARM}" >> "$GITHUB_OUTPUT"

      - name: Update Scoop manifest
        env:
          SCOOP_GITHUB_TOKEN: ${{ secrets.TAP_GITHUB_TOKEN }}
        run: |
          VERSION="${{ steps.release.outputs.version }}"
          TAG="${{ steps.release.outputs.tag }}"
          SHA256="${{ steps.release.outputs.sha256_win64 }}"
          SHA256_ARM="${{ steps.release.outputs.sha256_winarm64 }}"

          cat > tokensave.json << EOF
          {
            "version": "${VERSION}",
            "description": "Code intelligence tool that builds semantic knowledge graphs from source code",
            "homepage": "https://github.com/aovestdipaperino/tokensave",
            "license": "MIT",
            "architecture": {
              "64bit": {
                "url": "https://github.com/aovestdipaperino/tokensave/releases/download/${TAG}/tokensave-${TAG}-x86_64-windows.zip",
                "hash": "${SHA256}"
              },
              "arm64": {
                "url": "https://github.com/aovestdipaperino/tokensave/releases/download/${TAG}/tokensave-${TAG}-aarch64-windows.zip",
                "hash": "${SHA256_ARM}"
              }
            },
            "bin": "tokensave.exe",
            "checkver": {
              "github": "https://github.com/aovestdipaperino/tokensave"
            },
            "autoupdate": {
              "architecture": {
                "64bit": {
                  "url": "https://github.com/aovestdipaperino/tokensave/releases/download/v\$version/tokensave-v\$version-x86_64-windows.zip"
                },
                "arm64": {
                  "url": "https://github.com/aovestdipaperino/tokensave/releases/download/v\$version/tokensave-v\$version-aarch64-windows.zip"
                }
              }
            }
          }
          EOF

          git clone "https://x-access-token:${SCOOP_GITHUB_TOKEN}@github.com/aovestdipaperino/scoop-bucket.git" scoop-repo
          cd scoop-repo
          mkdir -p bucket
          cp ../tokensave.json bucket/tokensave.json
          git rm -f --ignore-unmatch tokensave.json
          git config user.name "github-actions[bot]"
          git config user.email "github-actions[bot]@users.noreply.github.com"
          git add bucket/tokensave.json
          git diff --cached --quiet || git commit -m "tokensave ${VERSION}"
          git push