diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index bb4c0c2..3a63331 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -35,10 +35,30 @@ jobs: - name: Setup Bazelisk run: go install github.com/bazelbuild/bazelisk@latest && export PATH=$PATH:$(go env GOPATH)/bin - uses: actions/checkout@v4 + - name: Free disk space on Ubuntu + if: runner.os == 'Linux' + run: | + # //cli:E2ETest plus coverage can exhaust the default ubuntu-latest image. + # Reclaim space from preinstalled toolchains this job does not use. + # Keep /usr/local/lib/android: rules_android resolves ANDROID_HOME there. + sudo rm -rf /usr/share/dotnet /opt/ghc + sudo docker image prune --all --force || true + # /mnt has more headroom than / on hosted runners; keep Bazel outputs off root. + if [ -d /mnt ]; then + sudo mkdir -p /mnt/bazel-output + sudo chown "$USER" /mnt/bazel-output + echo "BAZEL_OUTPUT_USER_ROOT=/mnt/bazel-output" >> "$GITHUB_ENV" + fi + df -h / /mnt 2>/dev/null || df -h / - name: Run bazel-diff tests env: USE_BAZEL_VERSION: ${{ matrix.bazel }} - run: ~/go/bin/bazelisk coverage --combined_report=lcov //cli/... //tools:coverage_check_test //tools/go/... --enable_bzlmod=true --enable_workspace=false + run: | + BAZEL_STARTUP=() + if [ -n "${BAZEL_OUTPUT_USER_ROOT:-}" ]; then + BAZEL_STARTUP=(--output_user_root="${BAZEL_OUTPUT_USER_ROOT}") + fi + ~/go/bin/bazelisk "${BAZEL_STARTUP[@]}" coverage --combined_report=lcov //cli/... //tools:coverage_check_test //tools/go/... --enable_bzlmod=true --enable_workspace=false - name: Upload coverage report uses: actions/upload-artifact@v4 if: always() @@ -54,7 +74,12 @@ jobs: # (seen flaking on macos-latest x bazel 9.x). USE_BAZEL_VERSION: ${{ matrix.bazel }} COVERAGE_THRESHOLD: '90' - run: ~/go/bin/bazelisk run //tools:coverage-check -- --badge-json coverage.json bazel-out/_coverage/_coverage_report.dat + run: | + BAZEL_STARTUP=() + if [ -n "${BAZEL_OUTPUT_USER_ROOT:-}" ]; then + BAZEL_STARTUP=(--output_user_root="${BAZEL_OUTPUT_USER_ROOT}") + fi + ~/go/bin/bazelisk "${BAZEL_STARTUP[@]}" run //tools:coverage-check -- --badge-json coverage.json bazel-out/_coverage/_coverage_report.dat - name: Enforce Go coverage threshold (>= 90% line coverage under tools/go/) env: # Must match the `Run bazel-diff tests` step above; see the note on the @@ -63,7 +88,12 @@ jobs: # Gate Go independently of the Kotlin overall so thin Go coverage can't hide # behind well-covered Kotlin (and vice versa). --include scopes the report to # Go production source only (tools/go/, which excludes tools/coverage_check.py). - run: ~/go/bin/bazelisk run //tools:coverage-check -- --include tools/go/ --threshold 90 bazel-out/_coverage/_coverage_report.dat + run: | + BAZEL_STARTUP=() + if [ -n "${BAZEL_OUTPUT_USER_ROOT:-}" ]; then + BAZEL_STARTUP=(--output_user_root="${BAZEL_OUTPUT_USER_ROOT}") + fi + ~/go/bin/bazelisk "${BAZEL_STARTUP[@]}" run //tools:coverage-check -- --include tools/go/ --threshold 90 bazel-out/_coverage/_coverage_report.dat - name: Upload test logs uses: actions/upload-artifact@v4 if: always() diff --git a/cli/src/main/kotlin/com/bazel_diff/server/GitClient.kt b/cli/src/main/kotlin/com/bazel_diff/server/GitClient.kt index 541f7c3..94c1772 100644 --- a/cli/src/main/kotlin/com/bazel_diff/server/GitClient.kt +++ b/cli/src/main/kotlin/com/bazel_diff/server/GitClient.kt @@ -68,6 +68,9 @@ interface GitClient { fun checkout(revision: String) } +/** A full 40-char lowercase hex commit SHA as printed by `git rev-parse`. */ +private val COMMIT_SHA = Regex("^[0-9a-f]{40}$") + /** [GitClient] backed by the `git` binary at [gitPath], run inside [workspacePath]. */ class ProcessGitClient( private val workspacePath: Path, @@ -121,10 +124,17 @@ class ProcessGitClient( } catch (e: GitClientException) { throw MissingRevisionException(revision, e) } - return output.firstOrNull()?.trim()?.takeIf { it.isNotEmpty() } - ?: throw MissingRevisionException(revision) + return parseCommitShaOutput(output) ?: throw MissingRevisionException(revision) } + /** + * Picks the commit SHA out of `git rev-parse` stdout. Git may concurrently write informational + * messages to stderr (e.g. "Auto packing the repository in background for optimum performance."); + * [run] captures stdout only, but matching the SHA shape is an extra guard. + */ + private fun parseCommitShaOutput(output: List): String? = + output.asSequence().map { it.trim() }.firstOrNull { COMMIT_SHA.matches(it) } + override fun checkout(revision: String) { logger.i { "git checkout $revision in $workspacePath" } run("checkout", "--force", revision) @@ -136,17 +146,22 @@ class ProcessGitClient( val result = runBlocking { process( *cmd.toTypedArray(), - // Merge stdout+stderr so a failure's diagnostics are captured in one place. + // Capture stdout only: git writes informational messages (e.g. background auto-pack) to + // stderr that must not be parsed as command output. Failures still surface on stderr via + // PRINT so they appear in the service logs. stdout = Redirect.CAPTURE, - stderr = Redirect.CAPTURE, + stderr = Redirect.PRINT, workingDirectory = workspacePath.toFile(), destroyForcibly = true, ) } if (result.resultCode != 0) { + val stdout = result.output.joinToString("\n").trim() throw GitClientException( - "git ${args.joinToString(" ")} failed (exit ${result.resultCode}): " + - result.output.joinToString("\n")) + buildString { + append("git ${args.joinToString(" ")} failed (exit ${result.resultCode})") + if (stdout.isNotEmpty()) append(": ").append(stdout) + }) } return result.output }