Skip to content

test(ssh): add authenticated push and wrong-key rejection (GM-08) #44

test(ssh): add authenticated push and wrong-key rejection (GM-08)

test(ssh): add authenticated push and wrong-key rejection (GM-08) #44

name: Git Protocol Smoke
# Automated regression gate for `scripts/git_protocol_smoke.sh` (protocol.md
# P0: "建立真实 Git 客户端兼容性矩阵"). The script exercises the real Git
# CLI against a running `service http` instance, covering ls-remote, clone,
# fetch, protocol v2 fetch, shallow clone, and blob:none partial clone.
#
# CI starts `service http` (which applies migrations), then seeds a one-off
# Mono access token into the smoke database and runs the HTTP push/delete
# branch+tag and LFS round-trip matrices with Basic Auth.
on:
pull_request:
paths:
- ".github/workflows/git-protocol-smoke.yml"
- "scripts/git_protocol_smoke.sh"
- "src/ceres/protocol/**"
- "src/contract/git_protocol/**"
- "src/ceres/lfs/**"
- "src/api/router/lfs_router.rs"
- "src/server/http_server.rs"
- "docs/refactoring/protocol.md"
- "docker-compose.test.yml"
- "src/ceres/pack/**"
- "src/jupiter/storage/git_db_storage.rs"
- "src/jupiter/storage/mono_storage.rs"
- "src/jupiter/storage/lfs_db_storage.rs"
push:
branches:
- main
paths:
- ".github/workflows/git-protocol-smoke.yml"
- "scripts/git_protocol_smoke.sh"
- "src/ceres/protocol/**"
- "src/contract/git_protocol/**"
- "src/ceres/lfs/**"
- "src/api/router/lfs_router.rs"
- "src/server/http_server.rs"
- "docs/refactoring/protocol.md"
- "docker-compose.test.yml"
- "src/ceres/pack/**"
- "src/jupiter/storage/git_db_storage.rs"
- "src/jupiter/storage/mono_storage.rs"
- "src/jupiter/storage/lfs_db_storage.rs"
jobs:
git-protocol-smoke:
runs-on: ubuntu-latest
# release build + cargo-native integration_git_cli + shell smoke exceeds 30m
# (v0.1.174/175 timed out during the cargo gate). Keep headroom for cold caches.
timeout-minutes: 60
env:
# Hosted-runner client pins (ubuntu-latest 20260726 image). Drift fails the job.
MONOENGINE_CI_GIT_VERSION: "2.54.0"
MONOENGINE_CI_GIT_LFS_VERSION: "3.7.1"
steps:
- name: Checkout monoengine
uses: actions/checkout@v5
with:
path: monoengine
- name: Validate orbit checkout token
env:
ORBIT_CHECKOUT_TOKEN: ${{ secrets.ORBIT_CHECKOUT_TOKEN }}
run: |
if [ -z "${ORBIT_CHECKOUT_TOKEN}" ]; then
echo "::error title=Missing ORBIT_CHECKOUT_TOKEN::Git protocol smoke needs to checkout the private gitmono-dev/orbit sibling repository. Add an Actions secret named ORBIT_CHECKOUT_TOKEN with read-only contents access to gitmono-dev/orbit."
exit 1
fi
- name: Checkout orbit sibling
uses: actions/checkout@v5
with:
repository: gitmono-dev/orbit
ref: main
path: orbit
token: ${{ secrets.ORBIT_CHECKOUT_TOKEN }}
- name: Install Rust toolchain
run: |
rustup toolchain install stable --profile minimal
rustup default stable
- name: Install and pin git / git-lfs
run: |
set -euo pipefail
parse_git_ver() { sed -n 's/^git version //p'; }
parse_lfs_ver() { sed -n 's|^git-lfs/\([^ ]*\).*|\1|p'; }
# git: assert the hosted-runner binary equals the pin (do not apt-upgrade).
git_out="$(git --version)"
git_ver="$(printf '%s\n' "$git_out" | parse_git_ver)"
[ -n "$git_ver" ] || { echo "::error::failed to parse git --version: $git_out"; exit 1; }
[ "$git_ver" = "${MONOENGINE_CI_GIT_VERSION}" ] || {
echo "::error::git version drift: got [$git_ver], pin [${MONOENGINE_CI_GIT_VERSION}]"
exit 1
}
# git-lfs: keep a matching preinstalled binary; otherwise install the
# pinned GitHub release artifact (immutable). Never `apt install` latest.
need_lfs_install=1
if command -v git-lfs >/dev/null 2>&1; then
lfs_out="$(git lfs version)"
lfs_ver="$(printf '%s\n' "$lfs_out" | parse_lfs_ver)"
if [ -n "$lfs_ver" ] && [ "$lfs_ver" = "${MONOENGINE_CI_GIT_LFS_VERSION}" ]; then
need_lfs_install=0
fi
fi
if [ "$need_lfs_install" -eq 1 ]; then
arch="$(uname -m)"
case "$arch" in
x86_64) lfs_arch=amd64 ;;
aarch64|arm64) lfs_arch=arm64 ;;
*) echo "::error::unsupported arch for git-lfs pin: $arch"; exit 1 ;;
esac
ver="${MONOENGINE_CI_GIT_LFS_VERSION}"
url="https://github.com/git-lfs/git-lfs/releases/download/v${ver}/git-lfs-linux-${lfs_arch}-v${ver}.tar.gz"
curl -fsSL "$url" -o /tmp/git-lfs.tgz
rm -rf "/tmp/git-lfs-${ver}"
mkdir -p "/tmp/git-lfs-${ver}"
tar -xzf /tmp/git-lfs.tgz -C "/tmp/git-lfs-${ver}" --strip-components=1
sudo "/tmp/git-lfs-${ver}/install.sh"
fi
lfs_out="$(git lfs version)"
lfs_ver="$(printf '%s\n' "$lfs_out" | parse_lfs_ver)"
[ -n "$lfs_ver" ] || { echo "::error::failed to parse git lfs version: $lfs_out"; exit 1; }
[ "$lfs_ver" = "${MONOENGINE_CI_GIT_LFS_VERSION}" ] || {
echo "::error::git-lfs version drift: got [$lfs_ver], pin [${MONOENGINE_CI_GIT_LFS_VERSION}]"
exit 1
}
{
echo "## CI git client pins"
echo ""
echo "- \`git --version\`: \`${git_out}\` (pin \`${MONOENGINE_CI_GIT_VERSION}\`)"
echo "- \`git lfs version\`: \`${lfs_out}\` (pin \`${MONOENGINE_CI_GIT_LFS_VERSION}\`)"
} >> "${GITHUB_STEP_SUMMARY}"
git config --global user.email "ci@example.invalid"
git config --global user.name "CI Smoke"
- name: Build monoengine binary
working-directory: monoengine
run: cargo build --release -p monoengine
- name: Mask test secrets in logs
run: |
echo "::add-mask::monoengine_test_password"
- name: Start test services (PostgreSQL, Redis, git-cli)
working-directory: monoengine
run: |
set -euo pipefail
dir="${MONOENGINE_IT_GIT_WORKDIR:-/tmp/monoengine-git}"
mkdir -p "$dir" && chmod 1777 "$dir"
export MONOENGINE_IT_GIT_UID="$(id -u)"
export MONOENGINE_IT_GIT_GID="$(id -g)"
# git-cli is under profiles: ["git"]; bring it up alongside postgres/redis so
# the cargo-native integration_git_cli gate can run on protocol path changes.
docker compose -p monoengine-it -f docker-compose.test.yml --profile git up -d --wait postgres redis git-cli
docker compose -p monoengine-it -f docker-compose.test.yml ps
- name: Run cargo-native git-cli integration gate
working-directory: monoengine
env:
MEGA_DATABASE__DB_URL: postgres://monoengine:monoengine_test_password@127.0.0.1:15432/monoengine
MEGA_REDIS__URL: redis://127.0.0.1:16379
run: |
set -euo pipefail
# Covers auth / failpath / byte-for-byte assertions when protocol paths
# change (config-validation allowlist A does not include those paths).
# --release reuses the prior release build artifacts (debug rebuild was
# the dominant cost that pushed v0.1.174/175 past the old 30m timeout).
cargo test -p monoengine --release --test integration_git_cli -- --test-threads=1
- name: Prepare smoke config
working-directory: monoengine
env:
MEGA_DATABASE__DB_URL: postgres://monoengine:monoengine_test_password@127.0.0.1:15432/monoengine_smoke
MEGA_REDIS__URL: redis://127.0.0.1:16379
MEGA_BASE_DIR: ${{ runner.temp }}/monoengine-smoke/base
run: |
set -euo pipefail
smoke_dir="${RUNNER_TEMP}/monoengine-smoke"
mkdir -p "${smoke_dir}/base"
cp config/config.toml "${smoke_dir}/config.toml"
# Create a dedicated database for the smoke run. Schema is applied when
# `service http` boots (see jupiter::storage::init::database_connection).
# Do not INSERT into access_token here — the table does not exist yet.
psql "postgres://monoengine:monoengine_test_password@127.0.0.1:15432/monoengine" \
-c "DROP DATABASE IF EXISTS monoengine_smoke" \
-c "CREATE DATABASE monoengine_smoke"
smoke_git_token="$(python3 -c 'import secrets; print(secrets.token_urlsafe(32))')"
echo "::add-mask::${smoke_git_token}"
echo "SMOKE_CONFIG_PATH=${smoke_dir}/config.toml" >> "$GITHUB_ENV"
echo "SMOKE_BASE_DIR=${smoke_dir}/base" >> "$GITHUB_ENV"
echo "SMOKE_GIT_TOKEN=${smoke_git_token}" >> "$GITHUB_ENV"
- name: Start service http
working-directory: monoengine
env:
MEGA_DATABASE__DB_URL: postgres://monoengine:monoengine_test_password@127.0.0.1:15432/monoengine_smoke
MEGA_REDIS__URL: redis://127.0.0.1:16379
MEGA_DATABASE__SQLX_LOGGING: "false"
MEGA_LOG__PRINT_STD: "true"
MEGA_LOG__LEVEL: "info"
MEGA_BASE_DIR: ${{ env.SMOKE_BASE_DIR }}
run: |
set -euo pipefail
PORT=9000
echo "Starting monoengine service http on port ${PORT}..."
./target/release/monoengine --config "${SMOKE_CONFIG_PATH}" \
service http --host 127.0.0.1 -p "${PORT}" &
SERVICE_PID=$!
echo "SERVICE_PID=${SERVICE_PID}" >> "$GITHUB_ENV"
# Wait for the service to become ready (max 90 seconds).
deadline=$((SECONDS + 90))
until curl -sf "http://127.0.0.1:${PORT}/api/openapi.json" >/dev/null 2>&1; do
if [ $SECONDS -ge $deadline ]; then
echo "::error::service http did not become ready within 90 seconds"
kill $SERVICE_PID 2>/dev/null || true
exit 1
fi
sleep 1
done
echo "service http is ready"
- name: Seed smoke access token
working-directory: monoengine
env:
MEGA_DATABASE__DB_URL: postgres://monoengine:monoengine_test_password@127.0.0.1:15432/monoengine_smoke
run: |
set -euo pipefail
# Matches bin/tests/integration_git_cli: seed after service bootstrap so
# migrations have created access_token.
psql "${MEGA_DATABASE__DB_URL}" \
-v ON_ERROR_STOP=1 \
-v token="${SMOKE_GIT_TOKEN}" <<'SQL'
INSERT INTO access_token (id, username, token, created_at)
VALUES ((extract(epoch from clock_timestamp()) * 1000000)::bigint, 'ci-smoke', :'token', now());
SQL
- name: Run git protocol smoke (HTTP)
working-directory: monoengine
run: |
set -euo pipefail
# The monorepo root "/" is initialized by init_monorepo during
# service startup and has refs/heads/main. Push/delete uses the
# one-off access token seeded into the smoke database above.
MONOENGINE_HTTP_REPO_URL="http://ci-smoke:${SMOKE_GIT_TOKEN}@127.0.0.1:9000/" \
MONOENGINE_GIT_SMOKE_PUSH=1 \
MONOENGINE_GIT_SMOKE_LFS=1 \
bash scripts/git_protocol_smoke.sh
- name: Stop service http
if: always()
run: |
if [ -n "${SERVICE_PID:-}" ]; then
kill "${SERVICE_PID}" 2>/dev/null || true
wait "${SERVICE_PID}" 2>/dev/null || true
fi
- name: Stop test services
if: always()
working-directory: monoengine
run: docker compose -p monoengine-it -f docker-compose.test.yml --profile git down -v