You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@tooling.apache.org by ar...@apache.org on 2026/09/15 08:43:33 UTC
(tooling-trusted-releases) branch arm updated: #1068 - notify workflow for Dockerfile pinned software versions
This is an automated email from the ASF dual-hosted git repository.
alitheg pushed a commit to branch arm
in repository https://gitbox.apache.org/repos/asf/tooling-trusted-releases.git
The following commit(s) were added to refs/heads/arm by this push:
new b9cf2d30 #1068 - notify workflow for Dockerfile pinned software versions
b9cf2d30 is described below
commit b9cf2d30e643e9c91830edba9661f9f40c634d3c
Author: Alastair McFarlane <ar...@apache.org>
AuthorDate: Tue Sep 15 09:43:10 2026 +0100
#1068 - notify workflow for Dockerfile pinned software versions
---
.github/workflows/tool-freshness.yml | 60 +++++++++
scripts/ci/check_dockerfile_tool_versions.py | 185 +++++++++++++++++++++++++++
2 files changed, 245 insertions(+)
diff --git a/.github/workflows/tool-freshness.yml b/.github/workflows/tool-freshness.yml
new file mode 100644
index 00000000..4d4288a6
--- /dev/null
+++ b/.github/workflows/tool-freshness.yml
@@ -0,0 +1,60 @@
+name: Tool freshness
+
+# Notice, weekly, when the external tools pinned in Dockerfile.alpine have newer
+# upstream releases. This raises or refreshes a tracking issue; it never blocks a
+# build or a pull request. A human tests the newer version before bumping it.
+
+on:
+ schedule:
+ # Mondays at 06:30 UTC, clear of the CodeQL cron.
+ - cron: '30 6 * * 1'
+ workflow_dispatch:
+
+permissions:
+ contents: read
+
+env:
+ UV_VERSION: '0.11.20'
+
+jobs:
+ check:
+ runs-on: ubuntu-latest
+ permissions:
+ contents: read
+ issues: write
+ steps:
+ - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
+ with:
+ persist-credentials: false
+ - uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0
+ with:
+ python-version: '3.14'
+ - name: Install uv
+ uses: astral-sh/setup-uv@20cfd1bf945f4377ade1205e4dbc17946fc9a30d # v10.0.1
+ with:
+ version: ${{ env.UV_VERSION }}
+ - name: Install dependencies
+ run: uv sync --frozen
+ - name: Check tool versions
+ id: check
+ env:
+ GITHUB_TOKEN: ${{ github.token }}
+ run: |
+ uv run --frozen python scripts/ci/check_dockerfile_tool_versions.py > tool-freshness-report.md
+ cat tool-freshness-report.md
+ - name: Open or update the tracking issue
+ if: steps.check.outputs.stale == 'true'
+ env:
+ GH_TOKEN: ${{ github.token }}
+ TITLE: Dockerfile tool updates available
+ run: |
+ number=$(gh issue list --state open --search "$TITLE in:title" --json number,title \
+ --jq "map(select(.title == \"$TITLE\")) | .[0].number // empty")
+ if [ -n "$number" ]
+ then
+ gh issue edit "$number" --body-file tool-freshness-report.md
+ echo "Updated issue #$number"
+ else
+ gh issue create --title "$TITLE" --body-file tool-freshness-report.md
+ echo "Opened a new tracking issue"
+ fi
diff --git a/scripts/ci/check_dockerfile_tool_versions.py b/scripts/ci/check_dockerfile_tool_versions.py
new file mode 100755
index 00000000..377c9f89
--- /dev/null
+++ b/scripts/ci/check_dockerfile_tool_versions.py
@@ -0,0 +1,185 @@
+#!/usr/bin/env python3
+
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements. See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership. The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied. See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+"""Notice when the external tools pinned in Dockerfile.alpine have newer upstream releases.
+
+This is notify only. It never fails the build; it prints a report of the pinned tools
+that are behind their latest upstream version, so a scheduled workflow can raise or
+refresh a tracking issue. Someone can then test the newer version before bumping it.
+
+The GitHub-hosted tools are resolved from their latest release. Apache RAT is resolved
+from ATR's own release catalogue, which already tracks Apache versions, rather than from
+GitHub.
+"""
+
+import asyncio
+import os
+import pathlib
+import re
+import sys
+from typing import Final, NamedTuple
+
+import aiohttp
+
+_DOCKERFILE: Final = pathlib.Path("Dockerfile.alpine")
+_GITHUB_API: Final = "https://api.github.com"
+
+# The tools resolved from a GitHub repository's latest release, keyed by the
+# Dockerfile ENV that pins them, with the binary name each produces.
+_GITHUB_TOOLS: Final[dict[str, tuple[str, str]]] = {
+ "SYFT_VERSION": ("anchore/syft", "syft"),
+ "PARLAY_VERSION": ("snyk/parlay", "parlay"),
+ "SBOMQS_VERSION": ("interlynk-io/sbomqs", "sbomqs"),
+ "CDXCLI_VERSION": ("CycloneDX/cyclonedx-cli", "cyclonedx-cli"),
+}
+
+# Apache RAT lives in ATR's catalogue feed under this project key.
+_RAT_PROJECT_KEY: Final = "creadur-rat"
+_RAT_VERSION_VAR: Final = "RAT_VERSION"
+_RELEASE_CATALOG_URL: Final = os.environ.get("RELEASE_CATALOG_URL", "https://release-catalog.apache.org/")
+_REQUEST_TIMEOUT: Final = aiohttp.ClientTimeout(total=30)
+
+
+class _Result(NamedTuple):
+ tool: str
+ current: str
+ latest: str | None
+
+
+def main() -> None:
+ asyncio.run(_run())
+
+
+def _emit_output(behind: list[_Result]) -> None:
+ # Hand the scheduled workflow a single flag for whether to raise an issue.
+ output_path = os.environ.get("GITHUB_OUTPUT")
+ if output_path is None:
+ return
+ with open(output_path, "a", encoding="utf-8") as handle:
+ handle.write(f"stale={'true' if behind else 'false'}\n")
+
+
+def _github_headers() -> dict[str, str]:
+ headers = {"Accept": "application/vnd.github+json"}
+ token = os.environ.get("GITHUB_TOKEN")
+ if token:
+ headers["Authorization"] = f"Bearer {token}"
+ return headers
+
+
+async def _github_latest(session: aiohttp.ClientSession, repo: str) -> str | None:
+ url = f"{_GITHUB_API}/repos/{repo}/releases/latest"
+ try:
+ async with session.get(url, headers=_github_headers()) as response:
+ if response.status != 200:
+ return None
+ data = await response.json()
+ except aiohttp.ClientError:
+ return None
+ tag = data.get("tag_name")
+ return tag.lstrip("v") if isinstance(tag, str) else None
+
+
+async def _github_result(session: aiohttp.ClientSession, var: str, current: str | None) -> _Result:
+ repo, label = _GITHUB_TOOLS[var]
+ latest = None if current is None else await _github_latest(session, repo)
+ return _Result(tool=label, current=current or "unknown", latest=latest)
+
+
+def _is_behind(current: str, latest: str) -> bool:
+ current_parsed = _parse_version(current)
+ latest_parsed = _parse_version(latest)
+ if (current_parsed is None) or (latest_parsed is None):
+ return current != latest
+ return latest_parsed > current_parsed
+
+
+def _parse_version(text: str) -> tuple[int, ...] | None:
+ try:
+ return tuple(int(part) for part in text.strip().lstrip("v").split("."))
+ except ValueError:
+ return None
+
+
+def _pinned_versions() -> dict[str, str]:
+ text = _DOCKERFILE.read_text(encoding="utf-8")
+ pinned: dict[str, str] = {}
+ for match in re.finditer(r'(?m)^ENV\s+([A-Z0-9_]+_VERSION)=("?)([^"\s]+)\2', text):
+ pinned[match.group(1)] = match.group(3)
+ return pinned
+
+
+async def _rat_latest(session: aiohttp.ClientSession) -> str | None:
+ url = _RELEASE_CATALOG_URL.rstrip("/") + "/project_releases.json"
+ try:
+ async with session.get(url) as response:
+ if response.status != 200:
+ return None
+ data = await response.json(content_type=None)
+ except aiohttp.ClientError:
+ return None
+ entry = data.get(_RAT_PROJECT_KEY) if isinstance(data, dict) else None
+ if not isinstance(entry, dict):
+ return None
+ candidates: list[tuple[tuple[int, ...], str]] = []
+ for release in entry.get("release", []):
+ revision = release.get("revision")
+ if isinstance(revision, str):
+ parsed = _parse_version(revision)
+ if parsed is not None:
+ candidates.append((parsed, revision))
+ return max(candidates)[1] if candidates else None
+
+
+async def _rat_result(session: aiohttp.ClientSession, current: str | None) -> _Result:
+ latest = None if current is None else await _rat_latest(session)
+ return _Result(tool="apache-rat", current=current or "unknown", latest=latest)
+
+
+def _report(behind: list[_Result], unknown: list[_Result]) -> None:
+ if behind:
+ print("## Dockerfile tool updates available\n")
+ print("These tools pinned in `Dockerfile.alpine` have newer upstream releases. Test the")
+ print("newer version before bumping it; this notice changes nothing on its own.\n")
+ print("| Tool | Pinned | Latest |")
+ print("| --- | --- | --- |")
+ for result in behind:
+ print(f"| {result.tool} | {result.current} | {result.latest} |")
+ print()
+ else:
+ print("## Dockerfile tools are up to date\n")
+ print("Every pinned tool matches its latest upstream release.\n")
+ if unknown:
+ names = ", ".join(sorted(result.tool for result in unknown))
+ print(f"Could not determine the latest version for: {names}.")
+
+
+async def _run() -> None:
+ pinned = _pinned_versions()
+ async with aiohttp.ClientSession(timeout=_REQUEST_TIMEOUT) as session:
+ github = [_github_result(session, var, pinned.get(var)) for var in _GITHUB_TOOLS]
+ results = list(await asyncio.gather(*github, _rat_result(session, pinned.get(_RAT_VERSION_VAR))))
+ behind = [result for result in results if (result.latest is not None) and _is_behind(result.current, result.latest)]
+ unknown = [result for result in results if result.latest is None]
+ _report(behind, unknown)
+ _emit_output(behind)
+
+
+if __name__ == "__main__":
+ sys.exit(main())
---------------------------------------------------------------------
To unsubscribe, e-mail: commits-unsubscribe@tooling.apache.org
For additional commands, e-mail: commits-help@tooling.apache.org