From 8cffdd911d25e455e505a18259d5277308e4de27 Mon Sep 17 00:00:00 2001 From: Tsvika Shapira Date: Sun, 27 Sep 2026 15:39:54 +0300 Subject: [PATCH 1/3] ci(deps-update): list the updates in the commit message The deps-update commit had only a subject, so projects that squash-merge with the PR's commit messages got an empty squash message. A new step compares uv.lock and the hook revs against HEAD (`uv sync --upgrade` does not print what it bumped) and the workflow puts the list in the commit body. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01GyMoD8tx4oz1bpJ1tEL845 --- .../.github/workflows/deps-update.yml | 37 ++++++++++++++++++- 1 file changed, 36 insertions(+), 1 deletion(-) diff --git a/project_name/.github/workflows/deps-update.yml b/project_name/.github/workflows/deps-update.yml index 92fa9bb..35b2950 100644 --- a/project_name/.github/workflows/deps-update.yml +++ b/project_name/.github/workflows/deps-update.yml @@ -26,6 +26,37 @@ jobs: run: uv sync - name: Update dependencies run: uv run just deps-update + - name: List the updates, for the commit message + id: updates + # Compare against HEAD, since `uv sync --upgrade` does not print what it bumped + shell: python + run: | + import os, re, subprocess, tomllib + + def at_head(path): + cmd = ["git", "show", f"HEAD:{path}"] + return subprocess.run(cmd, capture_output=True, text=True, check=True).stdout + + def packages(text): + lock = tomllib.loads(text).get("package", []) + return {p["name"]: p["version"] for p in lock if "version" in p} + + def hook_revs(text): + pairs = re.findall(r"- repo: https://github.com/(\S+)\n\s+rev: (\S+)", text) + return {repo: rev.strip("'\"") for repo, rev in pairs} + + lines = [] + for path, parse in [("uv.lock", packages), (".pre-commit-config.yaml", hook_revs)]: + old = parse(at_head(path)) + with open(path, encoding="utf-8") as f: + new = parse(f.read()) + lines += [ + f"- {name} {old[name]} → {version}" + for name, version in sorted(new.items()) + if old.get(name, version) != version + ] + with open(os.environ["GITHUB_OUTPUT"], "a", encoding="utf-8") as f: + f.write("updates< Date: Sun, 27 Sep 2026 20:46:32 +0300 Subject: [PATCH 2/3] ci(deps-update): list each fork's bump when a package has several versions MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit uv.lock holds one package at several versions when resolution forks by marker, and keying by name kept only one of them. Key by the fork's resolution-markers and list every changed pair: `- numpy a → a', b → b'`. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01GyMoD8tx4oz1bpJ1tEL845 --- .../.github/workflows/deps-update.yml | 29 ++++++++++++++----- 1 file changed, 21 insertions(+), 8 deletions(-) diff --git a/project_name/.github/workflows/deps-update.yml b/project_name/.github/workflows/deps-update.yml index 35b2950..72b0f1d 100644 --- a/project_name/.github/workflows/deps-update.yml +++ b/project_name/.github/workflows/deps-update.yml @@ -37,24 +37,37 @@ jobs: cmd = ["git", "show", f"HEAD:{path}"] return subprocess.run(cmd, capture_output=True, text=True, check=True).stdout + # Both map a name to {key: version}. A lock can hold one package at several + # versions, one per resolution fork, keyed by the fork's markers. def packages(text): - lock = tomllib.loads(text).get("package", []) - return {p["name"]: p["version"] for p in lock if "version" in p} + versions = {} + for p in tomllib.loads(text).get("package", []): + if "version" in p: + fork = tuple(p.get("resolution-markers", [])) + versions.setdefault(p["name"], {})[fork] = p["version"] + return versions def hook_revs(text): pairs = re.findall(r"- repo: https://github.com/(\S+)\n\s+rev: (\S+)", text) - return {repo: rev.strip("'\"") for repo, rev in pairs} + return {repo: {(): rev.strip("'\"")} for repo, rev in pairs} + + def bumps(old, new): + pairs = [(old[k], new[k]) for k in new if k in old] + # forks whose markers changed: pair what is left in version order + pairs += zip( + sorted(v for k, v in old.items() if k not in new), + sorted(v for k, v in new.items() if k not in old), + ) + return [f"{a} → {b}" for a, b in pairs if a != b] lines = [] for path, parse in [("uv.lock", packages), (".pre-commit-config.yaml", hook_revs)]: old = parse(at_head(path)) with open(path, encoding="utf-8") as f: new = parse(f.read()) - lines += [ - f"- {name} {old[name]} → {version}" - for name, version in sorted(new.items()) - if old.get(name, version) != version - ] + for name in sorted(new.keys() & old.keys()): + if changes := bumps(old[name], new[name]): + lines.append(f"- {name} {', '.join(changes)}") with open(os.environ["GITHUB_OUTPUT"], "a", encoding="utf-8") as f: f.write("updates< Date: Sun, 27 Sep 2026 20:47:28 +0300 Subject: [PATCH 3/3] ci(deps-update): pair versions across forks whose markers changed When a fork split (numpy 2.2.1 for >=3.10 became 2.2.6 for 3.10 and 2.3.2 for >=3.11), zipping the leftovers dropped a bump, and a string sort put 2.10 before 2.9. Pair each new version with the newest old one below it, any old version left over with the oldest new one above it, and compare versions numerically. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01GyMoD8tx4oz1bpJ1tEL845 --- .../.github/workflows/deps-update.yml | 22 ++++++++++++++----- 1 file changed, 17 insertions(+), 5 deletions(-) diff --git a/project_name/.github/workflows/deps-update.yml b/project_name/.github/workflows/deps-update.yml index 72b0f1d..15e1a17 100644 --- a/project_name/.github/workflows/deps-update.yml +++ b/project_name/.github/workflows/deps-update.yml @@ -51,13 +51,25 @@ jobs: pairs = re.findall(r"- repo: https://github.com/(\S+)\n\s+rev: (\S+)", text) return {repo: {(): rev.strip("'\"")} for repo, rev in pairs} + def vkey(version): + return tuple(int(n) for n in re.findall(r"\d+", version)) + def bumps(old, new): pairs = [(old[k], new[k]) for k in new if k in old] - # forks whose markers changed: pair what is left in version order - pairs += zip( - sorted(v for k, v in old.items() if k not in new), - sorted(v for k, v in new.items() if k not in old), - ) + # Forks whose markers changed, e.g. one fork split in two: pair each new + # version with the newest old one below it, and any old version left over + # with the oldest new one above it. + lo = sorted({v for k, v in old.items() if k not in new}, key=vkey) + ln = sorted({v for k, v in new.items() if k not in old}, key=vkey) + if lo and ln: + for b in ln: + below = [a for a in lo if vkey(a) <= vkey(b)] + pairs.append((below[-1] if below else lo[0], b)) + for a in lo: + if a not in {p[0] for p in pairs}: + above = [b for b in ln if vkey(b) >= vkey(a)] + pairs.append((a, above[0] if above else ln[-1])) + pairs = sorted(set(pairs), key=lambda p: (vkey(p[0]), vkey(p[1]))) return [f"{a} → {b}" for a, b in pairs if a != b] lines = []