diff --git a/.github/scripts/build-publish-summary.sh b/.github/scripts/build-publish-summary.sh index 69098e86..5e1ce23a 100755 --- a/.github/scripts/build-publish-summary.sh +++ b/.github/scripts/build-publish-summary.sh @@ -13,6 +13,13 @@ # "target_version": "0.2.2", # "status": "published" # published | up-to-date | failed # } +# +# For "published"/"up-to-date" cells, this renders a shields.io "Dynamic +# Regex Badge" that live-scrapes GitHub's public package-versions page for +# that pack + version's download count on every image load - no scraping +# infra or stored data of our own. NOTE: shields.io's dynamic/regex badge is +# documented as "experimental: may change or be removed at any time" - see +# CONTRIBUTING.md's "Releases & publishing" section. set -euo pipefail RESULTS_DIR="${1:?usage: build-publish-summary.sh }" @@ -52,6 +59,46 @@ is_ext_language() { return 1 } +# Escapes a string for safe embedding in an RE2 regex pattern (used to match +# a specific version tag literally in download_badge's search regex below). +regex_escape() { + printf '%s' "$1" | sed -E 's/[.[\*^$()+?{}|]/\\&/g' +} + +# Percent-encodes a string for use in a URL query string. jq's @uri (like +# several other encoders, e.g. .NET's HttpUtility.UrlEncode) follows RFC +# 2396's "mark" characters and leaves * ( ) ' ! unescaped. A bare "*" or +# unbalanced "(" / ")" surviving into the final markdown can be misread as +# emphasis syntax or prematurely close a markdown link's destination parens +# (this bit us once already - see PR discussion), so percent-encode those +# explicitly on top of jq's output. +url_encode() { + jq -rn --arg s "$1" '$s|@uri' | sed -e 's/\*/%2A/g' -e 's/(/%28/g' -e 's/)/%29/g' -e "s/'/%27/g" -e 's/!/%21/g' +} + +# Renders a shields.io Dynamic Regex Badge (see file header) for one +# package+version's live download count, labeled with the version and +# linked to that version's package page. +download_badge() { + local package="$1" version="$2" + local versions_url="https://github.com/${REPO}/pkgs/container/${package}/versions" + local tag_escaped + tag_escaped=$(regex_escape "$version") + # Finds this specific tag's row, then advances (non-greedily, since the + # page lists many tags) to the download icon/count that follows it. + local search="tag=${tag_escaped}\"[\\s\\S]*?octicon-download[\\s\\S]*?\\s*([\\d,]+)\\s*Version downloads" + + local encoded_url encoded_search encoded_replace encoded_label + encoded_url=$(url_encode "$versions_url") + encoded_search=$(url_encode "$search") + encoded_replace=$(url_encode '$1 downloads') + encoded_label=$(url_encode "$version") + + local badge_url="https://img.shields.io/badge/dynamic/regex?url=${encoded_url}&search=${encoded_search}&replace=${encoded_replace}&label=${encoded_label}&color=blue" + local pkg_url="https://github.com/${REPO}/pkgs/container/${package}?tag=${version}" + echo "[![${package} ${version} downloads](${badge_url})](${pkg_url})" +} + cell() { local lang="$1" type="$2" local entry @@ -70,11 +117,10 @@ cell() { version=$(echo "$entry" | jq -r '.target_version') status=$(echo "$entry" | jq -r '.status') previous=$(echo "$entry" | jq -r '.previous_version') - local url="https://github.com/${REPO}/pkgs/container/${package}?tag=${version}" case "$status" in - published) echo "[${version}](${url}) 🆕" ;; - up-to-date) echo "[${version}](${url})" ;; + published) echo "$(download_badge "$package" "$version") 🆕" ;; + up-to-date) echo "$(download_badge "$package" "$version")" ;; failed) if [ -n "$previous" ] && [ "$previous" != "null" ]; then echo "⚠️ publish failed (still \`${previous}\`)" @@ -88,7 +134,7 @@ cell() { echo "## Publish summary" echo -echo "_Generated by [publish.yml run #${GITHUB_RUN_NUMBER:-}](${RUN_URL}) — 🆕 marks a package republished by this run._" +echo "_Generated by [publish.yml run #${GITHUB_RUN_NUMBER:-}](${RUN_URL}) — 🆕 marks a package republished by this run. Download counts are live (fetched by shields.io on each view, not stored here) and may take a little while to reflect the newest activity._" echo echo "| Language | Queries (\`src\`) | Library (\`lib\`) | Extensions (\`ext\`) | Library sources (\`ext-library-sources\`) |" echo "| --- | --- | --- | --- | --- |" diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 6640302e..0909b90b 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -167,6 +167,20 @@ fails), aggregates their per-pack results into the publish-summary and CodeQL li version tables, and (on the release-cut `push` trigger only) upserts both tables into the GitHub Release's notes. +Each published/up-to-date cell in the publish-summary table is a shields.io **Dynamic Regex +Badge**, labeled with the version and showing that pack version's download count (e.g. `0.6.0 | +2,559 downloads`), linked through to that version's GHCR package page. The badge's `url`/`search` +params point shields at GitHub's public `.../pkgs/container//versions` page and scrape the +count live on every image load - there's no scraping infra, cron, or stored data of our own; the +number simply reflects whatever GitHub reports at view time (shields.io caches responses briefly, +so it can lag live activity by a bit). **This relies on shields.io's `dynamic/regex` badge type, +which is explicitly documented upstream as "experimental: may change or be removed at any time"** +— if it ever breaks or is removed, the affected cells will render as a broken image/`invalid` +badge rather than failing the workflow; see `download_badge()` in +[`build-publish-summary.sh`][build-publish-summary-script] for the implementation and its comments +on why the URL/regex must be percent-encoded a specific way (jq's `@uri`, plus manual `%2A`/`%28`/ +`%29` fixups) to survive round-tripping through markdown without corruption. + **Each `` × `` combination is checked and published completely independently.** For every matrix entry, the job compares the `version:` in that one pack's `qlpack.yml` on `main` to the version currently published on [GHCR][ghcr-packages], and only @@ -414,6 +428,7 @@ Please do get in touch (privacy@github.com) if you have any questions about this [detect-codeql-release-workflow]: ./.github/workflows/detect-codeql-release.yml [copilot-setup-steps-workflow]: ./.github/workflows/copilot-setup-steps.yml [pin-codeql-library-versions-script]: ./.github/scripts/pin-codeql-library-versions.sh +[build-publish-summary-script]: ./.github/scripts/build-publish-summary.sh [codeql-cli-binaries]: https://github.com/github/codeql-cli-binaries/releases [release-config]: ./.release.yml [patch-release-me]: https://github.com/42ByteLabs/patch-release-me