From 7c799751c401565bd05ce9920cf5579ea44cded0 Mon Sep 17 00:00:00 2001 From: Tim <73599525+timtogan@users.noreply.github.com> Date: Sun, 16 Aug 2026 23:54:19 -0700 Subject: [PATCH 1/4] Add github workflow and script to auto update vendordeps --- .github/scripts/update_vendordeps.py | 123 ++++++++++++++++++++++++ .github/workflows/update-vendordeps.yml | 30 ++++++ 2 files changed, 153 insertions(+) create mode 100644 .github/scripts/update_vendordeps.py create mode 100644 .github/workflows/update-vendordeps.yml diff --git a/.github/scripts/update_vendordeps.py b/.github/scripts/update_vendordeps.py new file mode 100644 index 0000000..13e2298 --- /dev/null +++ b/.github/scripts/update_vendordeps.py @@ -0,0 +1,123 @@ +#!/usr/bin/env python3 +"""Update WPILib vendordep JSON files from the WPILib vendordep marketplace. + +This mirrors what the WPILib VS Code extension's dependency manager does: +fetch the marketplace manifest for the frcYear, match each local vendordep +by uuid, and if the marketplace lists a newer version, download that exact +file. + +Vendordeps whose uuid is not in the marketplace (e.g. WPILibNewCommands, +which ships with WPILib itself) are reported and left alone. + +Writes a markdown summary of what changed to the path given by +--summary (used as the pull request body), and prints it to stdout. +Exits 0 whether or not anything changed; the workflow decides what to do +based on whether the git tree is dirty. +""" + +import argparse +import json +import re +import sys +import urllib.request +from pathlib import Path + +MARKETPLACE_ROOT = "https://frcmaven.wpi.edu/artifactory/vendordeps/vendordep-marketplace" +VENDORDEPS_DIR = Path(__file__).resolve().parents[2] / "vendordeps" +TIMEOUT_SECONDS = 30 + + +def fetch(url: str) -> str: + req = urllib.request.Request(url) + with urllib.request.urlopen(req, timeout=TIMEOUT_SECONDS) as resp: + return resp.read().decode("utf-8") + + +def version_key(version: str) -> tuple: + # Converts version string into a tuple for easier comparison + return tuple(int(part) for part in re.findall(r"\d+", version)) + + +def main() -> int: + parser = argparse.ArgumentParser() + parser.add_argument("--summary", type=Path, help="Path to write the markdown summary to") + args = parser.parse_args() + + manifests: dict[str, list] = {} + updated: list[str] = [] + unchanged: list[str] = [] + skipped: list[str] = [] + errors: list[str] = [] + + for path in sorted(VENDORDEPS_DIR.glob("*.json")): + local = json.loads(path.read_text()) + name = local.get("name", path.name) + uuid = local.get("uuid") + year = local.get("frcYear") + local_version = local.get("version", "") + + if not uuid or not year: + skipped.append(f"**{name}** (`{path.name}`): missing uuid or frcYear") + continue + + if year not in manifests: + try: + manifests[year] = json.loads(fetch(f"{MARKETPLACE_ROOT}/{year}.json")) + except Exception as e: # noqa: BLE001 to make Ruff stop complaining about such futile things as "bad code" + manifests[year] = [] + errors.append(f"Failed to fetch the {year} marketplace manifest ({e})") + + candidates = [e for e in manifests[year] if e.get("uuid") == uuid] # list and not var bc wpilib repo keeps mutliple versions + if not candidates: + skipped.append(f"**{name}** (`{path.name}`): not in the {year} marketplace") + continue + + best = max(candidates, key=lambda e: version_key(e.get("version", ""))) + best_version = best.get("version", "") + + if version_key(best_version) <= version_key(local_version): + unchanged.append(f"**{name}**: {local_version}") + continue + + try: + new_text = fetch(f"{MARKETPLACE_ROOT}/{best['path']}") + new_json = json.loads(new_text) + except Exception as e: # noqa: BLE001 + errors.append(f"**{name}**: failed to download {best['path']} ({e})") + continue + + new_path = VENDORDEPS_DIR / new_json.get("fileName", Path(best["path"]).name) + try: + new_path.write_text(new_text) + if new_path != path: + path.unlink() # delete the old file + except OSError as e: + errors.append(f"**{name}**: failed to write `{new_path.name}` ({e})") + continue + + updated.append(f"**{name}**: {local_version} -> {best_version} (`{new_path.name}`)") + + lines = [] + if updated: + lines.append("## Updated") + lines += [f"- {s}" for s in updated] + if errors: + lines.append("\n## Errors (left unchanged)") + lines += [f"- {s}" for s in errors] + if unchanged: + lines.append("\n## Already up to date") + lines += [f"- {s}" for s in unchanged] + if skipped: + lines.append("\n## Skipped") + lines += [f"- {s}" for s in skipped] + summary = "\n".join(lines) + "\n" + + print(summary) + if args.summary: + args.summary.write_text(summary) + + return 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/.github/workflows/update-vendordeps.yml b/.github/workflows/update-vendordeps.yml new file mode 100644 index 0000000..dcbdf2a --- /dev/null +++ b/.github/workflows/update-vendordeps.yml @@ -0,0 +1,30 @@ +name: Update vendordeps + +on: + schedule: + # Every Monday at 12:00 UTC + - cron: '0 12 * * 1' + workflow_dispatch: + +permissions: + contents: write + pull-requests: write + +jobs: + update: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Check for vendordep updates + run: python3 .github/scripts/update_vendordeps.py --summary /tmp/vendordep-summary.md + + - name: Create pull request + uses: peter-evans/create-pull-request@v7 + with: + token: ${{ secrets.VENDORDEP_UPDATE_TOKEN || github.token }} + branch: bot/update-vendordeps + title: Update vendordeps + body-path: /tmp/vendordep-summary.md + commit-message: Update vendordeps + delete-branch: true From ca7725abe3fef150acbffc9c065f7a51c06f71a9 Mon Sep 17 00:00:00 2001 From: Tim <73599525+timtogan@users.noreply.github.com> Date: Mon, 17 Aug 2026 21:19:31 -0700 Subject: [PATCH 2/4] Restrict auto update vendordeps to one run at a time Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- .github/workflows/update-vendordeps.yml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.github/workflows/update-vendordeps.yml b/.github/workflows/update-vendordeps.yml index dcbdf2a..961841f 100644 --- a/.github/workflows/update-vendordeps.yml +++ b/.github/workflows/update-vendordeps.yml @@ -6,6 +6,10 @@ on: - cron: '0 12 * * 1' workflow_dispatch: +concurrency: + group: update-vendordeps + cancel-in-progress: true + permissions: contents: write pull-requests: write From 914d88a6182621b415c9bd2a04278675e13dc9d7 Mon Sep 17 00:00:00 2001 From: Tim <73599525+timtogan@users.noreply.github.com> Date: Mon, 17 Aug 2026 21:21:59 -0700 Subject: [PATCH 3/4] Add handling for when wpilib vendordep marketplace is down Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- .github/scripts/update_vendordeps.py | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/.github/scripts/update_vendordeps.py b/.github/scripts/update_vendordeps.py index 13e2298..7cd1d2d 100644 --- a/.github/scripts/update_vendordeps.py +++ b/.github/scripts/update_vendordeps.py @@ -63,11 +63,17 @@ def main() -> int: if year not in manifests: try: manifests[year] = json.loads(fetch(f"{MARKETPLACE_ROOT}/{year}.json")) - except Exception as e: # noqa: BLE001 to make Ruff stop complaining about such futile things as "bad code" - manifests[year] = [] + except Exception as e: # noqa: BLE001 (network/remote errors; handled and reported below) + manifests[year] = None errors.append(f"Failed to fetch the {year} marketplace manifest ({e})") - candidates = [e for e in manifests[year] if e.get("uuid") == uuid] # list and not var bc wpilib repo keeps mutliple versions + if manifests.get(year) is None: + skipped.append( + f"**{name}** (`{path.name}`): skipped because the {year} marketplace manifest could not be fetched" + ) + continue + + candidates = [e for e in manifests[year] if e.get("uuid") == uuid] # WPILib repo keeps multiple versions if not candidates: skipped.append(f"**{name}** (`{path.name}`): not in the {year} marketplace") continue From b2323c50fabee82d2cbe95d0fd67a668c4e5f1ee Mon Sep 17 00:00:00 2001 From: Tim <73599525+timtogan@users.noreply.github.com> Date: Mon, 17 Aug 2026 21:23:31 -0700 Subject: [PATCH 4/4] Fix docstring in update vendordeps --- .github/scripts/update_vendordeps.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/.github/scripts/update_vendordeps.py b/.github/scripts/update_vendordeps.py index 7cd1d2d..9c372cc 100644 --- a/.github/scripts/update_vendordeps.py +++ b/.github/scripts/update_vendordeps.py @@ -11,8 +11,6 @@ Writes a markdown summary of what changed to the path given by --summary (used as the pull request body), and prints it to stdout. -Exits 0 whether or not anything changed; the workflow decides what to do -based on whether the git tree is dirty. """ import argparse