ssh-mcp-rs 3.0.2

MCP server exposing SSH control for Linux systems via Model Context Protocol
Documentation
name: CI and Release

on:
  push:
    branches: [main, dev]
  pull_request:
    branches: [main, dev]
  workflow_dispatch:
    inputs:
      release_type:
        description: 'Тип релиза'
        required: true
        default: 'rolling'
        type: choice
        options:
          - rolling
          - stable
      version:
        description: 'Версия для stable релиза (например: 1.2.3). Игнорируется для rolling.'
        required: false
        type: string
  schedule:
    - cron: '0 0 * * *'

env:
  CARGO_TERM_COLOR: always
  RUST_BACKTRACE: 1

permissions:
  contents: write

jobs:
  test:
    name: Test
    runs-on: ubuntu-24.04
    steps:
      - uses: actions/checkout@v4
      - uses: dtolnay/rust-toolchain@stable
        with:
          components: rustfmt, clippy
      - uses: actions/cache@v4
        with:
          path: ~/.cargo/registry
          key: ${{ runner.os }}-cargo-registry-${{ hashFiles('**/Cargo.lock') }}
      - uses: actions/cache@v4
        with:
          path: ~/.cargo/git
          key: ${{ runner.os }}-cargo-index-${{ hashFiles('**/Cargo.lock') }}
      - uses: actions/cache@v4
        with:
          path: target
          key: ${{ runner.os }}-cargo-build-target-${{ hashFiles('**/Cargo.lock') }}
      - name: Run rustfmt
        run: cargo fmt --all -- --check
      - name: Run clippy
        run: cargo clippy --all-targets --all-features -- -D warnings
      - name: Run tests
        run: cargo test --all-features --verbose

  lint:
    name: Lint
    runs-on: ubuntu-24.04
    needs: test
    steps:
      - uses: actions/checkout@v4
      - uses: dtolnay/rust-toolchain@stable
      - uses: actions/cache@v4
        with:
          path: ~/.cargo/registry
          key: ${{ runner.os }}-cargo-registry-${{ hashFiles('**/Cargo.lock') }}
      - uses: actions/cache@v4
        with:
          path: ~/.cargo/git
          key: ${{ runner.os }}-cargo-index-${{ hashFiles('**/Cargo.lock') }}
      - name: Run cargo check
        run: cargo check --all-features --verbose

  build:
    name: Build (${{ matrix.target }})
    runs-on: ${{ matrix.os }}
    needs: [test, lint]
    if: (github.event_name == 'push' && github.ref == 'refs/heads/main') || github.event_name == 'schedule' || github.event_name == 'workflow_dispatch'
    strategy:
      matrix:
        include:
          - os: ubuntu-24.04
            target: x86_64-unknown-linux-gnu
            artifact_name: ssh-mcp
            asset_name: ssh-mcp-linux-x86_64
          - os: windows-latest
            target: x86_64-pc-windows-msvc
            artifact_name: ssh-mcp.exe
            asset_name: ssh-mcp-windows-x86_64.exe
          - os: macos-latest
            target: aarch64-apple-darwin
            artifact_name: ssh-mcp
            asset_name: ssh-mcp-macos-aarch64
    steps:
      - uses: actions/checkout@v4
      - uses: dtolnay/rust-toolchain@stable
        with:
          targets: ${{ matrix.target }}
      - uses: actions/cache@v4
        with:
          path: ~/.cargo/registry
          key: ${{ runner.os }}-cargo-registry-${{ hashFiles('**/Cargo.lock') }}
      - uses: actions/cache@v4
        with:
          path: ~/.cargo/git
          key: ${{ runner.os }}-cargo-index-${{ hashFiles('**/Cargo.lock') }}
      - name: Install dependencies (Ubuntu)
        if: matrix.os == 'ubuntu-24.04'
        run: |
          sudo apt-get update
          sudo apt-get install -y pkg-config libssl-dev
      - name: Build release binary
        run: cargo build --release --target ${{ matrix.target }}
      - name: Strip binary (Linux/macOS)
        if: runner.os != 'Windows'
        run: strip target/${{ matrix.target }}/release/${{ matrix.artifact_name }}
      - name: Upload artifact
        uses: actions/upload-artifact@v4
        with:
          name: ${{ matrix.asset_name }}
          path: target/${{ matrix.target }}/release/${{ matrix.artifact_name }}

  release-rolling:
    name: Create Rolling Release (Pre-release)
    runs-on: ubuntu-24.04
    needs: build
    if: (github.event_name == 'push' && github.ref == 'refs/heads/main') || github.event_name == 'schedule' || (github.event_name == 'workflow_dispatch' && github.event.inputs.release_type == 'rolling')
    steps:
      - uses: actions/checkout@v4
        with:
          fetch-depth: 0
      - uses: actions/download-artifact@v4
        with:
          path: artifacts
      - name: Set SHORT_SHA env
        run: echo "SHORT_SHA=$(git rev-parse --short HEAD)" >> $GITHUB_ENV
      - name: Prepare assets
        run: |
          mkdir -p dist
          for dir in artifacts/*/; do
            dirname=$(basename "$dir")
            cp "${dir}"* "dist/${dirname}"
          done
          chmod +x dist/*
      - name: Generate rolling release notes
        env:
          BEFORE_SHA: ${{ github.event.before }}
        run: |
          set -euo pipefail

          ZERO_SHA="0000000000000000000000000000000000000000"
          PREVIOUS_SHA="$(git rev-parse --verify 'rolling^{commit}' 2>/dev/null || true)"
          BASE_REF=""
          PREVIOUS_LABEL=""

          if [ -n "$PREVIOUS_SHA" ] && git merge-base --is-ancestor "$PREVIOUS_SHA" "$GITHUB_SHA"; then
            BASE_REF="$PREVIOUS_SHA"
            PREVIOUS_LABEL="rolling@${PREVIOUS_SHA:0:7}"
          elif [ -n "$BEFORE_SHA" ] && [ "$BEFORE_SHA" != "$ZERO_SHA" ] \
            && git cat-file -e "${BEFORE_SHA}^{commit}" 2>/dev/null \
            && git merge-base --is-ancestor "$BEFORE_SHA" "$GITHUB_SHA"; then
            BASE_REF="$BEFORE_SHA"
          fi

          if [ -n "$BASE_REF" ]; then
            RANGE="${BASE_REF}..${GITHUB_SHA}"
          else
            RANGE="$GITHUB_SHA"
          fi
          COMMIT_COUNT="$(git rev-list --count "$RANGE")"

          {
            echo "## SSH MCP Rolling Release"
            echo
            if [ -n "$PREVIOUS_LABEL" ]; then
              echo "**Previous release:** \`${PREVIOUS_LABEL}\`"
            elif [ -n "$BASE_REF" ]; then
              echo "**Previous commit:** \`${BASE_REF:0:7}\`"
            else
              echo "**Range:** entire commit history"
            fi
            echo
            echo "**New commits:** ${COMMIT_COUNT}"
            echo
            echo "### Changes"
            echo
            if [ "$COMMIT_COUNT" -eq 0 ]; then
              echo "- No new commits."
            else
              git log --reverse \
                --format="- [\`%h\`](${GITHUB_SERVER_URL}/${GITHUB_REPOSITORY}/commit/%H) %s" \
                "$RANGE"
            fi
            echo
            if [ -n "$BASE_REF" ]; then
              echo "[Full changelog](${GITHUB_SERVER_URL}/${GITHUB_REPOSITORY}/compare/${BASE_REF}...${GITHUB_SHA})"
              echo
            fi
            echo "### Artifacts"
            echo
            for asset in dist/*; do
              echo "- \`$(basename "$asset")\`"
            done
            echo
            echo "> [!WARNING]"
            echo "> This is a pre-release build and may not be stable. Use at your own risk."
          } > release-notes.md
      - name: Delete existing rolling release
        env:
          GH_TOKEN: ${{ github.token }}
        run: |
          gh release delete rolling --cleanup-tag --yes || true
      - name: Create rolling release
        env:
          GH_TOKEN: ${{ github.token }}
        run: |
          gh release create rolling \
            --target ${{ github.sha }} \
            --title "Rolling Release (${{ env.SHORT_SHA }})" \
            --notes-file release-notes.md \
            --prerelease \
            dist/*

  release-stable:
    name: Create Stable Release
    runs-on: ubuntu-24.04
    needs: build
    if: github.event_name == 'workflow_dispatch' && github.event.inputs.release_type == 'stable'
    steps:
      - uses: actions/checkout@v4
        with:
          fetch-depth: 0
      - uses: actions/download-artifact@v4
        with:
          path: artifacts
      - name: Validate version input
        env:
          VERSION: ${{ github.event.inputs.version }}
        run: |
          if [ -z "$VERSION" ]; then
            echo "Error: version input is required for stable releases"
            exit 1
          fi
          if ! echo "$VERSION" | grep -qE '^[0-9]+\.[0-9]+\.[0-9]+$'; then
            echo "Error: version must be in semver format (e.g., 1.2.3)"
            exit 1
          fi
          echo "VERSION=$VERSION" >> $GITHUB_ENV
      - name: Prepare assets
        run: |
          mkdir -p dist
          for dir in artifacts/*/; do
            dirname=$(basename "$dir")
            cp "${dir}"* "dist/${dirname}"
          done
          chmod +x dist/*
      - name: Check if tag exists
        id: check_tag
        env:
          VERSION: ${{ env.VERSION }}
        run: |
          if git rev-parse "v${VERSION}" >/dev/null 2>&1; then
            echo "exists=true" >> $GITHUB_OUTPUT
          else
            echo "exists=false" >> $GITHUB_OUTPUT
          fi
      - name: Configure git
        run: |
          git config --global user.email "github-actions[bot]@users.noreply.github.com"
          git config --global user.name "github-actions[bot]"

      - name: Create git tag
        if: steps.check_tag.outputs.exists == 'false'
        env:
          VERSION: ${{ env.VERSION }}
        run: |
          git tag -a "v${VERSION}" -m "Release v${VERSION}"
          git push origin "v${VERSION}"
      - name: Check if release exists
        id: check_release
        env:
          GH_TOKEN: ${{ github.token }}
          VERSION: ${{ env.VERSION }}
        run: |
          if gh release view "v${VERSION}" >/dev/null 2>&1; then
            echo "exists=true" >> $GITHUB_OUTPUT
          else
            echo "exists=false" >> $GITHUB_OUTPUT
          fi

      - name: Generate stable release notes
        if: steps.check_release.outputs.exists == 'false'
        env:
          BEFORE_SHA: ${{ github.event.before }}
          VERSION: ${{ env.VERSION }}
        run: |
          set -euo pipefail

          ZERO_SHA="0000000000000000000000000000000000000000"
          PREVIOUS_TAG="$(git describe --tags --match 'v[0-9]*' --abbrev=0 "${GITHUB_SHA}^" 2>/dev/null || true)"
          BASE_REF=""

          if [ -n "$PREVIOUS_TAG" ]; then
            BASE_REF="$PREVIOUS_TAG"
          elif [ -n "$BEFORE_SHA" ] && [ "$BEFORE_SHA" != "$ZERO_SHA" ] \
            && git cat-file -e "${BEFORE_SHA}^{commit}" 2>/dev/null \
            && git merge-base --is-ancestor "$BEFORE_SHA" "$GITHUB_SHA"; then
            BASE_REF="$BEFORE_SHA"
          fi

          if [ -n "$BASE_REF" ]; then
            RANGE="${BASE_REF}..${GITHUB_SHA}"
          else
            RANGE="$GITHUB_SHA"
          fi
          COMMIT_COUNT="$(git rev-list --count "$RANGE")"

          {
            echo "## SSH MCP v${VERSION}"
            echo
            if [ -n "$PREVIOUS_TAG" ]; then
              echo "**Previous release:** \`${PREVIOUS_TAG}\`"
            elif [ -n "$BASE_REF" ]; then
              echo "**Previous commit:** \`${BASE_REF:0:7}\`"
            else
              echo "**Range:** entire commit history"
            fi
            echo
            echo "**New commits:** ${COMMIT_COUNT}"
            echo
            echo "### Changes"
            echo
            if [ "$COMMIT_COUNT" -eq 0 ]; then
              echo "- No new commits."
            else
              git log --reverse \
                --format="- [\`%h\`](${GITHUB_SERVER_URL}/${GITHUB_REPOSITORY}/commit/%H) %s" \
                "$RANGE"
            fi
            echo
            if [ -n "$BASE_REF" ]; then
              echo "[Full changelog](${GITHUB_SERVER_URL}/${GITHUB_REPOSITORY}/compare/${BASE_REF}...${GITHUB_SHA})"
              echo
            fi
            echo "### Artifacts"
            echo
            for asset in dist/*; do
              echo "- \`$(basename "$asset")\`"
            done
          } > release-notes.md

      - name: Create stable release
        if: steps.check_release.outputs.exists == 'false'
        env:
          GH_TOKEN: ${{ github.token }}
          VERSION: ${{ env.VERSION }}
        run: |
          gh release create "v${VERSION}" \
            --title "Release v${VERSION}" \
            --notes-file release-notes.md \
            --latest \
            dist/*