From 55756a30de04c0773cd58ccc4c051d48d6401846 Mon Sep 17 00:00:00 2001 From: LucaLin233 Date: Wed, 9 Sep 2026 03:35:55 +0800 Subject: [PATCH] ci: parallelize isolated test shards and separate lint --- .github/workflows/shell-tests.yml | 86 ++++++++++++++++++++++++------- tests/run.sh | 20 +++++++ tests/test-shards.sh | 65 +++++++++++++++++++++++ 3 files changed, 152 insertions(+), 19 deletions(-) create mode 100644 tests/test-shards.sh diff --git a/.github/workflows/shell-tests.yml b/.github/workflows/shell-tests.yml index a949d38..9a0299b 100644 --- a/.github/workflows/shell-tests.yml +++ b/.github/workflows/shell-tests.yml @@ -13,8 +13,8 @@ concurrency: cancel-in-progress: true jobs: - ubuntu: - name: Ubuntu 24.04 lint and tests + lint: + name: ShellCheck runs-on: ubuntu-24.04 timeout-minutes: 15 steps: @@ -37,19 +37,6 @@ jobs: run: | command -v shellcheck shellcheck --version - command -v jq - command -v ip - command -v rsync - command -v ssh - - - name: Verify checkout and real Git selection - shell: bash - run: | - # Checkout uses a temporary HOME; trust only this job's workspace. - git config --global --add safe.directory "$GITHUB_WORKSPACE" - test "$(git rev-parse --show-toplevel)" = "$GITHUB_WORKSPACE" - git rev-parse --verify HEAD - bash tests/test-selection-git.sh - name: Check Bash syntax run: find . -type f -name '*.sh' -print0 | xargs -0 -n1 bash -n @@ -64,17 +51,51 @@ jobs: git ls-files -z -- "tests/*.sh" | xargs -0 -r shellcheck -x --severity=error -- || status=1 exit "$status" + ubuntu-tests: + name: Ubuntu 24.04 / ${{ matrix.shard }} + runs-on: ubuntu-24.04 + timeout-minutes: 15 + strategy: + fail-fast: false + matrix: + shard: [motd, push, other] + steps: + - uses: actions/checkout@v7 + with: + fetch-depth: 0 + persist-credentials: false + + - name: Verify test dependencies + run: | + command -v jq + command -v ip + command -v rsync + command -v ssh + + - name: Verify checkout and real Git selection + shell: bash + run: | + git config --global --add safe.directory "$GITHUB_WORKSPACE" + test "$(git rev-parse --show-toplevel)" = "$GITHUB_WORKSPACE" + git rev-parse --verify HEAD + bash tests/test-selection-git.sh + - name: Run tests env: TEST_BASE_SHA: ${{ github.event.pull_request.base.sha }} TEST_HEAD_SHA: ${{ github.event.pull_request.head.sha }} - run: bash tests/run.sh + TEST_SHARD: ${{ matrix.shard }} + run: bash tests/run.sh "$TEST_SHARD" - debian-13: - name: Debian 13 tests + debian-tests: + name: Debian 13 / ${{ matrix.shard }} runs-on: ubuntu-24.04 container: debian:13-slim timeout-minutes: 15 + strategy: + fail-fast: false + matrix: + shard: [motd, push, other] steps: - name: Install test dependencies run: | @@ -103,4 +124,31 @@ jobs: env: TEST_BASE_SHA: ${{ github.event.pull_request.base.sha }} TEST_HEAD_SHA: ${{ github.event.pull_request.head.sha }} - run: bash tests/run.sh + TEST_SHARD: ${{ matrix.shard }} + run: bash tests/run.sh "$TEST_SHARD" + + # Preserve existing required-check names; skipped/cancelled dependencies fail closed. + ubuntu: + name: Ubuntu 24.04 lint and tests + if: ${{ always() }} + needs: [lint, ubuntu-tests] + runs-on: ubuntu-24.04 + timeout-minutes: 2 + steps: + - name: Require lint and every Ubuntu shard + env: + LINT_RESULT: ${{ needs.lint.result }} + TEST_RESULT: ${{ needs.ubuntu-tests.result }} + run: test "$LINT_RESULT" = success && test "$TEST_RESULT" = success + + debian-13: + name: Debian 13 tests + if: ${{ always() }} + needs: [debian-tests] + runs-on: ubuntu-24.04 + timeout-minutes: 2 + steps: + - name: Require every Debian shard + env: + TEST_RESULT: ${{ needs.debian-tests.result }} + run: test "$TEST_RESULT" = success diff --git a/tests/run.sh b/tests/run.sh index 8d97553..dbb2aa4 100644 --- a/tests/run.sh +++ b/tests/run.sh @@ -3,6 +3,17 @@ set -euo pipefail readonly ROOT_DIR=$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd) +# Positional, not exported: nested selection fixtures must still run all suites. +shard=${1:-all} +if (( $# > 1 )); then + printf 'Usage: tests/run.sh [all|motd|push|other]\n' >&2 + exit 2 +fi +case "$shard" in + all|motd|push|other) ;; + *) printf 'Invalid test shard: %s\n' "$shard" >&2; exit 2 ;; +esac + declare -A selected=() full=true if [[ -n "${TEST_BASE_SHA:-}" && -n "${TEST_HEAD_SHA:-}" ]]; then @@ -38,6 +49,15 @@ for test_file in "$ROOT_DIR"/tests/test-*.sh; do continue fi test_name=$(basename "$test_file") + case "$test_name" in + test-motd.sh) suite_shard=motd ;; + test-push.sh|test-push-worker-registration.sh) suite_shard=push ;; + *) suite_shard=other ;; + esac + if [[ "$shard" != all && "$shard" != "$suite_shard" ]]; then + printf 'SKIP: %s (assigned to %s shard)\n' "$test_name" "$suite_shard" + continue + fi printf '\n==> %s\n' "$test_name" started=$SECONDS # Keep the child shell standalone: an if/|| wrapper can change errexit behavior. diff --git a/tests/test-shards.sh b/tests/test-shards.sh new file mode 100644 index 0000000..598fd82 --- /dev/null +++ b/tests/test-shards.sh @@ -0,0 +1,65 @@ +#!/usr/bin/env bash +set -euo pipefail +# Inert fixtures only: verify partitioning without running production suites. +ROOT_DIR=$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd) +TEMP_DIR=$(mktemp -d) +trap 'rm -rf "$TEMP_DIR"' EXIT +mkdir -p "$TEMP_DIR/tests" "$TEMP_DIR/bin" +cp "$ROOT_DIR/tests/run.sh" "$TEMP_DIR/tests/run.sh" +export CAPTURE="$TEMP_DIR/capture" GITHUB_STEP_SUMMARY="$TEMP_DIR/summary" +unset TEST_BASE_SHA TEST_HEAD_SHA +for name in motd push push-worker-registration new-suite; do + printf '#!/usr/bin/env bash\nprintf "%%s\\n" "%s" >> "$CAPTURE"\n' "test-$name.sh" > "$TEMP_DIR/tests/test-$name.sh" +done +check() { + local expected=$1 + shift + : > "$CAPTURE" + bash "$TEMP_DIR/tests/run.sh" "$@" > "$TEMP_DIR/output" 2>&1 + [[ "$(cat "$CAPTURE")" == "$expected" ]] || { cat "$TEMP_DIR/output"; exit 1; } +} +all=$'test-motd.sh\ntest-new-suite.sh\ntest-push-worker-registration.sh\ntest-push.sh' +check "$all" +check "$all" all +check test-motd.sh motd +check $'test-push-worker-registration.sh\ntest-push.sh' push +check test-new-suite.sh other +# Every current and future suite belongs to exactly one shard. +: > "$TEMP_DIR/union" +for shard in motd push other; do + : > "$CAPTURE" + bash "$TEMP_DIR/tests/run.sh" "$shard" > "$TEMP_DIR/output" 2>&1 + cat "$CAPTURE" >> "$TEMP_DIR/union" +done +[[ "$(sort "$TEMP_DIR/union")" == "$all" ]] +for args in invalid extra; do + : > "$CAPTURE" + rc=0 + if [[ "$args" == extra ]]; then + bash "$TEMP_DIR/tests/run.sh" all extra > "$TEMP_DIR/output" 2>&1 || rc=$? + else + bash "$TEMP_DIR/tests/run.sh" invalid > "$TEMP_DIR/output" 2>&1 || rc=$? + fi + [[ "$rc" == 2 && ! -s "$CAPTURE" ]] +done +# PR selection intersects the shard; unavailable history still covers all shards. +cat > "$TEMP_DIR/bin/git" <<'STUB' +#!/usr/bin/env bash +[[ "${DIFF_FAIL:-0}" == 0 ]] || exit 1 +printf 'tools/push.sh\0' +STUB +chmod +x "$TEMP_DIR/bin/git" +export PATH="$TEMP_DIR/bin:$PATH" TEST_BASE_SHA=base TEST_HEAD_SHA=head +check '' motd +check '' other +check $'test-push-worker-registration.sh\ntest-push.sh' push +DIFF_FAIL=1 check test-motd.sh motd +DIFF_FAIL=1 check test-new-suite.sh other +# Child errexit and failure propagation must survive sharding. +printf '#!/usr/bin/env bash\nset -e\nfalse\nprintf unexpected >> "$CAPTURE"\n' > "$TEMP_DIR/tests/test-push-worker-registration.sh" +: > "$CAPTURE" +rc=0 +bash "$TEMP_DIR/tests/run.sh" push > "$TEMP_DIR/output" 2>&1 || rc=$? +[[ "$rc" == 1 && ! -s "$CAPTURE" ]] +grep -q 'exit=1' "$TEMP_DIR/output" +printf 'All shard fixture tests passed.\n'