Skip to content

Commit f5f4844

Browse files
codexByron
authored andcommitted
ci(downstream): test releases with CLI and Gix backends
The downstream runner always installed plain `GitPython` into a fresh virtual environment, so starting it from a Gix-enabled environment still exercised CLI. Add `--backend {cli,gix}`, retaining CLI as the default, and pass the selection to tests and subprocesses through `GIT_PYTHON_BACKEND`. Verify the imported checkout, selected backend and `gix.testtools` before running downstream tests. Retain backend identity in result metadata and expand the five-project CI matrix to ten jobs. Both environments install GixPython 0.2.0 for fixture tooling. Move the local SWE-bench checks' initial two-commit repositories and URL-rewrite configuration into a Bash fixture; execute it at its final location because configuration contains absolute fixture paths. Preserve tested clone/reset behavior and best-effort cleanup. External upstream test files stay unchanged. Prepare Rust and cache Cargo outputs in CI for source builds. The four SWE-bench object/ref-format combinations passed with CLI and Gix on the completed stack. Ruff lint/format, ShellCheck, actionlint and whitespace checks passed at this commit. Other downstream releases and hosted CI were not rerun for this fixture migration.
1 parent 35ac3c6 commit f5f4844

5 files changed

Lines changed: 99 additions & 48 deletions

File tree

‎.github/workflows/downstream.yml‎

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ jobs:
1717
fail-fast: false
1818
matrix:
1919
project: [langchain, mlflow, bandit, swebench, datahub]
20+
backend: [gix, cli]
2021
steps:
2122
- uses: actions/checkout@v7
2223
with:
@@ -28,10 +29,23 @@ jobs:
2829
sudo apt-get update
2930
sudo apt-get install --yes git
3031
fi
32+
- name: Prepare Rust for GixPython fixture tooling
33+
run: |
34+
rustup update stable
35+
rustup default stable
36+
echo "CARGO_TARGET_DIR=$RUNNER_TEMP/gix-target" >> "$GITHUB_ENV"
37+
- name: Cache GixPython builds
38+
uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
39+
with:
40+
path: |
41+
~/.cargo/registry
42+
~/.cargo/git
43+
${{ runner.temp }}/gix-target
44+
key: downstream-gix-${{ runner.os }}-${{ hashFiles('gix-requirements.txt') }}
3145
- name: Install uv
3246
uses: astral-sh/setup-uv@c18668ad3cf93ea998bef934396af7bb5c839dc7 # v10.2.0
3347
with:
3448
version: '0.12.13'
3549
enable-cache: false
3650
- name: Test latest release against this checkout
37-
run: uv run test/downstream/run.py "${{ matrix.project }}"
51+
run: uv run test/downstream/run.py "${{ matrix.project }}" --backend "${{ matrix.backend }}"

‎test/downstream/README.md‎

Lines changed: 20 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -5,16 +5,25 @@ Run released projects' GitPython-related tests against this checkout with
55

66
```sh
77
uv run test/downstream/run.py bandit
8+
uv run test/downstream/run.py bandit --backend gix
89
uv run test/downstream/run.py bandit --version 1.9.4
9-
for project in langchain mlflow bandit swebench datahub; do
10-
uv run test/downstream/run.py "$project"
10+
for backend in gix cli; do
11+
for project in langchain mlflow bandit swebench datahub; do
12+
uv run test/downstream/run.py "$project" --backend "$backend"
13+
done
1114
done
1215
```
1316

1417
The default resolves the latest release from PyPI each time. `--version` reproduces
15-
a release; `--python` selects the test interpreter (default: 3.12). The runner
16-
creates a fresh environment, replaces GitPython with this editable checkout, and checks
17-
the imported module before testing. Upstream test files are not modified.
18+
a release; `--python` selects the test interpreter (default: 3.12), including an
19+
existing interpreter's path. `--backend` selects `cli` (the default) or `gix`.
20+
The runner creates a fresh environment and installs this editable checkout,
21+
including the official GixPython release for `gix.testtools` on both backends.
22+
It sets `GIT_PYTHON_BACKEND` from `--backend` and verifies
23+
both the imported checkout and the selected backend before testing, and records
24+
the backend in `result.json` and the default work-directory name. Starting the
25+
runner from a Gix environment alone does not select Gix in the isolated test
26+
environment. Upstream test files are not modified.
1827

