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