chore: add GHCR downloads badge and update workflow for automated fetching
This commit is contained in:
7
.github/badges/ghcr-downloads.json
vendored
Normal file
7
.github/badges/ghcr-downloads.json
vendored
Normal file
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"schemaVersion": 1,
|
||||
"label": "GHCR pulls",
|
||||
"message": "0",
|
||||
"color": "blue",
|
||||
"cacheSeconds": 3600
|
||||
}
|
||||
54
.github/workflows/badge-ghcr-downloads.yml
vendored
Normal file
54
.github/workflows/badge-ghcr-downloads.yml
vendored
Normal 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
|
||||
@@ -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>
|
||||
|
||||
107
scripts/update-ghcr-downloads-badge.mjs
Normal file
107
scripts/update-ghcr-downloads-badge.mjs
Normal 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);
|
||||
});
|
||||
Reference in New Issue
Block a user