From cd2911f3a69a80a6e8db99206fc1eeefcfec2c67 Mon Sep 17 00:00:00 2001 From: lusoris Date: Tue, 10 Nov 2026 14:40:01 +0100 Subject: [PATCH 1/7] ci(badges): draw the README status badges in one design The README header mixed four badge designs: GitHub's workflow badge.svg images, the Best Practices and Codacy service badges, a static Scorecard image without the score, and a flat-square backends row. GitHub's badge.svg cannot be restyled, so the ten status badges are now drawn by the repository in the for-the-badge style. The README Badges workflow runs every 15 minutes. readme_badges.py reads the newest decisive push run of each badged workflow on master and the public Scorecard, Best Practices and Codacy APIs; render.mjs draws the SVGs with the pinned badge-maker; the publish job commits them to the badges branch, signed by GitHub and never forced. A failed source keeps its value for six hours, then reads stale, and the run turns red. Requests go through the new ci_tier.https_json, which refuses non-https URLs and caps each answer; api_request uses it too and is now size-capped. check-readme refuses any README image outside the one design and runs as a pre-commit and pre-push hook; the Praetor governance badge is the one exception until Praetor issue 1893 lands. ADR-3519 records the design and why a schedule replaces workflow_run. Signed-off-by: lusoris --- .github/workflows/readme-badges.yml | 109 +++ .pre-commit-config.yaml | 16 + README.md | 40 +- changelog.d/changed/readme-badges-uniform.md | 5 + docs/README-STANDARD.md | 35 +- .../adr/3519-readme-badges-rendered-branch.md | 83 ++ .../3519-readme-badges-rendered-branch.md | 1 + docs/development/readme-badges.md | 112 +++ mkdocs.yml | 1 + scripts/ci/AGENTS-gates.md | 1 + scripts/ci/AGENTS.d/readme-badges.md | 24 + scripts/ci/ci_tier.py | 51 +- scripts/ci/readme_badges.py | 620 +++++++++++++++ scripts/ci/tests/test_ci_tier.py | 70 ++ scripts/ci/tests/test_readme_badges.py | 752 ++++++++++++++++++ tools/badges/package-lock.json | 87 ++ tools/badges/package.json | 14 + tools/badges/render.mjs | 62 ++ 18 files changed, 2035 insertions(+), 48 deletions(-) create mode 100644 .github/workflows/readme-badges.yml create mode 100644 changelog.d/changed/readme-badges-uniform.md create mode 100644 docs/adr/3519-readme-badges-rendered-branch.md create mode 100644 docs/adr/_index_fragments/3519-readme-badges-rendered-branch.md create mode 100644 docs/development/readme-badges.md create mode 100644 scripts/ci/AGENTS.d/readme-badges.md create mode 100644 scripts/ci/readme_badges.py create mode 100644 scripts/ci/tests/test_readme_badges.py create mode 100644 tools/badges/package-lock.json create mode 100644 tools/badges/package.json create mode 100644 tools/badges/render.mjs diff --git a/.github/workflows/readme-badges.yml b/.github/workflows/readme-badges.yml new file mode 100644 index 0000000000..5447adfb0e --- /dev/null +++ b/.github/workflows/readme-badges.yml @@ -0,0 +1,109 @@ +name: README Badges + +# Render the README status badges in one style (docs/development/readme-badges.md). +# scripts/ci/readme_badges.py reads the latest decisive push run of every badged +# workflow on master plus the OpenSSF Scorecard, OpenSSF Best Practices and +# Codacy public APIs; tools/badges/render.mjs draws the SVGs with the pinned +# shields.io renderer; the publish job commits them to the badges branch the +# README links. A source that fails keeps its last value for six hours, then +# reads "stale", and this run turns red so the failure is seen. + +on: + # Every 15 minutes: a badge lags a master run by at most that. A + # workflow_run trigger would be faster, but zizmor rates it a dangerous + # trigger, and its branch filter would also match a fork's branch named + # master. + schedule: + - cron: "7,22,37,52 * * * *" + workflow_dispatch: + +permissions: {} + +concurrency: + group: readme-badges + cancel-in-progress: false + +jobs: + render: + name: Read the sources and render + # github.com only: the job reads and the publish job writes through the + # GitHub API, which a mirror's token cannot reach (as in supply-chain.yml). + if: github.repository == 'VMAFx/vmafx' && github.server_url == 'https://github.com' + runs-on: ubuntu-26.04 + timeout-minutes: 10 + permissions: + actions: read + contents: read + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + ref: master + persist-credentials: false + - uses: actions/setup-python@6d4fd22b20465ccc0f7354476588e8df7c9ca0e0 # v7.1.0 + with: + python-version: "3.14.10" + - uses: actions/setup-node@949feb2413d6458794dcd2491c4babbbce0c15c1 # v7.1.0 + with: + node-version: "26.12.1" + - name: Install the pinned badge renderer + working-directory: tools/badges + run: npm ci --ignore-scripts --no-audit --no-fund + - name: Read the badge sources + env: + GITHUB_TOKEN: ${{ github.token }} + run: >- + python3 scripts/ci/readme_badges.py spec + --spec build/badges-spec.json --state build/badges/state.json + - name: Render the SVGs + run: node tools/badges/render.mjs build/badges-spec.json build/badges + - if: ${{ env.GITEA_ACTIONS != 'true' }} + uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2 + with: + name: readme-badges + path: build/badges/ + retention-days: 1 + if-no-files-found: error + - name: Upload artifact (Gitea) + if: ${{ env.GITEA_ACTIONS == 'true' }} + uses: actions/upload-artifact@c6a366c94c3e0affe28c06c8df20a878f24da3cf # v3.2.2 + with: + name: readme-badges + path: build/badges/ + retention-days: 1 + if-no-files-found: error + + publish: + name: Publish to the badges branch + needs: render + runs-on: ubuntu-26.04 + timeout-minutes: 5 + # contents: write is the narrowest token right that can create a commit; + # the job writes refs/heads/badges only, and the master ruleset refuses + # this token on master. + permissions: + contents: write + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + ref: master + persist-credentials: false + - if: ${{ env.GITEA_ACTIONS != 'true' }} + uses: actions/download-artifact@9000827ccba6bdab643e8b6fd33ac0654aef8333 # v8.0.2 + with: + name: readme-badges + path: build/badges + - name: Download artifact (Gitea) + if: ${{ env.GITEA_ACTIONS == 'true' }} + uses: actions/download-artifact@a9bc5e6ef2cb54c177f32aa5726adaa15e7e2d59 # v3.1.0 + with: + name: readme-badges + path: build/badges + - uses: actions/setup-python@6d4fd22b20465ccc0f7354476588e8df7c9ca0e0 # v7.1.0 + with: + python-version: "3.14.10" + - name: Commit the badges + env: + GITHUB_TOKEN: ${{ github.token }} + run: python3 scripts/ci/readme_badges.py publish --dir build/badges + - name: Fail the run when a source failed + run: python3 scripts/ci/readme_badges.py verdict --state build/badges/state.json diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index f7ee76802f..244af4319d 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -808,6 +808,22 @@ repos: files: '^(scripts/ci/(check-workflow-size\.py|tests/test_check_workflow_size\.py)|\.pre-commit-config\.yaml)$' pass_filenames: false + - id: readme-badge-design + name: Every README image keeps the one badge design (ADR-3519) + entry: python3 -B scripts/ci/readme_badges.py check-readme --readme README.md + language: system + files: '^(README\.md|scripts/ci/readme_badges\.py)$' + pass_filenames: false + stages: [pre-commit, pre-push] + + - id: test-readme-badges + name: README badge renderer and design check keep positive, negative and boundary cases (ADR-3519) + entry: bash -c 'python3 -B scripts/ci/tests/test_readme_badges.py && python3 -B scripts/ci/tests/test_ci_tier.py' + language: system + files: '^(README\.md|scripts/ci/(readme_badges|ci_tier|tests/test_readme_badges|tests/test_ci_tier)\.py|tools/badges/.*|\.github/workflows/.*\.yml|\.pre-commit-config\.yaml)$' + pass_filenames: false + stages: [pre-commit, pre-push] + - id: check-hosted-runner-labels name: Workflow jobs name an explicit hosted Ubuntu image, an older one only with a dated fallback entry: python3 scripts/ci/check_hosted_runner_labels.py diff --git a/README.md b/README.md index 662bab473b..4f2bdd28a3 100644 --- a/README.md +++ b/README.md @@ -11,17 +11,17 @@ [![Release](https://img.shields.io/static/v1?label=release&message=v1.0.0-rc.3&style=for-the-badge&color=3b82f6&labelColor=111a35)](https://github.com/VMAFx/vmafx/releases) - [![Tests](https://github.com/VMAFx/vmafx/actions/workflows/tests-and-quality-gates.yml/badge.svg?branch=master&event=push)](https://github.com/VMAFx/vmafx/actions/workflows/tests-and-quality-gates.yml) - [![Builds](https://github.com/VMAFx/vmafx/actions/workflows/libvmaf-build-matrix.yml/badge.svg?branch=master&event=push)](https://github.com/VMAFx/vmafx/actions/workflows/libvmaf-build-matrix.yml) - [![Lint](https://github.com/VMAFx/vmafx/actions/workflows/lint-and-format.yml/badge.svg?branch=master&event=push)](https://github.com/VMAFx/vmafx/actions/workflows/lint-and-format.yml) - [![FFmpeg](https://github.com/VMAFx/vmafx/actions/workflows/ffmpeg-integration.yml/badge.svg?branch=master&event=push)](https://github.com/VMAFx/vmafx/actions/workflows/ffmpeg-integration.yml) - [![Go](https://github.com/VMAFx/vmafx/actions/workflows/go-ci.yml/badge.svg?branch=master&event=push)](https://github.com/VMAFx/vmafx/actions/workflows/go-ci.yml) - [![Rust](https://github.com/VMAFx/vmafx/actions/workflows/rust-ci.yml/badge.svg?branch=master&event=push)](https://github.com/VMAFx/vmafx/actions/workflows/rust-ci.yml) + [![Tests](https://raw.githubusercontent.com/VMAFx/vmafx/badges/tests.svg)](https://github.com/VMAFx/vmafx/actions/workflows/tests-and-quality-gates.yml) + [![Builds](https://raw.githubusercontent.com/VMAFx/vmafx/badges/builds.svg)](https://github.com/VMAFx/vmafx/actions/workflows/libvmaf-build-matrix.yml) + [![Lint](https://raw.githubusercontent.com/VMAFx/vmafx/badges/lint.svg)](https://github.com/VMAFx/vmafx/actions/workflows/lint-and-format.yml) + [![FFmpeg](https://raw.githubusercontent.com/VMAFx/vmafx/badges/ffmpeg.svg)](https://github.com/VMAFx/vmafx/actions/workflows/ffmpeg-integration.yml) + [![Go](https://raw.githubusercontent.com/VMAFx/vmafx/badges/go.svg)](https://github.com/VMAFx/vmafx/actions/workflows/go-ci.yml) + [![Rust](https://raw.githubusercontent.com/VMAFx/vmafx/badges/rust.svg)](https://github.com/VMAFx/vmafx/actions/workflows/rust-ci.yml) - [![Security](https://github.com/VMAFx/vmafx/actions/workflows/security-scans.yml/badge.svg?branch=master&event=push)](https://github.com/VMAFx/vmafx/actions/workflows/security-scans.yml) - [![OpenSSF Scorecard](https://img.shields.io/badge/OpenSSF-Scorecard-06b6d4?style=for-the-badge&labelColor=111a35)](https://scorecard.dev/viewer/?uri=github.com/VMAFx/vmafx) - [![OpenSSF Best Practices](https://www.bestpractices.dev/projects/14549/badge)](https://www.bestpractices.dev/projects/14549) - [![Codacy](https://app.codacy.com/project/badge/Grade/ca74eb98b9b94cfc8d60d965b836b55d)](https://app.codacy.com/gh/VMAFx/vmafx/dashboard) + [![Security](https://raw.githubusercontent.com/VMAFx/vmafx/badges/security.svg)](https://github.com/VMAFx/vmafx/actions/workflows/security-scans.yml) + [![OpenSSF Scorecard](https://raw.githubusercontent.com/VMAFx/vmafx/badges/scorecard.svg)](https://scorecard.dev/viewer/?uri=github.com/VMAFx/vmafx) + [![OpenSSF Best Practices](https://raw.githubusercontent.com/VMAFx/vmafx/badges/best-practices.svg)](https://www.bestpractices.dev/projects/14549) + [![Codacy](https://raw.githubusercontent.com/VMAFx/vmafx/badges/codacy.svg)](https://app.codacy.com/gh/VMAFx/vmafx/dashboard) [![CodeRabbit](https://img.shields.io/badge/Reviews-CodeRabbit-8b5cf6?style=for-the-badge&logo=coderabbit&logoColor=white&labelColor=111a35)](https://coderabbit.ai) [![HISS-21](https://img.shields.io/badge/Standards-HISS--21-06b6d4?style=for-the-badge&logo=nasa&labelColor=111a35)](AGENTS.md) @@ -105,16 +105,16 @@ and output options. For compressed inputs such as MP4, use ## 🧩 Backends at a glance -[![C23](https://img.shields.io/badge/C-23-3b82f6?style=flat-square&labelColor=111a35)](docs/principles.md) -[![C++23](https://img.shields.io/badge/C%2B%2B-23-3b82f6?style=flat-square&labelColor=111a35)](docs/principles.md) -[![Go](https://img.shields.io/badge/Go-1.27-06b6d4?style=flat-square&labelColor=111a35)](go.mod) -[![Rust](https://img.shields.io/badge/Rust-2024-8b5cf6?style=flat-square&labelColor=111a35)](bindings/rust/vmafx-sys/Cargo.toml) -[![Python](https://img.shields.io/badge/Python-3.14%2B-3b82f6?style=flat-square&labelColor=111a35)](pyproject.toml) -[![CUDA](https://img.shields.io/badge/CUDA-13.4-06b6d4?style=flat-square&labelColor=111a35)](docs/backends/cuda/overview.md) -[![ROCm](https://img.shields.io/badge/ROCm-10.1-06b6d4?style=flat-square&labelColor=111a35)](docs/backends/hip/overview.md) -[![SYCL](https://img.shields.io/badge/SYCL-oneAPI-06b6d4?style=flat-square&labelColor=111a35)](docs/backends/sycl/overview.md) -[![Metal](https://img.shields.io/badge/Metal-Apple_Silicon-06b6d4?style=flat-square&labelColor=111a35)](docs/backends/metal/index.md) -[![FFmpeg](https://img.shields.io/badge/FFmpeg-n9.0.2-8b5cf6?style=flat-square&labelColor=111a35)](docs/usage/ffmpeg.md) +[![C23](https://img.shields.io/badge/C-23-3b82f6?style=for-the-badge&labelColor=111a35)](docs/principles.md) +[![C++23](https://img.shields.io/badge/C%2B%2B-23-3b82f6?style=for-the-badge&labelColor=111a35)](docs/principles.md) +[![Go](https://img.shields.io/badge/Go-1.27-06b6d4?style=for-the-badge&labelColor=111a35)](go.mod) +[![Rust](https://img.shields.io/badge/Rust-2024-8b5cf6?style=for-the-badge&labelColor=111a35)](bindings/rust/vmafx-sys/Cargo.toml) +[![Python](https://img.shields.io/badge/Python-3.14%2B-3b82f6?style=for-the-badge&labelColor=111a35)](pyproject.toml) +[![CUDA](https://img.shields.io/badge/CUDA-13.4-06b6d4?style=for-the-badge&labelColor=111a35)](docs/backends/cuda/overview.md) +[![ROCm](https://img.shields.io/badge/ROCm-10.1-06b6d4?style=for-the-badge&labelColor=111a35)](docs/backends/hip/overview.md) +[![SYCL](https://img.shields.io/badge/SYCL-oneAPI-06b6d4?style=for-the-badge&labelColor=111a35)](docs/backends/sycl/overview.md) +[![Metal](https://img.shields.io/badge/Metal-Apple_Silicon-06b6d4?style=for-the-badge&labelColor=111a35)](docs/backends/metal/index.md) +[![FFmpeg](https://img.shields.io/badge/FFmpeg-n9.0.2-8b5cf6?style=for-the-badge&labelColor=111a35)](docs/usage/ffmpeg.md) Every GPU-backed feature extractor has at least one device twin, and each twin is held to the CPU reference by the cross-backend parity gate. The coverage diff --git a/changelog.d/changed/readme-badges-uniform.md b/changelog.d/changed/readme-badges-uniform.md new file mode 100644 index 0000000000..177c50ad57 --- /dev/null +++ b/changelog.d/changed/readme-badges-uniform.md @@ -0,0 +1,5 @@ +- **Every README badge now has the same `for-the-badge` design, and the status badges come from this project's own render.** + `.github/workflows/readme-badges.yml` draws the seven workflow badges and the OpenSSF Scorecard, + OpenSSF Best Practices and Codacy badges every 15 minutes onto the `badges` branch: a workflow badge + shows the newest finished master push run, so a run in progress never hides a red one, and the + Scorecard badge now shows the score. See `docs/development/readme-badges.md` (ADR-3519). diff --git a/docs/README-STANDARD.md b/docs/README-STANDARD.md index 8e2cef2195..59b407710e 100644 --- a/docs/README-STANDARD.md +++ b/docs/README-STANDARD.md @@ -39,10 +39,11 @@ the VMAFx banner and documentation site. | `text` | `#e2e8f0` | light text on dark fills | | `muted` | `#94a3b8` | secondary text in the banner | -The static badge messages use the three accent colours and set -`labelColor=111a35` so the block reads as one band. Workflow-status badges are -GitHub's own and keep its green, red or grey colour, because that colour is the -status. +Every badge uses `style=for-the-badge` and `labelColor=111a35`, so the page +shows one badge design. Static badges take their message colour from the three +accents. Status badges (workflow, Scorecard, Best Practices, Codacy) keep a +status colour, because that colour is the status: green `#1f883d`, amber +`#9a6700`, red `#cf222e`, grey `#6e7781` for unknown or stale. ## Hero banner @@ -58,15 +59,15 @@ status. ## Badge block All badges at the top sit in one block directly under the banner, inside the -centred header; static badges use `style=for-the-badge`. The groups always +centred header; every badge uses `style=for-the-badge`. The groups always come in this order; a repository leaves out a group it has nothing for, but never reorders them: 1. **Release / version**: the current release as a static badge that release-please keeps up to date (see "Badge sources"). -2. **CI / build**: one workflow-status badge per main workflow, with - `branch=&event=push`. The badge label is the workflow name, so a - red badge points at the workflow of the same name. +2. **CI / build**: one status badge per main workflow, showing its latest + decisive push run on the default branch. The badge label is the workflow + name, so a red badge points at the workflow of the same name. 3. **Security / scorecard**: the security workflow, the OpenSSF Scorecard, the OpenSSF Best Practices level and the review bots that check every pull request (Codacy, CodeRabbit), where the project has them. @@ -80,9 +81,16 @@ reviewer can check the order without rendering the page. Version badges for languages, toolchains, backends and integrations (C, Go, Rust, Python, CUDA, ROCm and the like) are not part of the top block. Where -they help, they go in one compact row (`style=flat-square`) inside the section -they describe; for VMAFx that is "Backends at a glance". No `for-the-badge` -badge appears below the header. +they help, they go in one row inside the section they describe, in the same +`for-the-badge` style as the header; for VMAFx that is "Backends at a glance". + +One badge keeps another design: the governance badge inside the +`praetor:readme-governance` block, which the Praetor tool writes and checks. +That block is not edited by hand. + +`python3 scripts/ci/readme_badges.py check-readme` enforces the one design on +every commit that touches `README.md`; see +[README status badges](development/readme-badges.md#checks). A static badge is built from shields.io's path form, `https://img.shields.io/badge/