1928
Source, the environment, frozen requirements, JUnit results, and release provenance
2029
are retained under `.cache/downstream/`. `--work-dir PATH` uses a new directory
@@ -35,7 +44,7 @@ and test-only dependencies are excluded. Downloads are not unique installations.
3544
| Project | Distribution downloads | Last tested release | Selected coverage |
3645
| --- | ---: | --- | --- |
3746
| LangChain Community | 27,882,881 | 0.4.2 | 2 upstream GitLoader tests: real clones, commits, checkout, tree traversal, ignored paths, and remote validation |
38-
| MLflow (`mlflow-skinny`) | 25,850,354 | 3.16.1 | 47 upstream tests: 31 repository/project/model-versioning cases plus 16 Git context and credential-redaction contract cases |
47+
| MLflow (`mlflow-skinny`) | 25,850,354 | 3.17.0 | 47 upstream tests: 31 repository/project/model-versioning cases plus 16 Git context and credential-redaction contract cases |
3948
| Bandit | 24,935,372 | 1.9.4 | 12 upstream baseline CLI tests: real repository creation, commits, branches, resets, discovery, and dirty state |
4049
| SWE-bench | 22,942,741 | 5.0.2 | 4 supplemental integration cases for `AutoContextManager`; no upstream tests cover its GitPython callers |
4150
| DataHub (`acryl-datahub`) | 5,019,402 | 1.7.0.14 | 7 upstream tests passed, 1 credential-dependent skip: public clone/checkout, SSH timeout, exception/redaction contracts, and configuration |
@@ -94,6 +103,8 @@ The private SSH-clone test retains its upstream skip: the runner removes its
94103
credential variable and needs no private credentials. `ssh`, public GitLab
95104
access, and local TCP sockets are needed for the selected tests.
96105

97-
CI runs the same command against the latest release and fails when no test passes,
98-
including when all selected tests are skipped. Test dependency ranges only supply
99-
the upstream test harness; they do not pin the dependent's release.
106+
CI runs all five projects against the latest release with both CLI and Gix
107+
backends, for ten independent jobs. A backend mismatch or a run in which no
108+
test passes fails the job, including when all selected tests are skipped.
109+
Test dependency ranges only supply the upstream test harness; they do not pin
110+
the dependent's release.

‎test/downstream/checks/swebench.py‎

Lines changed: 27 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -1,60 +1,54 @@
11
"""Supplemental checks: SWE-bench 5.0.2 ships no tests for its GitPython callers.
22
33
Import the ordinary upstream utility module. The minimal environment only needs
4-
chardet and GitPython; no SWE-bench code or imported module is replaced.
4+
chardet, GitPython and GixPython fixtures; no SWE-bench code or imported module is replaced.
55
"""
66

7-
import subprocess
7+
import logging
88
from pathlib import Path
99

1010
import pytest
11+
from gix import Error, testtools
1112
from git import Repo
1213
from swebench.inference.make_datasets.utils import (
1314
AutoContextManager,
1415
ingest_directory_contents,
1516
)
1617

1718

