fix(ci): run dependency PR file/git ops inside a clone of the target repo
CI / validate (pull_request) Failing after 1m10s
CI / auto-merge (pull_request) Skipped

This commit is contained in:
Emil Simeonov
2026-09-19 04:22:11 +02:00
parent 0fc1d82365
commit 2bdb7542fc
3 changed files with 49 additions and 18 deletions
+13 -5
View File
@@ -17,11 +17,19 @@ in a JSON manifest. Two gaps in devx block that:
## Approach ## Approach
REQ-1: `create_dependency_pr` gains `--manifest <path>` + REQ-1: `create_dependency_pr` gains `--manifest <path>` +
`--verify-container <owner/name>`: before opening the PR it resolves the `--verify-container <owner/name>` + `--container-tag` +
container tag's OCI digest via the registry `manifests` API `--source-ref`: before opening the PR it resolves the container tag's
(`Docker-Content-Digest`), then updates manifest fields OCI digest via the packages API (`manifest.json` blob sha256), then
`{version, git_ref, image_tag, image_digest, source_commit, updated_at}` updates manifest fields `{version, git_ref, image_tag, image_digest,
in the PR branch instead of a regex bump. source_run_id, updated_at}` in the PR branch instead of a regex bump.
`--container-tag` decouples the image tag from the release version
(sso-bridge images tag `__init__.py.__version__`, not the git tag).
REQ-1b: `create_dependency_pr` clones the *target* repo into a tempdir
and performs all file lookups and git operations inside it. Previously
it operated on CWD — the producer repo's own checkout — so file updates
silently targeted the wrong repo and the whole dep-PR path no-oped
behind `|| echo warning`.
REQ-2: `clean_images` gains `--protect` (repeatable): named versions are REQ-2: `clean_images` gains `--protect` (repeatable): named versions are
never deleted regardless of `--keep` trimming. never deleted regardless of `--keep` trimming.
+29 -13
View File
@@ -21,7 +21,7 @@ from __future__ import annotations
import re import re
import subprocess # nosec B404 import subprocess # nosec B404
from datetime import UTC from datetime import UTC, datetime
from pathlib import Path from pathlib import Path
import click import click
@@ -282,15 +282,29 @@ def cli(
) )
) )
# Clone the target repo — this tool runs from the *producer* repo's CI,
# so every file lookup and git operation must happen in a clone of the
# target repo, not the producer checkout in CWD.
import tempfile
workdir = Path(tempfile.mkdtemp(prefix="dep-pr-"))
clone_url = f"{GITEA_API_URL.removesuffix('/api/v1')}/{repo}.git"
auth_cfg = f"http.extraHeader=Authorization: token {token}"
subprocess.run( # nosec B603 B607
["git", "-c", auth_cfg, "clone", "--depth", "50", clone_url, str(workdir)],
check=True,
capture_output=True,
)
# Find current pinned version # Find current pinned version
old_version = None old_version = None
changed_file = None changed_file = None
if manifest_path: if manifest_path:
old_version = read_manifest_version(manifest_path, package) old_version = read_manifest_version(str(workdir / manifest_path), package)
changed_file = manifest_path if old_version else None changed_file = manifest_path if old_version else None
else: else:
for f in [PYPROJECT_PATH, IMAGES_YML_PATH, ROLE_DEFAULTS_PATH]: for f in [PYPROJECT_PATH, IMAGES_YML_PATH, ROLE_DEFAULTS_PATH]:
old_version = find_pinned_version(package, f) old_version = find_pinned_version(package, str(workdir / f))
if old_version: if old_version:
changed_file = f changed_file = f
break break
@@ -342,13 +356,11 @@ def cli(
else: else:
raise click.ClickException(_("Failed to create branch: {error}", error=str(e))) from None raise click.ClickException(_("Failed to create branch: {error}", error=str(e))) from None
# Clone, update file, commit, push # Check out the API-created branch inside the target clone.
subprocess.run(["git", "fetch", "origin", f"{branch_name}"], check=False, capture_output=True) # nosec B603 B607 subprocess.run(["git", "fetch", "origin", f"{branch_name}"], check=False, capture_output=True, cwd=workdir) # nosec B603 B607
subprocess.run(["git", "checkout", branch_name], check=False, capture_output=True) # nosec B603 B607 subprocess.run(["git", "checkout", branch_name], check=False, capture_output=True, cwd=workdir) # nosec B603 B607
if manifest_path: if manifest_path:
from datetime import datetime
fields = { fields = {
"version": new_version, "version": new_version,
"git_ref": source_ref or f"v{new_version}", "git_ref": source_ref or f"v{new_version}",
@@ -359,16 +371,20 @@ def cli(
fields["image_digest"] = image_digest fields["image_digest"] = image_digest
if source_run_id: if source_run_id:
fields["source_run_id"] = source_run_id fields["source_run_id"] = source_run_id
if not update_manifest(manifest_path, package, fields): if not update_manifest(str(workdir / manifest_path), package, fields):
raise click.ClickException(_("Failed to update {file}", file=manifest_path)) raise click.ClickException(_("Failed to update {file}", file=manifest_path))
elif not changed_file or not update_pinned_version(changed_file, package, old_version, new_version): elif not changed_file or not update_pinned_version(str(workdir / changed_file), package, old_version, new_version):
raise click.ClickException(_("Failed to update {file}", file=changed_file)) raise click.ClickException(_("Failed to update {file}", file=changed_file))
assert changed_file is not None # nosec B101 — narrowed by the early exit above assert changed_file is not None # nosec B101 — narrowed by the early exit above
subprocess.run(["git", "add", changed_file], check=True) # nosec B603 B607 subprocess.run(["git", "add", changed_file], check=True, cwd=workdir) # nosec B603 B607
commit_msg = f"deps: bump {package} from {old_version} to {new_version}" commit_msg = f"deps: bump {package} from {old_version} to {new_version}"
subprocess.run(["git", "commit", "-m", commit_msg], check=True) # nosec B603 B607 subprocess.run(["git", "commit", "-m", commit_msg], check=True, cwd=workdir) # nosec B603 B607
subprocess.run(["git", "push", "origin", branch_name], check=True) # nosec B603 B607 subprocess.run( # nosec B603 B607
["git", "-c", auth_cfg, "push", "origin", branch_name],
check=True,
cwd=workdir,
)
# Create Vikunja task for tracking # Create Vikunja task for tracking
task_title = f"Bump {package} to {new_version}" task_title = f"Bump {package} to {new_version}"
+7
View File
@@ -16,6 +16,13 @@ from devx.ci.create_dependency_pr import (
) )
@pytest.fixture(autouse=True)
def _mock_subprocess() -> MagicMock:
"""Mock subprocess so CLI tests never run a real git clone."""
with patch("devx.ci.create_dependency_pr.subprocess.run") as m:
yield m
class TestFindPinnedVersion: class TestFindPinnedVersion:
def test_finds_pip_git_pin(self, tmp_path: Path) -> None: def test_finds_pip_git_pin(self, tmp_path: Path) -> None:
content = "grm @ git+https://git.example.com/repo.git@v0.5.1" content = "grm @ git+https://git.example.com/repo.git@v0.5.1"