tsift-cli 0.1.64

CLI dispatch layer for tsift — clap types, command handlers, and output formatting
Documentation
name: Release

on:
  push:
    tags:
      - "v*"
  workflow_dispatch:

env:
  CARGO_TERM_COLOR: always
  BINARY_NAME: tsift

permissions:
  contents: write

concurrency:
  group: release-${{ github.ref }}
  cancel-in-progress: false

jobs:
  verify:
    name: verify release inputs
    runs-on: ubuntu-latest
    defaults:
      run:
        working-directory: tsift

    steps:
      - uses: actions/checkout@v4
        with:
          path: tsift

      - name: Install Rust stable
        uses: dtolnay/rust-toolchain@stable
        with:
          components: clippy

      - name: Install ripgrep
        run: sudo apt-get update && sudo apt-get install -y ripgrep

      - name: Validate tag matches crate version
        if: startsWith(github.ref, 'refs/tags/v')
        shell: bash
        run: |
          tag_version="${GITHUB_REF_NAME#v}"
          while IFS= read -r manifest; do
            crate_name="$(sed -n 's/^name = "\(.*\)"/\1/p' "$manifest" | head -n 1)"
            crate_version="$(sed -n 's/^version = "\(.*\)"/\1/p' "$manifest" | head -n 1)"
            if [ -z "$crate_name" ] || [ -z "$crate_version" ]; then
              echo "failed to read package name/version from $manifest" >&2
              exit 1
            fi
            if [ "$tag_version" != "$crate_version" ]; then
              echo "tag version v$tag_version does not match $crate_name version $crate_version in $manifest" >&2
              exit 1
            fi
          done < <(find . -name Cargo.toml -not -path './target/*' | sort)

      - name: Full CI suite
        run: make ci-full

      - name: Crate package file check
        shell: bash
        run: |
          for package in \
            tsift-core \
            tsift-graph \
            tsift-sqlite \
            tsift-algorithms \
            tsift-resolution \
            tsift-tokensave \
            tsift-libsql \
            tsift-index \
            tsift-summarize \
            tsift-quality \
            tsift-agent-doc \
            tsift-digest \
            tsift-search \
            tsift-status \
            tsift-session \
            tsift-memory \
            tsift-surrealdb \
            tsift-cli \
            tsift \
            tsift-sim-world
          do
            cargo package -p "$package" --locked --allow-dirty --list > "/tmp/${package}.package-files"
          done

      - name: OpenCode plugin publish dry run
        run: npm run publish:check
        working-directory: tsift/packages/opencode-tsift

  build:
    name: build ${{ matrix.target }}
    runs-on: ${{ matrix.os }}
    needs: verify
    defaults:
      run:
        working-directory: tsift
    strategy:
      fail-fast: false
      matrix:
        include:
          - os: ubuntu-latest
            target: x86_64-unknown-linux-gnu
            asset_name: tsift-x86_64-unknown-linux-gnu.tar.gz
          - os: macos-14
            target: aarch64-apple-darwin
            asset_name: tsift-aarch64-apple-darwin.tar.gz
          - os: windows-latest
            target: x86_64-pc-windows-msvc
            asset_name: tsift-x86_64-pc-windows-msvc.zip

    steps:
      - uses: actions/checkout@v4
        with:
          path: tsift

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

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

      - name: Package Unix archive
        if: runner.os != 'Windows'
        shell: bash
        run: |
          stage_dir="dist/${BINARY_NAME}-${{ matrix.target }}"
          mkdir -p "$stage_dir"
          cp "target/${{ matrix.target }}/release/${BINARY_NAME}" "$stage_dir/"
          cp LICENSE "$stage_dir/"
          tar -C dist -czf "dist/${{ matrix.asset_name }}" "${BINARY_NAME}-${{ matrix.target }}"
          shasum -a 256 "dist/${{ matrix.asset_name }}" > "dist/${{ matrix.asset_name }}.sha256"

      - name: Package Windows archive
        if: runner.os == 'Windows'
        shell: pwsh
        run: |
          $stageDir = "dist/${env:BINARY_NAME}-${{ matrix.target }}"
          New-Item -ItemType Directory -Force -Path $stageDir | Out-Null
          Copy-Item "target/${{ matrix.target }}/release/${env:BINARY_NAME}.exe" "$stageDir/"
          Copy-Item "LICENSE" "$stageDir/"
          Compress-Archive -Path "$stageDir/*" -DestinationPath "dist/${{ matrix.asset_name }}"
          $hash = (Get-FileHash "dist/${{ matrix.asset_name }}" -Algorithm SHA256).Hash.ToLower()
          "$hash  ${{ matrix.asset_name }}" | Out-File -Encoding ascii "dist/${{ matrix.asset_name }}.sha256"

      - name: Upload release artifact
        uses: actions/upload-artifact@v4
        with:
          name: release-${{ matrix.target }}
          path: tsift/dist/*
          if-no-files-found: error

  github-release:
    name: publish GitHub release
    runs-on: ubuntu-latest
    needs: build
    if: startsWith(github.ref, 'refs/tags/v')

    steps:
      - name: Download packaged artifacts
        uses: actions/download-artifact@v4
        with:
          pattern: release-*
          merge-multiple: true
          path: dist

      - name: Create GitHub release
        uses: softprops/action-gh-release@v2
        with:
          files: dist/*
          fail_on_unmatched_files: true
          generate_release_notes: true

  publish-crate:
    name: publish crates.io package
    runs-on: ubuntu-latest
    defaults:
      run:
        working-directory: tsift
    needs:
      - verify
      - build
    if: startsWith(github.ref, 'refs/tags/v') && vars.TSIFT_ENABLE_CRATES_PUBLISH == 'true'

    steps:
      - uses: actions/checkout@v4
        with:
          path: tsift

      - name: Install Rust stable
        uses: dtolnay/rust-toolchain@stable

      - name: Publish crate
        shell: bash
        run: |
          release_version="${GITHUB_REF_NAME#v}"
          max_attempts=12

          crate_version_exists() {
            local package="$1"
            cargo info --registry crates-io "$package@$release_version" >/dev/null 2>&1
          }

          publish_package() {
            local package="$1"
            local attempt=1
            local output
            local status

            if crate_version_exists "$package"; then
              echo "$package v$release_version already exists on crates.io; skipping"
              return 0
            fi

            while true; do
              cargo publish -p "$package" --locked --dry-run

              set +e
              output="$(cargo publish -p "$package" --locked 2>&1)"
              status=$?
              set -e
              printf '%s\n' "$output"

              if [ "$status" -eq 0 ]; then
                sleep 20
                return 0
              fi

              if crate_version_exists "$package"; then
                echo "$package v$release_version is visible on crates.io after publish attempt; continuing"
                sleep 20
                return 0
              fi

              if ! printf '%s\n' "$output" | grep -Eiq 'Too Many Requests|rate limit|try again after'; then
                return "$status"
              fi

              if [ "$attempt" -ge "$max_attempts" ]; then
                echo "crates.io rate limit did not clear for $package after $max_attempts attempts" >&2
                return "$status"
              fi

              echo "crates.io rate-limited $package publish attempt $attempt/$max_attempts; retrying in 180 seconds"
              sleep 180
              attempt=$((attempt + 1))
            done
          }

          for package in \
            tsift-core \
            tsift-graph \
            tsift-sqlite \
            tsift-algorithms \
            tsift-resolution \
            tsift-tokensave \
            tsift-libsql \
            tsift-index \
            tsift-summarize \
            tsift-quality \
            tsift-agent-doc \
            tsift-digest \
            tsift-search \
            tsift-status \
            tsift-session \
            tsift-memory \
            tsift-surrealdb \
            tsift-cli \
            tsift \
            tsift-sim-world
          do
            publish_package "$package"
          done
        env:
          CARGO_REGISTRY_TOKEN: ${{ secrets.CARGO_REGISTRY_TOKEN }}

  publish-opencode-plugin:
    name: publish OpenCode npm plugin
    runs-on: ubuntu-latest
    needs:
      - verify
      - build
    # OIDC trusted publishing — no NPM_TOKEN secret. Requires the package to
    # already exist on npm (npm has no "pending publisher"); bootstrap the first
    # version manually, then configure the Trusted Publisher on the package
    # (org/user, repo=tsift, workflow=release.yml, action=npm publish).
    permissions:
      id-token: write
      contents: read
    if: startsWith(github.ref, 'refs/tags/v') && vars.TSIFT_ENABLE_NPM_PUBLISH == 'true'

    steps:
      - uses: actions/checkout@v4
        with:
          path: tsift

      - uses: actions/setup-node@v4
        with:
          node-version: 24
          registry-url: https://registry.npmjs.org

      # Trusted publishing needs npm CLI >= 11.5.1; Node 24's bundled npm may lag.
      - name: Upgrade npm for OIDC trusted publishing
        run: npm install -g npm@latest

      - name: Publish OpenCode plugin package (OIDC trusted publishing)
        run: npm publish --access public
        working-directory: tsift/packages/opencode-tsift