19+
@pytest.fixture
20+
def checkout_fixture(object_format, ref_format):
21+
script = Path(__file__).resolve().parents[3] / "tests/fixtures/downstream-swebench.sh"
22+
# The URL rewrite embeds the private fixture's absolute origin path.
23+
try:
24+
fixture = testtools.scripted_fixture_writable_with_args(
25+
script, [object_format, ref_format], testtools.Creation.Execute
26+
)
27+
except Error:
28+
raise RuntimeError("SWE-bench fixture script failed; see its captured output") from None
29+
try:
30+
yield fixture.path().resolve()
31+
finally:
32+
try:
33+
fixture.close()
34+
except Error:
35+
logging.getLogger(__name__).warning("Could not completely clean up SWE-bench fixture")
36+
37+
1838
@pytest.mark.parametrize("object_format", ["sha1", "sha256"])
1939
@pytest.mark.parametrize("ref_format", ["files", "reftable"])
20-
def test_auto_context_manager_clones_and_resets(tmp_path, monkeypatch, object_format, ref_format):
40+
def test_auto_context_manager_clones_and_resets(checkout_fixture, monkeypatch, object_format, ref_format):
2141
"""Clone, revisit commits and reuse the checkout with real GitPython."""
42+
tmp_path = checkout_fixture
2243
origin = tmp_path / "origin"
23-
origin.mkdir()
24-
config = tmp_path / "gitconfig"
25-
config.write_text("[user]\n name = GitPython Compatibility\n email = tests@example.invalid\n")
26-
monkeypatch.setenv("GIT_CONFIG_GLOBAL", str(config))
44+
monkeypatch.setenv("GIT_CONFIG_GLOBAL", str(tmp_path / "gitconfig"))
2745
monkeypatch.setenv("GIT_CONFIG_NOSYSTEM", "1")
2846
monkeypatch.setenv("GIT_DEFAULT_REF_FORMAT", ref_format)
2947
monkeypatch.setenv("GIT_ALLOW_PROTOCOL", "file")
3048
monkeypatch.setenv("GIT_TERMINAL_PROMPT", "0")
31-
32-
def git(*args):
33-
return subprocess.run(
34-
["git", "-C", str(origin), *args], check=True, text=True, capture_output=True
35-
).stdout.strip()
36-
37-
git("init", "-b", "main", f"--object-format={object_format}", f"--ref-format={ref_format}")
38-
(origin / "README.md").write_text("Local fixture\n")
39-
(origin / "module.py").write_text("value = 1\n")
40-
git("add", "--", "README.md", "module.py")
41-
git("commit", "-m", "initial")
42-
first_commit = git("rev-parse", "HEAD")
43-
(origin / "module.py").write_text("value = 2\n")
44-
git("commit", "-am", "second")
45-
second_commit = git("rev-parse", "HEAD")
46-
47-
# Preserve SWE-bench's ordinary HTTPS URL construction while routing the
48-
# transport to our local fixture. No network access or credentials are used.
49-
git(
50-
"config",
51-
"--global",
52-
f"url.{origin.as_uri()}.insteadOf",
53-
"https://gitpython-tests@github.com/swe-bench-repos/fixture__project.git",
54-
)
55-
git("config", "--global", "protocol.file.allow", "always")
49+
first_commit = (tmp_path / "first-commit").read_text().strip()
50+
second_commit = (tmp_path / "second-commit").read_text().strip()
5651
root = tmp_path / "checkouts"
57-
root.mkdir()
5852
instance = {"repo": "fixture/project", "base_commit": first_commit}
5953
original_cwd = Path.cwd()
6054
manager = AutoContextManager(instance, root_dir=str(root), token="gitpython-tests")

‎test/downstream/run.py‎

Lines changed: 17 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,7 @@ def main():
8888
profiles = json.loads((HERE / "projects.json").read_text())
8989
parser = argparse.ArgumentParser(description=__doc__)
9090
parser.add_argument("project", choices=profiles)
91+
parser.add_argument("--backend", choices=("cli", "gix"), default="cli", help="Backend to install and verify")
9192
parser.add_argument("--version", help="Release to reproduce; defaults to PyPI's latest release")
9293
parser.add_argument("--python", default="3.12", help="Python used by the isolated test environment")
9394
parser.add_argument("--work-dir", type=Path, help="New directory for retained source, environment, and results")
@@ -100,8 +101,8 @@ def main():
100101
else:
101102
cache = CHECKOUT / ".cache" / "downstream"
102103
cache.mkdir(parents=True, exist_ok=True)
103-
work = Path(tempfile.mkdtemp(prefix=f"{args.project}-{version}-", dir=cache))
104-
print(f"Testing {args.project} {version}; retained work directory: {work}", flush=True)
104+
work = Path(tempfile.mkdtemp(prefix=f"{args.project}-{version}-{args.backend}-", dir=cache))
105+
print(f"Testing {args.project} {version} with {args.backend}; retained work directory: {work}", flush=True)
105106

