From 8f45223067c9537fd77f674f7cb5aaf7fcb1b54b Mon Sep 17 00:00:00 2001 From: Frank Scholter Peres Date: Wed, 21 Oct 2026 15:11:14 +0000 Subject: [PATCH 1/7] chore(deps): track only version in lockfile managers; postUpgradeTask refreshes digests --- .github/renovate.json5 | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/.github/renovate.json5 b/.github/renovate.json5 index df48a5b..5885273 100644 --- a/.github/renovate.json5 +++ b/.github/renovate.json5 @@ -30,8 +30,15 @@ "semanticCommits": "enabled", "commitMessagePrefix": "chore(deps):", "prConcurrentLimit": 5, - "prCreation": "auto", + "prCreation": "immediate", "automerge": false, + "postUpgradeTasks": [ + { + "description": "Refresh lockfile URLs and sha256 digests after a version bump", + "command": "python3 tools/internal/refresh_lockfile_digests.py", + "executionMode": "graceful" + } + ], "customManagers": [ { "customType": "regex", @@ -39,8 +46,7 @@ "^tools/lockfiles/apm\\.lock\\.json$" ], "matchStrings": [ - "\"version\":\\s*\"(?[^\"]+)\"", - "\"url\":\\s*\"https://github\\.com/microsoft/apm/releases/download/v(?[^/]+)/[^\"]+\",\\s*\"sha256\":\\s*\"(?[^\"]+)\"" + "\"version\":\\s*\"(?[^\"]+)\"" ], "datasourceTemplate": "github-releases", "depNameTemplate": "microsoft/apm", @@ -52,8 +58,7 @@ "^tools/lockfiles/opencode\\.lock\\.json$" ], "matchStrings": [ - "\"version\":\\s*\"(?[^\"]+)\"", - "\"url\":\\s*\"https://github\\.com/anomalyco/opencode/releases/download/v(?[^/]+)/[^\"]+\",\\s*\"sha256\":\\s*\"(?[^\"]+)\"" + "\"version\":\\s*\"(?[^\"]+)\"" ], "datasourceTemplate": "github-releases", "depNameTemplate": "anomalyco/opencode", From 2f8cb103c3eb589d89270bf56c19d01452f6e9a6 Mon Sep 17 00:00:00 2001 From: Frank Scholter Peres Date: Wed, 21 Oct 2026 15:12:19 +0000 Subject: [PATCH 2/7] chore(tools): add lockfile digest refresh script for Renovate postUpgradeTask --- tools/internal/refresh_lockfile_digests.py | 142 +++++++++++++++++++++ 1 file changed, 142 insertions(+) create mode 100755 tools/internal/refresh_lockfile_digests.py diff --git a/tools/internal/refresh_lockfile_digests.py b/tools/internal/refresh_lockfile_digests.py new file mode 100755 index 0000000..8eb9004 --- /dev/null +++ b/tools/internal/refresh_lockfile_digests.py @@ -0,0 +1,142 @@ +#!/usr/bin/env python3 +# ******************************************************************************* +# Copyright (c) 2026 Contributors to the Eclipse Foundation +# +# See the NOTICE file(s) distributed with this work for additional +# information regarding copyright ownership. +# +# This program and the accompanying materials are made available under the +# terms of the Apache License Version 2.0 which is available at +# https://www.apache.org/licenses/LICENSE-2.0 +# +# SPDX-License-Identifier: Apache-2.0 +# ******************************************************************************* +"""Refresh URLs and sha256 digests in tool lockfiles after a version bump. + +Intended to run as a Renovate ``postUpgradeTask``: Renovate rewrites the +``version`` field (and the version embedded in each binary URL) via its +``custom.regex`` manager, but it cannot compute the new sha256 digests. This +script reads the (possibly new) version from each lockfile, fetches the +corresponding GitHub release, and rewrites every binary's ``url`` and +``sha256`` from the release's asset list. + +Usage: + python3 tools/internal/refresh_lockfile_digests.py [LOCKFILE ...] + +With no arguments, refreshes all ``tools/lockfiles/*.lock.json`` files that +contain a ``binaries`` list. Exits non-zero if any lockfile cannot be +refreshed (e.g. the release or an expected asset is missing). +""" + +import json +import os +import sys +import urllib.request + +GITHUB_API = "https://api.github.com" + + +def _api_get(url, token=None): + req = urllib.request.Request( + url, + headers={ + "Accept": "application/vnd.github+json", + "User-Agent": "score-devcontainer-lockfile-refresh", + **({"Authorization": f"Bearer {token}"} if token else {}), + }, + ) + with urllib.request.urlopen(req) as resp: + return json.load(resp) + + +def _asset_name(url): + """Extract the asset filename from a GitHub release download URL.""" + return url.rsplit("/", 1)[-1] + + +def refresh_lockfile(path, token=None): + """Refresh one lockfile in place. Returns a list of changed binary names.""" + with open(path, encoding="utf-8") as f: + data = json.load(f) + + changed = [] + for tool_name, tool in data.items(): + if not isinstance(tool, dict) or "binaries" not in tool: + continue + version = tool.get("version") + if not version: + continue + + # Derive the GitHub repo from the first binary URL: + # https://github.com///releases/download/v/ + first_url = tool["binaries"][0].get("url", "") + parts = first_url.split("/") + if len(parts) < 8 or parts[2] != "github.com" or parts[5] != "releases": + print( + f" ! {tool_name}: cannot derive repo from {first_url!r}", + file=sys.stderr, + ) + continue + owner, repo = parts[3], parts[4] + + release = _api_get( + f"{GITHUB_API}/repos/{owner}/{repo}/releases/tags/v{version}", token + ) + digests = {a["name"]: a.get("digest", "") for a in release.get("assets", [])} + + for binary in tool["binaries"]: + asset = _asset_name(binary.get("url", "")) + digest = digests.get(asset) + if not digest: + print( + f" ! {tool_name}: no digest for asset {asset!r} in v{version}", + file=sys.stderr, + ) + continue + sha = digest.split(":", 1)[1] if digest.startswith("sha256:") else digest + # Rebuild the URL from the version so the file is consistent + # whether or not Renovate already updated the URL. + new_url = f"https://github.com/{owner}/{repo}/releases/download/v{version}/{asset}" + if binary.get("sha256") != sha or binary.get("url") != new_url: + binary["url"] = new_url + binary["sha256"] = sha + changed.append(f"{tool_name}/{asset}") + + if changed: + with open(path, "w", encoding="utf-8") as f: + json.dump(data, f, indent=2) + f.write("\n") + return changed + + +def main(argv): + token = os.environ.get("RENOVATE_TOKEN") or os.environ.get("GITHUB_TOKEN") + if not argv[1:]: + lockdir = os.path.join(os.path.dirname(__file__), "..", "lockfiles") + lockdir = os.path.abspath(lockdir) + argv = [argv[0]] + [ + os.path.join(lockdir, name) + for name in sorted(os.listdir(lockdir)) + if name.endswith(".lock.json") + ] + + failed = False + for path in argv[1:]: + print(f"Refreshing {path}") + try: + changed = refresh_lockfile(path, token) + except Exception as exc: + print(f" ! failed: {exc}", file=sys.stderr) + failed = True + continue + if changed: + for c in changed: + print(f" ~ {c}") + else: + print(" = up to date") + + return 1 if failed else 0 + + +if __name__ == "__main__": + sys.exit(main(sys.argv)) From aa2e033cdb7d0bd8f425e34928eeffa3d91ad9f9 Mon Sep 17 00:00:00 2001 From: Frank Scholter Peres Date: Wed, 21 Oct 2026 15:28:43 +0000 Subject: [PATCH 3/7] fix(renovate): use object form for postUpgradeTasks (renovate 39 schema) --- .github/renovate.json5 | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/.github/renovate.json5 b/.github/renovate.json5 index 5885273..bf1c7b3 100644 --- a/.github/renovate.json5 +++ b/.github/renovate.json5 @@ -32,13 +32,13 @@ "prConcurrentLimit": 5, "prCreation": "immediate", "automerge": false, - "postUpgradeTasks": [ - { - "description": "Refresh lockfile URLs and sha256 digests after a version bump", - "command": "python3 tools/internal/refresh_lockfile_digests.py", - "executionMode": "graceful" - } - ], + "postUpgradeTasks": { + "commands": [ + "python3 tools/internal/refresh_lockfile_digests.py" + ], + "fileFilters": ["**/*"], + "executionMode": "update" + }, "customManagers": [ { "customType": "regex", From fdc9fd6381fcdf598627335e4af90db369b64e76 Mon Sep 17 00:00:00 2001 From: Frank Scholter Peres Date: Wed, 21 Oct 2026 15:43:15 +0000 Subject: [PATCH 4/7] fix(renovate): grant contents:write and allow the lockfile digest refresh command The postUpgradeTasks command that refreshes lockfile URLs and sha256 digests is gated by the global-only allowedCommands option, which cannot be set in renovate.json5. Set it via the RENOVATE_ALLOWED_COMMANDS env var in the renovate job, and grant contents:write so the refreshed files can be committed. --- .github/workflows/daily.yml | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/.github/workflows/daily.yml b/.github/workflows/daily.yml index 52442e3..46e01bc 100644 --- a/.github/workflows/daily.yml +++ b/.github/workflows/daily.yml @@ -36,7 +36,7 @@ jobs: runs-on: ubuntu-latest if: ${{ github.event_name == 'schedule' }} permissions: - contents: read + contents: write steps: - name: Checkout config file uses: actions/checkout@v7 @@ -48,6 +48,10 @@ jobs: configurationFile: .github/renovate.json5 env: LOG_LEVEL: debug + # Allow the post-upgrade task that refreshes lockfile URLs and sha256 + # digests (allowedCommands is a global-only option, so it must be set + # here rather than in renovate.json5). + RENOVATE_ALLOWED_COMMANDS: '["python3 tools/internal/refresh_lockfile_digests.py"]' validate-renovate-config: name: Validate Renovate config runs-on: ubuntu-latest From 558af04798f1b0aafc8bb00072de005001c8de15 Mon Sep 17 00:00:00 2001 From: "Frank Scholter Peres(MBTI)" <145544737+FScholPer@users.noreply.github.com> Date: Thu, 22 Oct 2026 13:37:33 +0200 Subject: [PATCH 5/7] Apply suggestion from @lurtz Co-authored-by: lurtz <727209+lurtz@users.noreply.github.com> Signed-off-by: Frank Scholter Peres(MBTI) <145544737+FScholPer@users.noreply.github.com> --- .github/workflows/daily.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/daily.yml b/.github/workflows/daily.yml index 46e01bc..1ef8424 100644 --- a/.github/workflows/daily.yml +++ b/.github/workflows/daily.yml @@ -51,7 +51,7 @@ jobs: # Allow the post-upgrade task that refreshes lockfile URLs and sha256 # digests (allowedCommands is a global-only option, so it must be set # here rather than in renovate.json5). - RENOVATE_ALLOWED_COMMANDS: '["python3 tools/internal/refresh_lockfile_digests.py"]' + RENOVATE_ALLOWED_COMMANDS: '["tools/internal/refresh_lockfile_digests.py"]' validate-renovate-config: name: Validate Renovate config runs-on: ubuntu-latest From 6694751907eec0857055abf8a79287a0d7b15dea Mon Sep 17 00:00:00 2001 From: "Frank Scholter Peres(MBTI)" <145544737+FScholPer@users.noreply.github.com> Date: Thu, 22 Oct 2026 13:37:46 +0200 Subject: [PATCH 6/7] Apply suggestion from @lurtz Co-authored-by: lurtz <727209+lurtz@users.noreply.github.com> Signed-off-by: Frank Scholter Peres(MBTI) <145544737+FScholPer@users.noreply.github.com> --- .github/renovate.json5 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/renovate.json5 b/.github/renovate.json5 index bf1c7b3..650bff2 100644 --- a/.github/renovate.json5 +++ b/.github/renovate.json5 @@ -34,7 +34,7 @@ "automerge": false, "postUpgradeTasks": { "commands": [ - "python3 tools/internal/refresh_lockfile_digests.py" + "tools/internal/refresh_lockfile_digests.py" ], "fileFilters": ["**/*"], "executionMode": "update" From 62d287630de4736697cd32ce8bb587bdf1fcb0be Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 22 Oct 2026 11:40:27 +0000 Subject: [PATCH 7/7] Restore read-only Renovate workflow permission Co-authored-by: FScholPer <145544737+FScholPer@users.noreply.github.com> --- .github/workflows/daily.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/daily.yml b/.github/workflows/daily.yml index 1ef8424..cfcad8a 100644 --- a/.github/workflows/daily.yml +++ b/.github/workflows/daily.yml @@ -36,7 +36,7 @@ jobs: runs-on: ubuntu-latest if: ${{ github.event_name == 'schedule' }} permissions: - contents: write + contents: read steps: - name: Checkout config file uses: actions/checkout@v7