diff --git a/.github/workflows/pipeline-bump-cpp-deps.yml b/.github/workflows/pipeline-bump-cpp-deps.yml new file mode 100644 index 0000000..d2188b8 --- /dev/null +++ b/.github/workflows/pipeline-bump-cpp-deps.yml @@ -0,0 +1,77 @@ +name: Bump-Cpp-Deps + +# Weekly bump of the C++ library pins that Renovate has no manager for. + +on: + schedule: + - cron: "0 0 * * 1" # Mondays at 00:00 UTC + workflow_dispatch: + +permissions: + contents: read + +jobs: + bump-cpp-dependencies: + runs-on: ubuntu-24.04 + # The weekly cron only runs on the canonical repo. Forks can still trigger it manually. + if: github.event_name != 'schedule' || github.repository == 'ngcpp/proxy' + steps: + - name: Mint a GitHub App token + id: app-token + uses: actions/create-github-app-token@v2 + with: + app-id: ${{ secrets.DEPENDENCY_MANAGER_APP_ID }} + private-key: ${{ secrets.DEPENDENCY_MANAGER_APP_PRIVATE_KEY }} + + - name: Resolve the bot's commit identity + id: bot + env: + GH_TOKEN: ${{ steps.app-token.outputs.token }} + run: | + slug='${{ steps.app-token.outputs.app-slug }}' + uid=$(gh api "/users/${slug}[bot]" --jq '.id') + echo "name=${slug}[bot]" >> "$GITHUB_OUTPUT" + echo "email=${uid}+${slug}[bot]@users.noreply.github.com" >> "$GITHUB_OUTPUT" + + - uses: actions/checkout@v6 + with: + token: ${{ steps.app-token.outputs.token }} + + - uses: actions/setup-python@v6 + with: + python-version: "3.13" + + - name: Install Meson + run: python3 -m pip install --upgrade pip meson + + - name: Bump CMake and Meson dependencies + env: + GITHUB_TOKEN: ${{ steps.app-token.outputs.token }} + run: python3 tools/bump_cmake_meson_deps.py + + - name: Open or update the pull request + env: + GH_TOKEN: ${{ steps.app-token.outputs.token }} + GIT_NAME: ${{ steps.bot.outputs.name }} + GIT_EMAIL: ${{ steps.bot.outputs.email }} + BASE: ${{ github.ref_name }} + run: | + set -euo pipefail + if [ -z "$(git status --porcelain)" ]; then + echo "No dependency changes; nothing to do." + exit 0 + fi + branch=auto/bump-cpp-deps + git config user.name "$GIT_NAME" + git config user.email "$GIT_EMAIL" + git checkout -B "$branch" + git add -A + git commit -m "Bump CMake and Meson dependencies" + git push --force origin "$branch" + if [ -n "$(gh pr list --head "$branch" --base "$BASE" --state open --json number --jq '.[0].number // empty')" ]; then + echo "Updated the existing pull request." + else + gh pr create --base "$BASE" --head "$branch" \ + --title "Bump CMake and Meson dependencies" \ + --body 'Automated bump of the CMake FetchContent registries and the Meson wraps (tools/bump_cmake_meson_deps.py). The Renovate pipeline handles everything else.' + fi diff --git a/.github/workflows/pipeline-bump-renovate-deps.yml b/.github/workflows/pipeline-bump-renovate-deps.yml new file mode 100644 index 0000000..d1f21cf --- /dev/null +++ b/.github/workflows/pipeline-bump-renovate-deps.yml @@ -0,0 +1,53 @@ +name: Bump-Renovate-Deps + +# Weekly Renovate run that bumps every dependency it has a manager for, all in one pull request. + +on: + schedule: + - cron: "0 0 * * 1" # Mondays at 00:00 UTC + workflow_dispatch: + +permissions: + contents: read + +jobs: + renovate: + runs-on: ubuntu-24.04 + # The weekly cron only runs on the canonical repo. Forks can still trigger it manually. + if: github.event_name != 'schedule' || github.repository == 'ngcpp/proxy' + steps: + - name: Mint a GitHub App token + id: app-token + uses: actions/create-github-app-token@v2 + with: + app-id: ${{ secrets.DEPENDENCY_MANAGER_APP_ID }} + private-key: ${{ secrets.DEPENDENCY_MANAGER_APP_PRIVATE_KEY }} + + - name: Resolve the bot's commit identity + id: bot + env: + GH_TOKEN: ${{ steps.app-token.outputs.token }} + run: | + slug='${{ steps.app-token.outputs.app-slug }}' + uid=$(gh api "/users/${slug}[bot]" --jq '.id') + echo "git-author=${slug}[bot] <${uid}+${slug}[bot]@users.noreply.github.com>" >> "$GITHUB_OUTPUT" + + - uses: actions/checkout@v6 + + - uses: actions/setup-node@v4 + with: + node-version: lts/* + + - name: Run Renovate + env: + RENOVATE_TOKEN: ${{ steps.app-token.outputs.token }} + RENOVATE_USERNAME: ${{ steps.app-token.outputs.app-slug }}[bot] + RENOVATE_GIT_AUTHOR: ${{ steps.bot.outputs.git-author }} + RENOVATE_PLATFORM: github + RENOVATE_REPOSITORIES: ${{ github.repository }} + RENOVATE_AUTODISCOVER: "false" + RENOVATE_ONBOARDING: "false" + RENOVATE_BASE_BRANCHES: ${{ github.ref_name }} + RENOVATE_ALLOWED_POST_UPGRADE_COMMANDS: '["^bazel mod graph --lockfile_mode=update$"]' + LOG_LEVEL: info + run: npx --yes renovate diff --git a/CMakeLists.txt b/CMakeLists.txt index defa6e5..4409d63 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -106,6 +106,9 @@ install( if(BUILD_TESTING) include(CTest) + include(cmake/read_dependencies.cmake) + proxy_read_dependencies("${CMAKE_CURRENT_SOURCE_DIR}/cmake/dependencies.json") + include(FetchContent) # The policy uses the download time for timestamp, instead of the timestamp in the archive. This # allows for proper rebuilds when a projects URL changes. @@ -115,9 +118,8 @@ if(BUILD_TESTING) FetchContent_Declare( fmt - URL https://github.com/fmtlib/fmt/archive/refs/tags/12.1.0.tar.gz - URL_HASH - SHA256=ea7de4299689e12b6dddd392f9896f08fb0777ac7168897a244a6d6085043fea + URL ${PROXY_FMT_URL} + URL_HASH SHA256=${PROXY_FMT_SHA256} SYSTEM ) FetchContent_MakeAvailable(fmt) diff --git a/benchmarks/CMakeLists.txt b/benchmarks/CMakeLists.txt index 6843258..859ea8d 100644 --- a/benchmarks/CMakeLists.txt +++ b/benchmarks/CMakeLists.txt @@ -2,9 +2,8 @@ project(msft_proxy_benchmarks) FetchContent_Declare( benchmark - URL https://github.com/google/benchmark/archive/refs/tags/v1.9.5.tar.gz - URL_HASH - SHA256=9631341c82bac4a288bef951f8b26b41f69021794184ece969f8473977eaa340 + URL ${PROXY_BENCHMARK_URL} + URL_HASH SHA256=${PROXY_BENCHMARK_SHA256} ) set( BENCHMARK_ENABLE_TESTING diff --git a/cmake/dependencies.json b/cmake/dependencies.json new file mode 100644 index 0000000..e613794 --- /dev/null +++ b/cmake/dependencies.json @@ -0,0 +1,17 @@ +[ + { + "name": "fmt", + "url": "https://github.com/fmtlib/fmt/archive/refs/tags/12.1.0.tar.gz", + "sha256": "ea7de4299689e12b6dddd392f9896f08fb0777ac7168897a244a6d6085043fea" + }, + { + "name": "googletest", + "url": "https://github.com/google/googletest/archive/refs/tags/v1.17.0.tar.gz", + "sha256": "65fab701d9829d38cb77c14acdc431d2108bfdbf8979e40eb8ae567edf10b27c" + }, + { + "name": "benchmark", + "url": "https://github.com/google/benchmark/archive/refs/tags/v1.9.5.tar.gz", + "sha256": "9631341c82bac4a288bef951f8b26b41f69021794184ece969f8473977eaa340" + } +] diff --git a/cmake/read_dependencies.cmake b/cmake/read_dependencies.cmake new file mode 100644 index 0000000..8d68ba2 --- /dev/null +++ b/cmake/read_dependencies.cmake @@ -0,0 +1,20 @@ +# Read a JSON dependency registry and expose PROXY__URL / PROXY__SHA256 in the caller's scope for FetchContent_Declare. + +function(proxy_read_dependencies _json_path) + file(READ "${_json_path}" _json) + set_property( + DIRECTORY + APPEND + PROPERTY CMAKE_CONFIGURE_DEPENDS "${_json_path}" + ) + string(JSON _count LENGTH "${_json}") + math(EXPR _last "${_count} - 1") + foreach(_i RANGE 0 ${_last}) + string(JSON _name GET "${_json}" ${_i} "name") + string(JSON _url GET "${_json}" ${_i} "url") + string(JSON _sha GET "${_json}" ${_i} "sha256") + string(TOUPPER "${_name}" _name) + set("PROXY_${_name}_URL" "${_url}" PARENT_SCOPE) + set("PROXY_${_name}_SHA256" "${_sha}" PARENT_SCOPE) + endforeach() +endfunction() diff --git a/renovate.json b/renovate.json new file mode 100644 index 0000000..acc2823 --- /dev/null +++ b/renovate.json @@ -0,0 +1,38 @@ +{ + "$schema": "https://docs.renovatebot.com/renovate-schema.json", + "extends": ["config:recommended"], + "enabledManagers": [ + "github-actions", + "pre-commit", + "pip_requirements", + "bazel-module", + "bazelisk" + ], + "pre-commit": { + "enabled": true + }, + "separateMajorMinor": false, + "updateLockFiles": false, + "prHourlyLimit": 0, + "branchPrefix": "auto/bump-renovate-", + "packageRules": [ + { + "description": "One weekly PR on auto/bump-renovate-deps.", + "matchManagers": [ + "github-actions", + "pre-commit", + "pip_requirements", + "bazel-module", + "bazelisk" + ], + "groupName": "dependencies", + "groupSlug": "deps" + } + ], + "postUpgradeTasks": { + "description": "Refresh MODULE.bazel.lock to track the bazel_dep versions Renovate bumped.", + "commands": ["bazel mod graph --lockfile_mode=update"], + "fileFilters": ["MODULE.bazel.lock"], + "executionMode": "branch" + } +} diff --git a/subprojects/fmt.wrap b/subprojects/fmt.wrap index 6544857..7e9c5c4 100644 --- a/subprojects/fmt.wrap +++ b/subprojects/fmt.wrap @@ -1,13 +1,13 @@ [wrap-file] -directory = fmt-12.1.0 -source_url = https://github.com/fmtlib/fmt/archive/12.1.0.tar.gz -source_filename = fmt-12.1.0.tar.gz -source_hash = ea7de4299689e12b6dddd392f9896f08fb0777ac7168897a244a6d6085043fea -source_fallback_url = https://github.com/wrapdb/fmt/releases/download/12.1.0-4/fmt-12.1.0.tar.gz -patch_filename = fmt_12.1.0-4_patch.zip -patch_url = https://github.com/wrapdb/fmt/releases/download/12.1.0-4/fmt_12.1.0-4_patch.zip -patch_hash = 65b7fe3c29f25528011bc295e83e4f6f10028c922407e003b7856bb79789f345 -3rdparty_wrapdb_version = 12.1.0-4 +directory = fmt-12.0.0 +source_url = https://github.com/fmtlib/fmt/archive/12.0.0.tar.gz +source_filename = fmt-12.0.0.tar.gz +source_hash = aa3e8fbb6a0066c03454434add1f1fc23299e85758ceec0d7d2d974431481e40 +source_fallback_url = https://github.com/mesonbuild/wrapdb/releases/download/fmt_12.0.0-1/fmt-12.0.0.tar.gz +patch_filename = fmt_12.0.0-1_patch.zip +patch_url = https://wrapdb.mesonbuild.com/v2/fmt_12.0.0-1/get_patch +patch_hash = 307f288ebf3850abf2f0c50ac1fb07de97df9538d39146d802f3c0d6cada8998 +wrapdb_version = 12.0.0-1 [provide] dependency_names = fmt diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 8b6f678..5f7b4cc 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -2,9 +2,8 @@ project(msft_proxy_tests) FetchContent_Declare( googletest - URL https://github.com/google/googletest/archive/refs/tags/v1.17.0.tar.gz - URL_HASH - SHA256=65fab701d9829d38cb77c14acdc431d2108bfdbf8979e40eb8ae567edf10b27c + URL ${PROXY_GOOGLETEST_URL} + URL_HASH SHA256=${PROXY_GOOGLETEST_SHA256} ) set(gtest_force_shared_crt ON CACHE BOOL "" FORCE) # For Windows: Prevent overriding the parent project's compiler/linker settings set(BUILD_GMOCK OFF CACHE BOOL "" FORCE) # Disable GMock diff --git a/tools/bump_cmake_meson_deps.py b/tools/bump_cmake_meson_deps.py new file mode 100644 index 0000000..7e2a338 --- /dev/null +++ b/tools/bump_cmake_meson_deps.py @@ -0,0 +1,131 @@ +#!/usr/bin/env python3 +# pyright: strict + +"""Bump the C++ dependencies that Renovate cannot manage. + +Run by .github/workflows/pipeline-bump-cpp-deps.yml, covering two families: + + cmake GitHub-archive entries in cmake/dependencies.json and + tools/report_generator/dependencies.json, bumped to each repo's latest release with + the sha256 recomputed. + meson subprojects/*.wrap, refreshed via `meson wrap update`. +""" + +import hashlib +import json +import os +import re +import shutil +import subprocess +import urllib.error +import urllib.request +from pathlib import Path +from typing import NoReturn + +_REPO_ROOT = Path(__file__).resolve().parent.parent +_ARCHIVE_URL_RE = re.compile( + r"https://github\.com/([^/]+/[^/]+)/archive/refs/tags/(.+)\.tar\.gz" +) +_WRAP_DIR_RE = re.compile(r"^directory\s*=\s*(.+)$", re.MULTILINE) + + +def _abort(msg: str) -> NoReturn: + print(f"error: {msg}", flush=True) + raise SystemExit(1) + + +def _http_get(url: str, *, accept: str | None = None) -> bytes | None: + req = urllib.request.Request(url) + req.add_header("User-Agent", "proxy-bump-dependencies") + if accept is not None: + req.add_header("Accept", accept) + token = os.environ.get("GITHUB_TOKEN") + if token: + req.add_header("Authorization", f"Bearer {token}") + try: + with urllib.request.urlopen(req, timeout=120) as resp: + return resp.read() + except (urllib.error.URLError, TimeoutError): + return None + + +def _github_latest_tag(repo: str) -> str | None: + body = _http_get( + f"https://api.github.com/repos/{repo}/releases/latest", + accept="application/json", + ) + if body is None: + return None + try: + data = json.loads(body) + except json.JSONDecodeError: + return None + if isinstance(data, dict): + name = data.get("tag_name") + if isinstance(name, str): + return name + return None + + +def _update_registry(rel_path: str, label: str) -> None: + """Bump each GitHub-archive entry to its latest release (downloading only on a change).""" + print(f"Checking {label} ({rel_path}) ...", flush=True) + path = _REPO_ROOT / rel_path + deps = json.loads(path.read_text(encoding="utf-8")) + changed = False + for dep in deps: + name, url = dep["name"], dep["url"] + m = _ARCHIVE_URL_RE.fullmatch(url) + if m is None: + _abort(f"{label}/{name}: url is not a GitHub archive tarball: {url}") + repo, current = m.group(1), m.group(2) + latest = _github_latest_tag(repo) + if latest is None: + _abort(f"{label}/{name}: could not determine the latest release of {repo}") + if latest == current: + continue # already current -- no download + new_url = f"https://github.com/{repo}/archive/refs/tags/{latest}.tar.gz" + body = _http_get(new_url) + if body is None: + _abort(f"{label}/{name}: could not download {new_url}") + print(f" {name}: {current} -> {latest}", flush=True) + dep["url"] = new_url + dep["sha256"] = hashlib.sha256(body).hexdigest() + changed = True + if changed: + path.write_text(json.dumps(deps, indent=2) + "\n", encoding="utf-8") + + +def _wrap_version(wrap: Path) -> str: + m = _WRAP_DIR_RE.search(wrap.read_text(encoding="utf-8")) + return m.group(1).strip() if m else "?" + + +def _update_meson() -> None: + print("Updating Meson wraps ...", flush=True) + wraps = sorted((_REPO_ROOT / "subprojects").glob("*.wrap")) + if not wraps: + _abort("no wrap files found in subprojects/") + if shutil.which("meson") is None: + _abort("meson not found on PATH") + for wrap in wraps: + name = wrap.stem + before = _wrap_version(wrap) + result = subprocess.run( + ["meson", "wrap", "update", name], + cwd=_REPO_ROOT, + capture_output=True, + text=True, + check=False, + ) + if result.returncode != 0: + _abort(f"`meson wrap update {name}` failed: {result.stderr.strip()}") + after = _wrap_version(wrap) + if after != before: + print(f" {name}: {before} -> {after}", flush=True) + + +if __name__ == "__main__": + _update_registry("cmake/dependencies.json", "cmake") + _update_registry("tools/report_generator/dependencies.json", "report_generator") + _update_meson() diff --git a/tools/report_generator/CMakeLists.txt b/tools/report_generator/CMakeLists.txt index 9436c13..9044c22 100644 --- a/tools/report_generator/CMakeLists.txt +++ b/tools/report_generator/CMakeLists.txt @@ -2,6 +2,9 @@ cmake_minimum_required(VERSION 3.5) project(report_generator) +include(${CMAKE_CURRENT_SOURCE_DIR}/../../cmake/read_dependencies.cmake) +proxy_read_dependencies("${CMAKE_CURRENT_SOURCE_DIR}/dependencies.json") + include(FetchContent) # The policy uses the download time for timestamp, instead of the timestamp in the archive. This # allows for proper rebuilds when a projects URL changes. @@ -11,9 +14,8 @@ endif() FetchContent_Declare( nlohmann_json - URL https://github.com/nlohmann/json/archive/refs/tags/v3.12.0.tar.gz - URL_HASH - SHA256=4b92eb0c06d10683f7447ce9406cb97cd4b453be18d7279320f7b2f025c10187 + URL ${PROXY_NLOHMANN_JSON_URL} + URL_HASH SHA256=${PROXY_NLOHMANN_JSON_SHA256} ) FetchContent_MakeAvailable(nlohmann_json) diff --git a/tools/report_generator/dependencies.json b/tools/report_generator/dependencies.json new file mode 100644 index 0000000..1d57e97 --- /dev/null +++ b/tools/report_generator/dependencies.json @@ -0,0 +1,7 @@ +[ + { + "name": "nlohmann_json", + "url": "https://github.com/nlohmann/json/archive/refs/tags/v3.12.0.tar.gz", + "sha256": "4b92eb0c06d10683f7447ce9406cb97cd4b453be18d7279320f7b2f025c10187" + } +]