diff --git a/.github/AGENTS.d/readme-badges.md b/.github/AGENTS.d/readme-badges.md new file mode 100644 index 0000000000..9b18bf27cd --- /dev/null +++ b/.github/AGENTS.d/readme-badges.md @@ -0,0 +1,28 @@ +--- +paths: + - .github/workflows/readme-badges.yml +invariant: Render job on master only; both checkouts at github.sha; publish job contents: write only; no workflow_run trigger. +--- +# README badges workflow (ADR-3519) + +`.github/workflows/readme-badges.yml` renders + publishes README status +badges onto `badges` branch. Script rules, design check, carry: +[scripts/ci/AGENTS.d/readme-badges.md](../../scripts/ci/AGENTS.d/readme-badges.md). +Human guide: `docs/development/readme-badges.md`. + +- Guard: render job `if:` = policy repository + `https://github.com` + + `github.ref == 'refs/heads/master'`. Publish job `needs: render`, so + skipped render skips publish. Manual run on other branch -> nothing runs; + publish token never meets branch copy of workflow. Names come from + `.github/repository-security-policy.json`; `test_readme_badges.py` keeps + them equal. +- Checkout: both jobs `ref: ${{ github.sha }}`, `persist-credentials: false`. + Never `ref: master`: master push between jobs hands publisher other code + than renderer ran. +- Rights: top level `permissions: {}`; render job `actions: read` + + `contents: read`; publish job `contents: write` only. No other write scope. +- Trigger: `schedule` (minutes 7, 22, 37, 52) + `workflow_dispatch`. No + `workflow_run` (zizmor dangerous trigger; fork branch named `master` + matches `branches` filter). +- Ref: publish writes `refs/heads/badges` only, `force: false`; + GitHub-signed commit required. diff --git a/.github/AGENTS.md b/.github/AGENTS.md index 2caba47c6b..27de45844c 100644 --- a/.github/AGENTS.md +++ b/.github/AGENTS.md @@ -39,6 +39,7 @@ broken `.c` file). | `workflows/libvmaf-build-matrix.yml`, `/scripts/ci/run_meson_test.py`, `/core/test/test_meson_secret_env_sanitization.py` | [meson-sanitization](AGENTS.d/meson-sanitization.md) | Every workflow step running Meson/Ninja test invokes run_meson_test.py; resolve Python before sudo. | | `workflows/lint-and-format.yml`, `/scripts/sync-pelorus-interop.sh` | [pelorus-mirror](AGENTS.d/pelorus-mirror.md) | Pre-Commit job verifies exact declared commit of VMAFx/pelorus before pre-commit; no moving branch or tag. | | `/pyproject.toml`, `/renovate.json` | [python-requirement](AGENTS.d/python-requirement.md) | Root pyproject.toml keeps requires-python >=3.14 without patch component; renovate excludes root requires-python. | +| `workflows/readme-badges.yml` | [readme-badges](AGENTS.d/readme-badges.md) | Render job on master only; both checkouts at github.sha; publish job contents: write only; no workflow_run trigger. | | `workflows/release-please.yml`, `workflows/supply-chain.yml`, `/scripts/release/verify-release-version.sh`, `/dev/Containerfile` | [release-fan-out](AGENTS.d/release-fan-out.md) | Single root package draft release; release-publish environment; native release-build stage; tag-bound recovery. | | `/renovate.json`, `/scripts/ci/tests/test_renovate_file_patterns.py`, `/docs/development/dependency-bot.md` | [renovate-bot](AGENTS.d/renovate-bot.md) | Mend Renovate config in renovate.json; dependabot.yml disabled; RENOVATE_TOKEN repository secret. | | `workflows/required-aggregator.yml`, `/scripts/ci/check-aggregator-names.sh`, `/scripts/ci/plan-ci-impact.py` | [required-aggregator](AGENTS.d/required-aggregator.md) | Aggregator runs on drafts; gates route in-job without paths filters; display names <=30 chars and match aggregator. | diff --git a/.github/workflows/readme-badges.yml b/.github/workflows/readme-badges.yml new file mode 100644 index 0000000000..0ff767b493 --- /dev/null +++ b/.github/workflows/readme-badges.yml @@ -0,0 +1,122 @@ +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). + # Default branch only: a manual run picked on another branch runs nothing, + # so the publish token never meets a branch's own copy of this file. + # .github/repository-security-policy.json holds both names; a test keeps + # these in step with it. + if: >- + github.repository == 'VMAFx/vmafx' + && github.server_url == 'https://github.com' + && github.ref == 'refs/heads/master' + runs-on: ubuntu-26.04 + timeout-minutes: 10 + permissions: + actions: read + contents: read + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + # The commit this run started from, in both jobs: a master push + # between them cannot hand the publisher other code than the + # renderer ran. + ref: ${{ github.sha }} + 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: + # The commit this run started from, in both jobs: a master push + # between them cannot hand the publisher other code than the + # renderer ran. + ref: ${{ github.sha }} + 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..dd0c81a314 --- /dev/null +++ b/changelog.d/changed/readme-badges-uniform.md @@ -0,0 +1,5 @@ +- **Every README badge but the Praetor-managed governance 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/