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/*