106107
config = work / "gitconfig"
107108
config.write_text(
@@ -122,6 +123,7 @@ def main():
122123
python = bin_dir / ("python.exe" if os.name == "nt" else "python")
123124
env["PATH"] = str(bin_dir) + os.pathsep + env["PATH"]
124125
env["GITPYTHON_CHECKOUT"] = str(CHECKOUT)
126+
env["GIT_PYTHON_BACKEND"] = args.backend
125127
for key in profile.get("unset_env", []):
126128
env.pop(key, None)
127129
env.update(profile.get("env", {}))
@@ -134,25 +136,35 @@ def expand(value):
134136
if profile.get("no_deps"):
135137
run(install + ["--no-deps"] + list(map(expand, profile["no_deps"])), env=env)
136138
# Replace the released dependency even if the downstream pins another version.
137-
run(install + ["--reinstall-package", "gitpython", "-e", str(CHECKOUT)], env=env)
139+
requirement = str(CHECKOUT) + "[gix]"
140+
run(install + ["--reinstall-package", "gitpython", "-e", requirement], env=env)
138141

139142
# Check the import used by tests and by downstream Python subprocesses.
140143
run(
141144
[
142145
str(python),
143146
"-c",
144-
"import os, pathlib, git; "
147+
"import os, pathlib, sys, git; from git import _backend; from gix import testtools; "
145148
"expected = pathlib.Path(os.environ['GITPYTHON_CHECKOUT']) / 'git' / '__init__.py'; "
146149
"assert pathlib.Path(git.__file__).resolve() == expected.resolve(), git.__file__; "
150+
"print('GitPython backend:', _backend.name); "
151+
"assert _backend.name == sys.argv[1], f'Expected {sys.argv[1]} backend, got {_backend.name}'; "
147152
"print('GitPython:', git.__file__); print(git.Git().version()); "
148153
"assert git.Git().version_info >= (2, 52), 'Git 2.52 or newer is required'",
154+
args.backend,
149155
],
150156
cwd=source,
151157
env=env,
152158
)
153159
with (work / "requirements-frozen.txt").open("w") as output:
154160
run(["uv", "pip", "freeze", "--python", str(python)], env=env, stdout=output)
155-
result = {"project": args.project, "version": version, "source": provenance, "gitpython": str(CHECKOUT)}
161+
result = {
162+
"project": args.project,
163+
"version": version,
164+
"backend": args.backend,
165+
"source": provenance,
166+
"gitpython": str(CHECKOUT),
167+
}
156168
result_path = work / "result.json"
157169
result_path.write_text(json.dumps(result, indent=2) + "\n")
158170
report = work / "junit.xml"
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
#!/usr/bin/env bash
2+
set -eu
3+
export GIT_AUTHOR_NAME='GitPython Compatibility' GIT_AUTHOR_EMAIL='tests@example.invalid'
4+
export GIT_COMMITTER_NAME="$GIT_AUTHOR_NAME" GIT_COMMITTER_EMAIL="$GIT_AUTHOR_EMAIL"
5+
export GIT_AUTHOR_DATE='2001-01-01T00:00:00 +0000'
6+
export GIT_COMMITTER_DATE="$GIT_AUTHOR_DATE"
7+
git init -q -b main --object-format="$1" --ref-format="$2" origin
8+
printf 'Local fixture\n' > origin/README.md
9+
printf 'value = 1\n' > origin/module.py
10+
git -C origin add README.md module.py
11+
git -C origin commit -qm initial
12+
git -C origin rev-parse HEAD > first-commit
13+
printf 'value = 2\n' > origin/module.py
14+
git -C origin commit -qam second
15+
git -C origin rev-parse HEAD > second-commit
16+
git config --file gitconfig user.name "$GIT_AUTHOR_NAME"
17+
git config --file gitconfig user.email "$GIT_AUTHOR_EMAIL"
18+
git config --file gitconfig "url.$PWD/origin.insteadOf" https://gitpython-tests@github.com/swe-bench-repos/fixture__project.git
19+
git config --file gitconfig protocol.file.allow always
20+
mkdir checkouts

0 commit comments

Comments
 (0)