|
| 1 | +name: bughunt uv probe |
| 2 | +on: |
| 3 | + push: |
| 4 | + branches: ["bughunt/uv/**"] |
| 5 | +permissions: |
| 6 | + contents: read |
| 7 | +jobs: |
| 8 | + probe: |
| 9 | + strategy: |
| 10 | + fail-fast: false |
| 11 | + matrix: |
| 12 | + os: [ubuntu-latest, macos-latest, windows-latest] |
| 13 | + runs-on: ${{ matrix.os }} |
| 14 | + timeout-minutes: 45 |
| 15 | + defaults: |
| 16 | + run: |
| 17 | + shell: bash |
| 18 | + steps: |
| 19 | + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 |
| 20 | + - uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5 |
| 21 | + with: |
| 22 | + python-version: "3.11" |
| 23 | + - uses: Swatinem/rust-cache@c19371144df3bb44fab255c43d04cbc2ab54d1c4 # v2.9.1 |
| 24 | + - name: Build CLI |
| 25 | + run: cargo build --release -p socket-patch-cli |
| 26 | + - name: Write probe files |
| 27 | + run: | |
| 28 | + mkdir -p "$RUNNER_TEMP/mock" "$RUNNER_TEMP/work" |
| 29 | + cat > "$RUNNER_TEMP/mock/mock.py" <<'PYEOF' |
| 30 | + #!/usr/bin/env python3 |
| 31 | + """Local mock of the Socket patch API serving one patched pypi package (six 1.16.0).""" |
| 32 | + import base64, hashlib, io, json, os, re, sys, zipfile |
| 33 | + from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer |
| 34 | +
|
| 35 | + HERE = os.path.dirname(os.path.abspath(__file__)) |
| 36 | + PORT = int(os.environ.get("MOCK_PORT", "18080")) |
| 37 | + ORG = "test-org" |
| 38 | + UUID = "0f6e7c1a-1111-4222-8333-444455556666" |
| 39 | + TOKEN = "11111111-2222-4333-8444-555555555555" |
| 40 | + SUFFIX = b"\n# SOCKET-PATCHED\nSOCKET_PATCHED = 1\n" |
| 41 | + NAME, VER = "six", "1.16.0" |
| 42 | + PURL = f"pkg:pypi/{NAME}@{VER}" |
| 43 | + LEAF = f"{NAME}-{VER}-py2.py3-none-any.whl" |
| 44 | + LOG = os.path.join(HERE, "requests.log") |
| 45 | +
|
| 46 | +
|
| 47 | + def git_sha256(b): |
| 48 | + return hashlib.sha256(b"blob %d\0" % len(b) + b).hexdigest() |
| 49 | +
|
| 50 | +
|
| 51 | + def build(): |
| 52 | + src = zipfile.ZipFile(os.path.join(HERE, LEAF)) |
| 53 | + members = [] |
| 54 | + dist = f"{NAME}-{VER}.dist-info" |
| 55 | + for info in src.infolist(): |
| 56 | + if info.filename == f"{dist}/RECORD": |
| 57 | + continue |
| 58 | + data = src.read(info.filename) |
| 59 | + if info.filename == "six.py": |
| 60 | + orig = data |
| 61 | + data = data + SUFFIX |
| 62 | + members.append((info.filename, data)) |
| 63 | + rec = "" |
| 64 | + for n, d in members: |
| 65 | + dig = base64.urlsafe_b64encode(hashlib.sha256(d).digest()).rstrip(b"=").decode() |
| 66 | + rec += f"{n},sha256={dig},{len(d)}\n" |
| 67 | + rec += f"{dist}/RECORD,,\n" |
| 68 | + members.append((f"{dist}/RECORD", rec.encode())) |
| 69 | + buf = io.BytesIO() |
| 70 | + with zipfile.ZipFile(buf, "w", zipfile.ZIP_DEFLATED) as z: |
| 71 | + for n, d in members: |
| 72 | + zi = zipfile.ZipInfo(n, date_time=(2020, 1, 1, 0, 0, 0)); zi.compress_type = zipfile.ZIP_DEFLATED |
| 73 | + z.writestr(zi, d) |
| 74 | + return orig, orig + SUFFIX, buf.getvalue() |
| 75 | +
|
| 76 | +
|
| 77 | + ORIG, PATCHED, WHEEL = build() |
| 78 | + SHA = hashlib.sha256(WHEEL).hexdigest() |
| 79 | + BASE = f"http://127.0.0.1:{PORT}" |
| 80 | + ART_PATH = f"/patch/pypi/{NAME}/{VER}/{TOKEN}/{UUID}/{LEAF}" |
| 81 | + ART = BASE + ART_PATH |
| 82 | + VIEW = { |
| 83 | + "uuid": UUID, "purl": PURL, "publishedAt": "Fri, 27 Mar 2026 00:00:00 GMT", |
| 84 | + "files": {"six.py": {"beforeHash": git_sha256(ORIG), "afterHash": git_sha256(PATCHED), |
| 85 | + "blobContent": base64.b64encode(PATCHED).decode()}}, |
| 86 | + "vulnerabilities": {"GHSA-uvbu-ildv-ex01": {"cves": ["CVE-2026-7101"], "summary": "s", |
| 87 | + "severity": "high", "description": "d"}}, |
| 88 | + "description": "uv bughunt patch", "license": "MIT", "tier": "free", |
| 89 | + } |
| 90 | +
|
| 91 | +
|
| 92 | + class H(BaseHTTPRequestHandler): |
| 93 | + def log_message(self, *a): |
| 94 | + pass |
| 95 | +
|
| 96 | + def send(self, code, body, ctype="application/json"): |
| 97 | + if not isinstance(body, bytes): |
| 98 | + body = json.dumps(body).encode() |
| 99 | + self.send_response(code) |
| 100 | + self.send_header("Content-Type", ctype) |
| 101 | + self.send_header("Content-Length", str(len(body))) |
| 102 | + self.end_headers() |
| 103 | + self.wfile.write(body) |
| 104 | +
|
| 105 | + def route(self): |
| 106 | + with open(LOG, "a") as f: |
| 107 | + f.write(f"{self.command} {self.path}\n") |
| 108 | + p = self.path.split("?")[0] |
| 109 | + n = int(self.headers.get("Content-Length") or 0) |
| 110 | + body = self.rfile.read(n) if n else b"" |
| 111 | + if p == ART_PATH: |
| 112 | + if self.command == "HEAD": |
| 113 | + self.send_response(200); self.send_header("Content-Length", str(len(WHEEL))); self.end_headers(); return |
| 114 | + return self.send(200, WHEEL, "application/octet-stream") |
| 115 | + if re.fullmatch(rf"/v0/orgs/{ORG}/patches/batch", p) or p.endswith("/patch/batch"): |
| 116 | + try: |
| 117 | + comps = json.loads(body or b"{}").get("components", []) |
| 118 | + except Exception: |
| 119 | + comps = [] |
| 120 | + purls = [c.get("purl", "") for c in comps] if comps else [PURL] |
| 121 | + if not any(pu.lower().startswith(f"pkg:pypi/{NAME}@{VER}") for pu in purls): |
| 122 | + return self.send(200, {"packages": [], "canAccessPaidPatches": False}) |
| 123 | + return self.send(200, {"packages": [{"purl": PURL, "patches": [{ |
| 124 | + "uuid": UUID, "purl": PURL, "tier": "free", "cveIds": ["CVE-2026-7101"], |
| 125 | + "ghsaIds": ["GHSA-uvbu-ildv-ex01"], "severity": "HIGH", "title": "uv bughunt"}]}], |
| 126 | + "canAccessPaidPatches": False}) |
| 127 | + if "/patches/by-package/" in p or "/patch/by-package/" in p: |
| 128 | + return self.send(200, {"patches": [{"uuid": UUID, "purl": PURL, |
| 129 | + "publishedAt": "2026-09-01T00:00:00Z", "description": "uv bughunt", |
| 130 | + "license": "MIT", "tier": "free", "vulnerabilities": {}}], |
| 131 | + "canAccessPaidPatches": False}) |
| 132 | + if p.endswith("/patches/package") or p.endswith("/patch/package"): |
| 133 | + return self.send(200, {"results": {UUID: {"status": "granted", "url": ART, "purl": PURL, |
| 134 | + "artifacts": [{"kind": "tarball", "url": ART, "integrity": {"sha256": SHA, "sha512": "sha512-" + base64.b64encode(hashlib.sha512(WHEEL).digest()).decode()}}], |
| 135 | + "registryOverride": None}}}) |
| 136 | + if p.endswith(f"/view/{UUID}"): |
| 137 | + return self.send(200, VIEW) |
| 138 | + if "/blob/" in p: |
| 139 | + h = p.rsplit("/", 1)[1] |
| 140 | + if h == git_sha256(PATCHED): |
| 141 | + return self.send(200, PATCHED, "application/octet-stream") |
| 142 | + if h == git_sha256(ORIG): |
| 143 | + return self.send(200, ORIG, "application/octet-stream") |
| 144 | + return self.send(404, {"error": "not found", "path": p}) |
| 145 | +
|
| 146 | + do_GET = route |
| 147 | + do_POST = route |
| 148 | + do_HEAD = route |
| 149 | +
|
| 150 | +
|
| 151 | + if __name__ == "__main__": |
| 152 | + print(json.dumps({"art": ART, "sha": SHA, "patched_py_sha": hashlib.sha256(PATCHED).hexdigest(), |
| 153 | + "orig_py_sha": hashlib.sha256(ORIG).hexdigest()}), flush=True) |
| 154 | + ThreadingHTTPServer(("127.0.0.1", PORT), H).serve_forever() |
| 155 | + PYEOF |
| 156 | + cat > "$RUNNER_TEMP/driver_rb.py" <<'PYEOF' |
| 157 | + #!/usr/bin/env python3 |
| 158 | + """uv hosted rollback-after-edit probe. usage: driver.py SOCKET_PATCH UV WORKDIR""" |
| 159 | + import json, os, shutil, subprocess, sys |
| 160 | +
|
| 161 | + SP, UV, WORK = sys.argv[1], sys.argv[2], sys.argv[3] |
| 162 | + API = ["--api-url", "http://127.0.0.1:18080", "--api-token", "fake-token", "--org", "test-org", "--patch-server-url", "http://127.0.0.1:18080"] |
| 163 | + ENV = dict(os.environ, SOCKET_TELEMETRY_DISABLED="1", SOCKET_NO_CONFIG="1") |
| 164 | + PYPROJECT = '[project]\nname = "uvp"\nversion = "0.1.0"\nrequires-python = ">=3.9"\ndependencies = ["six==1.16.0", "idna==3.7"]\n' |
| 165 | +
|
| 166 | +
|
| 167 | + def run(cmd, cwd): |
| 168 | + return subprocess.run(cmd, cwd=cwd, env=ENV, capture_output=True, text=True, encoding="utf-8", errors="replace") |
| 169 | +
|
| 170 | +
|
| 171 | + def sp(args, cwd): |
| 172 | + p = run([SP] + args + API, cwd) |
| 173 | + try: |
| 174 | + return p.returncode, json.loads(p.stdout) |
| 175 | + except Exception: |
| 176 | + return p.returncode, {"raw": p.stdout[-500:], "stderr": p.stderr[-500:]} |
| 177 | +
|
| 178 | +
|
| 179 | + def hosted_refs(d): |
| 180 | + n = 0 |
| 181 | + for f in os.listdir(d): |
| 182 | + if f in ("pyproject.toml", "uv.lock") or f.endswith(".py.lock") or f.endswith(".py"): |
| 183 | + with open(os.path.join(d, f), encoding="utf-8") as fh: |
| 184 | + n += fh.read().count("127.0.0.1:18080") |
| 185 | + return n |
| 186 | +
|
| 187 | +
|
| 188 | + def case(name, mutate): |
| 189 | + d = os.path.join(WORK, name) |
| 190 | + shutil.rmtree(d, ignore_errors=True) |
| 191 | + os.makedirs(d) |
| 192 | + with open(os.path.join(d, "pyproject.toml"), "w", newline="\n") as f: |
| 193 | + f.write(PYPROJECT) |
| 194 | + for c in (["lock", "-q"], ["sync", "-q"]): |
| 195 | + p = run([UV] + c, d) |
| 196 | + if p.returncode: |
| 197 | + return {"case": name, "result": "setup-failed", "err": p.stderr[-400:]} |
| 198 | + rc, s1 = sp(["scan", "--mode", "hosted", "--json", "--yes"], d) |
| 199 | + red = s1.get("redirect", {}).get("redirected") |
| 200 | + mutate(d) |
| 201 | + rc, s2 = sp(["scan", "--mode", "hosted", "--json", "--yes"], d) |
| 202 | + rc, rb = sp(["rollback", "--json", "--yes"], d) |
| 203 | + failed = [f.get("error", "")[:400] for f in rb.get("hosted", {}).get("failed", [])] |
| 204 | + refs = hosted_refs(d) |
| 205 | + return {"case": name, "scanRedirected": red, "rollbackExit": rc, "rollbackStatus": rb.get("status"), |
| 206 | + "hostedFailed": failed, "hostedRefsLeft": refs, |
| 207 | + "result": "PASS" if rc == 0 and refs == 0 else "FAIL"} |
| 208 | +
|
| 209 | +
|
| 210 | + def none(d): |
| 211 | + pass |
| 212 | +
|
| 213 | +
|
| 214 | + def add_desc(d): |
| 215 | + p = os.path.join(d, "pyproject.toml") |
| 216 | + with open(p, encoding="utf-8") as f: |
| 217 | + t = f.read() |
| 218 | + with open(p, "w", newline="\n") as f: |
| 219 | + f.write(t.replace('version = "0.1.0"\n', 'version = "0.1.0"\ndescription = "hello"\n', 1)) |
| 220 | +
|
| 221 | +
|
| 222 | + def uv_add(d): |
| 223 | + p = run([UV, "add", "-q", "certifi==2024.2.2"], d) |
| 224 | + if p.returncode: |
| 225 | + print("uv add failed:", p.stderr[-300:]) |
| 226 | +
|
| 227 | +
|
| 228 | + results = [case("control-no-edit", none), case("pyproject-description", add_desc), case("uv-add-idna", uv_add)] |
| 229 | + out = subprocess.run([UV, "--version"], capture_output=True, text=True).stdout.strip() |
| 230 | + for r in results: |
| 231 | + r["uv"] = out |
| 232 | + print("RESULT " + json.dumps(r)) |
| 233 | + PYEOF |
| 234 | + cat > "$RUNNER_TEMP/driver_rep.py" <<'PYEOF' |
| 235 | + #!/usr/bin/env python3 |
| 236 | + """uv vendored repair probe (lock-only checkout, committed wheel deleted). usage: driver.py SOCKET_PATCH UV WORKDIR""" |
| 237 | + import glob, json, os, shutil, subprocess, sys, hashlib |
| 238 | +
|
| 239 | + SP, UV, WORK = sys.argv[1], sys.argv[2], sys.argv[3] |
| 240 | + API = ["--api-url", "http://127.0.0.1:18080", "--api-token", "fake-token", "--org", "test-org", "--patch-server-url", "http://127.0.0.1:18080"] |
| 241 | + ENV = dict(os.environ, SOCKET_TELEMETRY_DISABLED="1", SOCKET_NO_CONFIG="1") |
| 242 | + PYPROJECT = '[project]\nname = "uvp"\nversion = "0.1.0"\nrequires-python = ">=3.9"\ndependencies = ["six==1.16.0"]\n' |
| 243 | + PATCHED = "c0c1c71ca455" |
| 244 | +
|
| 245 | +
|
| 246 | + def run(cmd, cwd, **kw): |
| 247 | + return subprocess.run(cmd, cwd=cwd, env=kw.get("env", ENV), capture_output=True, text=True, encoding="utf-8", errors="replace") |
| 248 | +
|
| 249 | +
|
| 250 | + def sp(args, cwd): |
| 251 | + p = run([SP] + args + API, cwd) |
| 252 | + try: |
| 253 | + return p.returncode, json.loads(p.stdout) |
| 254 | + except Exception: |
| 255 | + return p.returncode, {"raw": p.stdout[-500:], "stderr": p.stderr[-500:]} |
| 256 | +
|
| 257 | +
|
| 258 | + def six_sha(d): |
| 259 | + hits = glob.glob(os.path.join(d, ".venv", "**", "six.py"), recursive=True) |
| 260 | + return hashlib.sha256(open(hits[0], "rb").read()).hexdigest()[:12] if hits else None |
| 261 | +
|
| 262 | +
|
| 263 | + def install(d, tag, *flags): |
| 264 | + shutil.rmtree(os.path.join(d, ".venv"), ignore_errors=True) |
| 265 | + env = dict(ENV, UV_CACHE_DIR=os.path.join(WORK, "cache-" + tag)) |
| 266 | + p = run([UV, "sync"] + list(flags), d, env=env) |
| 267 | + return {"rc": p.returncode, "six": six_sha(d), "err": p.stderr[-300:] if p.returncode else ""} |
| 268 | +
|
| 269 | +
|
| 270 | + def git(d, *a): |
| 271 | + return run(["git"] + list(a), d) |
| 272 | +
|
| 273 | +
|
| 274 | + shutil.rmtree(WORK, ignore_errors=True) |
| 275 | + proj = os.path.join(WORK, "proj") |
| 276 | + os.makedirs(proj) |
| 277 | + with open(os.path.join(proj, "pyproject.toml"), "w", newline="\n") as f: |
| 278 | + f.write(PYPROJECT) |
| 279 | + with open(os.path.join(proj, ".gitignore"), "w", newline="\n") as f: |
| 280 | + f.write(".venv\n") |
| 281 | + r = {} |
| 282 | + for c in (["lock", "-q"], ["sync", "-q"]): |
| 283 | + run([UV] + c, proj) |
| 284 | + rc, s = sp(["scan", "--mode", "vendored", "--json", "--yes"], proj) |
| 285 | + r["vendorScan"] = [rc, s.get("vendor", {}).get("summary", {}).get("applied")] |
| 286 | + git(proj, "init", "-q", ".") |
| 287 | + git(proj, "add", "-A") |
| 288 | + git(proj, "commit", "-qm", "vendored") |
| 289 | + r["committed"] = git(proj, "ls-files", ".socket").stdout.split() |
| 290 | + def repair_case(tag, with_venv): |
| 291 | + clone = os.path.join(WORK, "clone-" + tag) |
| 292 | + git(WORK, "clone", "-q", proj, clone) |
| 293 | + c = {} |
| 294 | + if with_venv: |
| 295 | + c["preInstall"] = install(clone, tag + "0", "--locked") |
| 296 | + wheels = glob.glob(os.path.join(clone, ".socket", "vendor", "pypi", "*", "*.whl")) |
| 297 | + for w in wheels: |
| 298 | + os.remove(w) |
| 299 | + rc, rep = sp(["repair", "--json"], clone) |
| 300 | + c["repair"] = {"rc": rc, "status": rep.get("status"), |
| 301 | + "events": [(e.get("action"), e.get("errorCode"), (e.get("error") or "")[:260]) for e in rep.get("events", [])]} |
| 302 | + c["wheelBack"] = bool(glob.glob(os.path.join(clone, ".socket", "vendor", "pypi", "*", "*.whl"))) |
| 303 | + c["afterRepairLocked"] = install(clone, tag + "1", "--locked") |
| 304 | + c["gitStatus"] = git(clone, "status", "--short").stdout.strip().splitlines() |
| 305 | + ok = rc == 0 and c["wheelBack"] and c["afterRepairLocked"]["six"] == PATCHED and c["afterRepairLocked"]["rc"] == 0 |
| 306 | + c["result"] = "PASS" if ok else "FAIL" |
| 307 | + return c |
| 308 | +
|
| 309 | +
|
| 310 | + r["lockOnly"] = repair_case("lockonly", False) |
| 311 | + r["withPatchedVenv"] = repair_case("venv", True) |
| 312 | + r["uv"] = subprocess.run([UV, "--version"], capture_output=True, text=True).stdout.strip() |
| 313 | + print("RESULT " + json.dumps(r)) |
| 314 | + PYEOF |
| 315 | + cat > "$RUNNER_TEMP/driver_ovr.py" <<'PYEOF' |
| 316 | + #!/usr/bin/env python3 |
| 317 | + """uv hosted rollback/remove vs a user-authored override-dependencies pin. usage: driver_ovr.py SP UV WORK""" |
| 318 | + import json, os, re, shutil, subprocess, sys |
| 319 | + SP, UV, WORK = sys.argv[1], sys.argv[2], sys.argv[3] |
| 320 | + API = ["--api-url", "http://127.0.0.1:18080", "--api-token", "fake-token", "--org", "test-org", "--patch-server-url", "http://127.0.0.1:18080"] |
| 321 | + ENV = dict(os.environ, SOCKET_TELEMETRY_DISABLED="1", SOCKET_NO_CONFIG="1") |
| 322 | + PP = '[project]\nname = "uvp"\nversion = "0.1.0"\nrequires-python = ">=3.9"\ndependencies = ["python-dateutil==2.8.2"]\n\n[tool.uv]\noverride-dependencies = ["six==1.16.0"]\n' |
| 323 | + def run(cmd, cwd): |
| 324 | + return subprocess.run(cmd, cwd=cwd, env=ENV, capture_output=True, text=True, encoding="utf-8", errors="replace") |
| 325 | + def six_locked(d): |
| 326 | + t = open(os.path.join(d, "uv.lock"), encoding="utf-8").read() |
| 327 | + m = re.search(r'name = "six"\nversion = "([^"]+)"', t) |
| 328 | + return m.group(1) if m else None |
| 329 | + for cmd in (["rollback", "--json", "--yes"], ["remove", "pkg:pypi/six@1.16.0", "--json", "--yes"], None): |
| 330 | + name = cmd[0] if cmd else "control-no-socket-patch" |
| 331 | + d = os.path.join(WORK, name); shutil.rmtree(d, ignore_errors=True); os.makedirs(d) |
| 332 | + with open(os.path.join(d, "pyproject.toml"), "w", newline="\n") as f: f.write(PP) |
| 333 | + run([UV, "lock", "-q"], d) |
| 334 | + r = {"case": name} |
| 335 | + if cmd: |
| 336 | + p = run([SP, "scan", "--mode", "hosted", "--json", "--yes"] + API, d) |
| 337 | + r["scanRc"] = p.returncode |
| 338 | + p = run([SP] + cmd + API, d) |
| 339 | + r["undoRc"] = p.returncode |
| 340 | + try: r["warnings"] = [w.get("code") for w in json.loads(p.stdout).get("warnings", [])] |
| 341 | + except Exception: r["undoOut"] = p.stdout[-300:] |
| 342 | + r["userOverrideKept"] = "override-dependencies" in open(os.path.join(d, "pyproject.toml"), encoding="utf-8").read() |
| 343 | + run([UV, "lock", "-q", "--upgrade"], d) |
| 344 | + r["sixAfterUpgrade"] = six_locked(d) |
| 345 | + r["uv"] = run([UV, "--version"], d).stdout.strip() |
| 346 | + r["result"] = "PASS" if r["userOverrideKept"] and r["sixAfterUpgrade"] == "1.16.0" else "FAIL" |
| 347 | + print("RESULT " + json.dumps(r), flush=True) |
| 348 | + PYEOF |
| 349 | + python -m pip download --no-deps six==1.16.0 -d "$RUNNER_TEMP/mock" |
| 350 | + - name: Run probe |
| 351 | + run: | |
| 352 | + python "$RUNNER_TEMP/mock/mock.py" > "$RUNNER_TEMP/mock.out" 2>&1 & |
| 353 | + sleep 3; cat "$RUNNER_TEMP/mock.out" |
| 354 | + SP="$PWD/target/release/socket-patch"; [ -f "$SP.exe" ] && SP="$SP.exe" |
| 355 | + git config --global user.email probe@example.com; git config --global user.name probe |
| 356 | + for v in 0.2.37 0.5.31 0.12.21; do |
| 357 | + python -m venv "$RUNNER_TEMP/uv-$v" |
| 358 | + if [ -d "$RUNNER_TEMP/uv-$v/Scripts" ]; then B="$RUNNER_TEMP/uv-$v/Scripts"; else B="$RUNNER_TEMP/uv-$v/bin"; fi |
| 359 | + "$B/python" -m pip install -q "uv==$v" |
| 360 | + UVB="$B/uv"; [ -f "$UVB.exe" ] && UVB="$UVB.exe" |
| 361 | + echo "::group::uv $v rollback-after-edit (#379)"; python "$RUNNER_TEMP/driver_rb.py" "$SP" "$UVB" "$RUNNER_TEMP/work/rb-$v" || true; echo "::endgroup::" |
| 362 | + echo "::group::uv $v vendored repair (#381)"; python "$RUNNER_TEMP/driver_rep.py" "$SP" "$UVB" "$RUNNER_TEMP/work/rep-$v" || true; echo "::endgroup::" |
| 363 | + echo "::group::uv $v user override"; python "$RUNNER_TEMP/driver_ovr.py" "$SP" "$UVB" "$RUNNER_TEMP/work/ovr-$v" || true; echo "::endgroup::" |
| 364 | + done |
0 commit comments