mars-agents 0.2.7

Agent package manager for .agents/ directories
Documentation
name: Release

on:
  push:
    tags: ["v*"]

env:
  CARGO_TERM_COLOR: always

jobs:
  build:
    name: Build (${{ matrix.target }})
    runs-on: ${{ matrix.runner }}
    strategy:
      matrix:
        include:
          - target: x86_64-unknown-linux-gnu
            runner: ubuntu-latest
            artifact: mars-linux-x64
          - target: aarch64-unknown-linux-gnu
            runner: ubuntu-latest
            artifact: mars-linux-arm64
          - target: aarch64-apple-darwin
            runner: macos-latest
            artifact: mars-darwin-arm64
          - target: x86_64-apple-darwin
            runner: macos-15-intel
            artifact: mars-darwin-x64
          - target: x86_64-pc-windows-msvc
            runner: windows-latest
            artifact: mars-windows-x64.exe
    steps:
      - uses: actions/checkout@v4

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

      - uses: Swatinem/rust-cache@v2
        with:
          key: ${{ matrix.target }}

      - name: Install Linux ARM64 cross toolchain
        if: matrix.target == 'aarch64-unknown-linux-gnu'
        run: |
          sudo apt-get update
          sudo apt-get install -y gcc-aarch64-linux-gnu binutils-aarch64-linux-gnu

      - name: Build
        if: matrix.target != 'aarch64-unknown-linux-gnu'
        run: cargo build --release --target ${{ matrix.target }}

      - name: Build (Linux ARM64)
        if: matrix.target == 'aarch64-unknown-linux-gnu'
        env:
          CARGO_TARGET_AARCH64_UNKNOWN_LINUX_GNU_LINKER: aarch64-linux-gnu-gcc
        run: cargo build --release --target ${{ matrix.target }}

      - name: Strip binary
        if: matrix.target != 'aarch64-unknown-linux-gnu' && !contains(matrix.target, 'windows')
        run: strip target/${{ matrix.target }}/release/mars

      - name: Strip binary (Linux ARM64)
        if: matrix.target == 'aarch64-unknown-linux-gnu'
        run: aarch64-linux-gnu-strip target/${{ matrix.target }}/release/mars

      - name: Rename binary
        shell: bash
        run: |
          if [[ "${{ matrix.artifact }}" == *.exe ]]; then
            cp target/${{ matrix.target }}/release/mars.exe ${{ matrix.artifact }}
          else
            cp target/${{ matrix.target }}/release/mars ${{ matrix.artifact }}
          fi

      - name: Smoke test (Windows)
        if: contains(matrix.target, 'windows')
        shell: pwsh
        run: |
          .\target\${{ matrix.target }}\release\mars.exe --version
          $tmp = Join-Path $env:TEMP "mars-smoke-$([guid]::NewGuid())"
          New-Item -ItemType Directory -Path $tmp | Out-Null
          .\target\${{ matrix.target }}\release\mars.exe init --root $tmp
          .\target\${{ matrix.target }}\release\mars.exe doctor --root $tmp

      - uses: actions/upload-artifact@v4
        with:
          name: ${{ matrix.artifact }}
          path: ${{ matrix.artifact }}

  pypi-wheels:
    name: Build PyPI wheels (${{ matrix.target }})
    runs-on: ${{ matrix.runner }}
    strategy:
      matrix:
        include:
          - target: x86_64-unknown-linux-gnu
            runner: ubuntu-latest
            artifact: wheels-linux-x64
          - target: aarch64-unknown-linux-gnu
            runner: ubuntu-24.04-arm
            artifact: wheels-linux-arm64
          - target: aarch64-apple-darwin
            runner: macos-latest
            artifact: wheels-darwin-arm64
          - target: x86_64-apple-darwin
            runner: macos-15-intel
            artifact: wheels-darwin-x64
          - target: x86_64-pc-windows-msvc
            runner: windows-latest
            artifact: wheels-windows-x64
    steps:
      - uses: actions/checkout@v4

      - name: Build wheels
        uses: PyO3/maturin-action@v1
        with:
          command: build
          target: ${{ matrix.target }}
          manylinux: auto
          args: --release --out dist

      - uses: actions/upload-artifact@v4
        with:
          name: ${{ matrix.artifact }}
          path: dist/*.whl

  pypi-sdist:
    name: Build PyPI sdist
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - name: Build sdist
        uses: PyO3/maturin-action@v1
        with:
          command: sdist
          args: --out dist

      - uses: actions/upload-artifact@v4
        with:
          name: sdist
          path: dist/*.tar.gz

  pypi-publish:
    name: Publish PyPI
    needs: [pypi-wheels, pypi-sdist]
    runs-on: ubuntu-latest
    environment: pypi
    permissions:
      id-token: write
    steps:
      - uses: actions/download-artifact@v4
        with:
          path: dist
          pattern: wheels-*
          merge-multiple: true

      - uses: actions/download-artifact@v4
        with:
          path: dist
          name: sdist

      - name: Publish to PyPI
        uses: pypa/gh-action-pypi-publish@release/v1
        with:
          packages-dir: dist

  github-release:
    name: GitHub Release
    needs: build
    runs-on: ubuntu-latest
    permissions:
      contents: write
    steps:
      - uses: actions/checkout@v4

      - uses: actions/download-artifact@v4
        with:
          path: artifacts/
          merge-multiple: true

      - name: Create release
        uses: softprops/action-gh-release@v2
        with:
          generate_release_notes: true
          files: artifacts/mars-*

  npm-publish:
    name: Publish npm packages
    needs: build
    runs-on: ubuntu-latest
    permissions:
      id-token: write
    steps:
      - uses: actions/checkout@v4

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

      - name: Ensure npm supports trusted publishing
        run: npm install -g npm@latest

      - uses: actions/download-artifact@v4
        with:
          path: artifacts/
          pattern: mars-*
          merge-multiple: true

      - name: Extract version from tag
        id: version
        run: echo "version=${GITHUB_REF_NAME#v}" >> "$GITHUB_OUTPUT"

      - name: Verify tag matches Cargo.toml
        run: |
          CARGO_VERSION=$(grep '^version' Cargo.toml | head -1 | sed 's/.*"\(.*\)".*/\1/')
          TAG_VERSION=${{ steps.version.outputs.version }}
          if [ "$CARGO_VERSION" != "$TAG_VERSION" ]; then
            echo "::error::Tag version ($TAG_VERSION) does not match Cargo.toml ($CARGO_VERSION)"
            exit 1
          fi

      - name: Prepare and publish platform packages
        run: |
          VERSION=${{ steps.version.outputs.version }}

          publish_platform() {
            local pkg_dir=$1
            local binary=$2
            local pkg_name
            pkg_name=$(node -e "console.log(require('./$pkg_dir/package.json').name)")

            # Skip if already published (idempotent on re-run)
            if npm view "$pkg_name@$VERSION" version >/dev/null 2>&1; then
              echo "Skipping $pkg_name@$VERSION — already published"
              return 0
            fi

            (
              cd "$pkg_dir"
              npm version "$VERSION" --no-git-tag-version --allow-same-version
              if [[ "$binary" == *.exe ]]; then
                cp "$GITHUB_WORKSPACE/artifacts/$binary" mars.exe
              else
                cp "$GITHUB_WORKSPACE/artifacts/$binary" mars
                chmod +x mars
              fi
              npm publish --provenance --access public --tag latest
            )
          }

          publish_platform npm/@meridian-flow/mars-agents-linux-x64 mars-linux-x64
          publish_platform npm/@meridian-flow/mars-agents-linux-arm64 mars-linux-arm64
          publish_platform npm/@meridian-flow/mars-agents-darwin-arm64 mars-darwin-arm64
          publish_platform npm/@meridian-flow/mars-agents-darwin-x64 mars-darwin-x64
          publish_platform npm/@meridian-flow/mars-agents-win32-x64 mars-windows-x64.exe

      - name: Publish CLI stub package
        run: |
          VERSION=${{ steps.version.outputs.version }}

          # Skip if already published
          if npm view "@meridian-flow/mars-agents@$VERSION" version >/dev/null 2>&1; then
            echo "Skipping @meridian-flow/mars-agents@$VERSION — already published"
            exit 0
          fi

          (
            cd npm/@meridian-flow/mars-agents
            node -e "
              const pkg = require('./package.json');
              pkg.version = '$VERSION';
              for (const dep of Object.keys(pkg.optionalDependencies || {})) {
                pkg.optionalDependencies[dep] = '$VERSION';
              }
              require('fs').writeFileSync('package.json', JSON.stringify(pkg, null, 2) + '\n');
            "
            npm publish --provenance --access public --tag latest
          )

  cargo-publish:
    name: Publish crate
    needs: build
    runs-on: ubuntu-latest
    permissions:
      contents: read
    steps:
      - uses: actions/checkout@v4

      - uses: dtolnay/rust-toolchain@stable

      - name: Publish to crates.io
        env:
          CARGO_REGISTRY_TOKEN: ${{ secrets.CARGO_REGISTRY_TOKEN }}
        run: cargo publish || true