test(ssh): add authenticated push and wrong-key rejection (GM-08) #44
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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 |