chore: add GHCR downloads badge and update workflow for automated fetching

This commit is contained in:
GitHub Actions
2026-02-10 23:07:46 +00:00
parent 9b2d8e5455
commit 413f9609a1
4 changed files with 169 additions and 0 deletions

7
.github/badges/ghcr-downloads.json vendored Normal file
View File

@@ -0,0 +1,7 @@
{
"schemaVersion": 1,
"label": "GHCR pulls",
"message": "0",
"color": "blue",
"cacheSeconds": 3600
}

View File

@@ -0,0 +1,54 @@
name: "Badge: GHCR downloads"
on:
schedule:
# Update periodically (GitHub schedules may be delayed)
- cron: '17 * * * *'
workflow_dispatch: {}
permissions:
contents: write
packages: read
concurrency:
group: ghcr-downloads-badge
cancel-in-progress: false
jobs:
update:
runs-on: ubuntu-latest
steps:
- name: Checkout (main)
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
with:
ref: main
- name: Set up Node
uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6
with:
node-version: 20
- name: Update GHCR downloads badge
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
GHCR_OWNER: ${{ github.repository_owner }}
GHCR_PACKAGE: charon
BADGE_OUTPUT: .github/badges/ghcr-downloads.json
run: node scripts/update-ghcr-downloads-badge.mjs
- name: Commit and push (if changed)
shell: bash
run: |
set -euo pipefail
if git diff --quiet; then
echo "No changes."
exit 0
fi
git config user.name "github-actions[bot]"
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
git add .github/badges/ghcr-downloads.json
git commit -m "chore(badges): update GHCR downloads [skip ci]"
git push origin HEAD:main

View File

@@ -9,6 +9,7 @@
<p align="center">
<a href="https://www.repostatus.org/#active"><img src="https://www.repostatus.org/badges/latest/active.svg" alt="Project Status: Active The project is being actively developed." /></a>
<a href="https://hub.docker.com/r/wikid82/charon"><img src="https://img.shields.io/docker/pulls/wikid82/charon.svg" alt="Docker Pulls"></a>
<a href="https://github.com/users/Wikid82/packages/container/package/charon"><img src="https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/Wikid82/Charon/main/.github/badges/ghcr-downloads.json" alt="GHCR Pulls"></a>
<a href="https://github.com/Wikid82/charon/releases"><img src="https://img.shields.io/github/v/release/Wikid82/charon?include_prereleases" alt="Release"></a>
<br>
<a href="https://codecov.io/gh/Wikid82/Charon" ><img src="https://codecov.io/gh/Wikid82/Charon/branch/main/graph/badge.svg?token=RXSINLQTGE" alt="Code Coverage"/></a>

View File

@@ -0,0 +1,107 @@
const DEFAULT_OUTPUT = ".github/badges/ghcr-downloads.json";
const GH_API_BASE = "https://api.github.com";
const owner = process.env.GHCR_OWNER || process.env.GITHUB_REPOSITORY_OWNER;
const packageName = process.env.GHCR_PACKAGE || "charon";
const outputPath = process.env.BADGE_OUTPUT || DEFAULT_OUTPUT;
const token = process.env.GITHUB_TOKEN || "";
if (!owner) {
throw new Error("GHCR owner is required. Set GHCR_OWNER or GITHUB_REPOSITORY_OWNER.");
}
const headers = {
Accept: "application/vnd.github+json",
};
if (token) {
headers.Authorization = `Bearer ${token}`;
}
const formatCount = (value) => {
if (value >= 1_000_000_000) {
return `${(value / 1_000_000_000).toFixed(1).replace(/\.0$/, "")}B`;
}
if (value >= 1_000_000) {
return `${(value / 1_000_000).toFixed(1).replace(/\.0$/, "")}M`;
}
if (value >= 1_000) {
return `${(value / 1_000).toFixed(1).replace(/\.0$/, "")}k`;
}
return String(value);
};
const getNextLink = (linkHeader) => {
if (!linkHeader) {
return null;
}
const match = linkHeader.match(/<([^>]+)>;\s*rel="next"/);
return match ? match[1] : null;
};
const fetchPage = async (url) => {
const response = await fetch(url, { headers });
if (!response.ok) {
const detail = await response.text();
const error = new Error(`Request failed: ${response.status} ${response.statusText}`);
error.status = response.status;
error.detail = detail;
throw error;
}
const data = await response.json();
const link = response.headers.get("link");
return { data, next: getNextLink(link) };
};
const fetchAllVersions = async (baseUrl) => {
let url = `${baseUrl}?per_page=100`;
const versions = [];
while (url) {
const { data, next } = await fetchPage(url);
versions.push(...data);
url = next;
}
return versions;
};
const fetchVersionsWithFallback = async () => {
const userUrl = `${GH_API_BASE}/users/${owner}/packages/container/${packageName}/versions`;
try {
return await fetchAllVersions(userUrl);
} catch (error) {
if (error.status !== 404) {
throw error;
}
}
const orgUrl = `${GH_API_BASE}/orgs/${owner}/packages/container/${packageName}/versions`;
return fetchAllVersions(orgUrl);
};
const run = async () => {
const versions = await fetchVersionsWithFallback();
const totalDownloads = versions.reduce(
(sum, version) => sum + (version.download_count || 0),
0
);
const badge = {
schemaVersion: 1,
label: "GHCR pulls",
message: formatCount(totalDownloads),
color: "blue",
cacheSeconds: 3600,
};
const output = `${JSON.stringify(badge, null, 2)}\n`;
await import("node:fs/promises").then((fs) => fs.writeFile(outputPath, output));
console.log(`GHCR downloads: ${totalDownloads} -> ${outputPath}`);
};
run().catch((error) => {
console.error(error);
process.exit(1);
});