diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index b4e8212..12fbda6 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -45,7 +45,14 @@ jobs: with: node-version: 24 + - name: Fetch homepage metrics + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: bash scripts/fetch-homepage-metrics.sh + - name: Install dependencies and build + env: + REQUIRE_LIVE_HOMEPAGE_METRICS: "true" run: | pnpm install --no-frozen-lockfile pnpm run build diff --git a/lib/docker.ts b/lib/docker.ts index fe71b88..80fd1ec 100644 --- a/lib/docker.ts +++ b/lib/docker.ts @@ -3,15 +3,29 @@ * This function is called at build time on the server side * @returns Promise Docker pull count or fallback value */ +const DOCKER_FETCH_TIMEOUT_MS = 10_000; +const DOCKER_PULLS_FALLBACK = 7_166_727; +const REQUIRE_LIVE_HOMEPAGE_METRICS = process.env.REQUIRE_LIVE_HOMEPAGE_METRICS === 'true'; + export async function getDockerPulls(): Promise { + const injectedPullsValue = process.env.HOMEPAGE_DOCKER_PULLS; + if (injectedPullsValue !== undefined) { + const injectedPulls = Number(injectedPullsValue); + if (Number.isInteger(injectedPulls) && injectedPulls > 0) { + return injectedPulls; + } + + throw new Error('Invalid injected Docker Hub homepage metrics'); + } + const endpoints = [ - 'https://hub.docker.com/v2/namespaces/rustfs/repositories/rustfs', 'https://hub.docker.com/v2/repositories/rustfs/rustfs/', + 'https://hub.docker.com/v2/namespaces/rustfs/repositories/rustfs', ]; for (const endpoint of endpoints) { const controller = new AbortController(); - const timeoutId = setTimeout(() => controller.abort(), 3500); + const timeoutId = setTimeout(() => controller.abort(), DOCKER_FETCH_TIMEOUT_MS); try { const response = await fetch(endpoint, { @@ -25,7 +39,7 @@ export async function getDockerPulls(): Promise { if (response.ok) { const json = await response.json() as { pull_count?: number }; - if (typeof json.pull_count === 'number') { + if (typeof json.pull_count === 'number' && Number.isFinite(json.pull_count)) { return json.pull_count; } } @@ -40,5 +54,9 @@ export async function getDockerPulls(): Promise { } } - return 6371731; + if (REQUIRE_LIVE_HOMEPAGE_METRICS) { + throw new Error('Unable to fetch live Docker Hub homepage metrics'); + } + + return DOCKER_PULLS_FALLBACK; } diff --git a/lib/github.ts b/lib/github.ts index 0546b61..131ed95 100644 --- a/lib/github.ts +++ b/lib/github.ts @@ -18,7 +18,13 @@ export interface GitHubMetrics { commits: number; } -const GITHUB_FETCH_TIMEOUT_MS = 3500; +const GITHUB_FETCH_TIMEOUT_MS = 10_000; + +const GITHUB_METRICS_FALLBACK: GitHubMetrics = { + stars: 30_530, + forks: 1_349, + commits: 5_089, +}; async function fetchGitHub( url: string, @@ -140,11 +146,29 @@ export async function getLatestCliRelease(): Promise { * @returns Promise */ export async function getGitHubMetrics(): Promise { - const fallback: GitHubMetrics = { - stars: 30000, - forks: 1300, - commits: 4800, - }; + const injectedMetricValues = [ + process.env.HOMEPAGE_GITHUB_STARS, + process.env.HOMEPAGE_GITHUB_FORKS, + process.env.HOMEPAGE_GITHUB_COMMITS, + ]; + + if (injectedMetricValues.some((value) => value !== undefined)) { + if (injectedMetricValues.some((value) => value === undefined)) { + throw new Error('Incomplete injected GitHub homepage metrics'); + } + + const injectedMetrics = { + stars: Number(injectedMetricValues[0]), + forks: Number(injectedMetricValues[1]), + commits: Number(injectedMetricValues[2]), + }; + + if (Object.values(injectedMetrics).every((value) => Number.isInteger(value) && value > 0)) { + return injectedMetrics; + } + + throw new Error('Invalid injected GitHub homepage metrics'); + } try { const [repoRes, commitsRes] = await Promise.all([ @@ -165,7 +189,7 @@ export async function getGitHubMetrics(): Promise { ]); if (!repoRes.ok || !commitsRes.ok) { - return fallback; + throw new Error(`GitHub metrics API returned ${repoRes.status}/${commitsRes.status}`); } const repo = await repoRes.json() as { stargazers_count?: number; forks_count?: number }; @@ -181,9 +205,9 @@ export async function getGitHubMetrics(): Promise { } return { - stars: repo.stargazers_count ?? fallback.stars, - forks: repo.forks_count ?? fallback.forks, - commits: Number.isFinite(commits) && commits > 0 ? commits : fallback.commits, + stars: repo.stargazers_count ?? GITHUB_METRICS_FALLBACK.stars, + forks: repo.forks_count ?? GITHUB_METRICS_FALLBACK.forks, + commits: Number.isFinite(commits) && commits > 0 ? commits : GITHUB_METRICS_FALLBACK.commits, }; } catch (error) { if (isAbortError(error)) { @@ -192,7 +216,11 @@ export async function getGitHubMetrics(): Promise { console.warn('Failed to fetch GitHub metrics:', error); } - return fallback; + if (process.env.REQUIRE_LIVE_HOMEPAGE_METRICS === 'true') { + throw new Error('Unable to fetch live GitHub homepage metrics', { cause: error }); + } + + return GITHUB_METRICS_FALLBACK; } } diff --git a/scripts/fetch-homepage-metrics.sh b/scripts/fetch-homepage-metrics.sh new file mode 100644 index 0000000..7ab3659 --- /dev/null +++ b/scripts/fetch-homepage-metrics.sh @@ -0,0 +1,34 @@ +#!/usr/bin/env bash + +set -euo pipefail + +: "${GITHUB_TOKEN:?GITHUB_TOKEN is required}" +: "${GITHUB_ENV:?GITHUB_ENV is required}" + +github_metrics="$({ + curl --fail --silent --show-error \ + --retry 3 --retry-all-errors --connect-timeout 10 --max-time 30 \ + -H "Accept: application/vnd.github+json" \ + -H "Authorization: Bearer ${GITHUB_TOKEN}" \ + -H "User-Agent: RustFS-Website" \ + --data-binary '{"query":"query { repository(owner: \"rustfs\", name: \"rustfs\") { stargazerCount forkCount defaultBranchRef { target { ... on Commit { history { totalCount } } } } } }"}' \ + https://api.github.com/graphql +})" + +docker_metrics="$({ + curl --fail --silent --show-error \ + --retry 3 --retry-all-errors --connect-timeout 10 --max-time 30 \ + 'https://hub.docker.com/v2/namespaces/rustfs/repositories?page_size=100&name=rustfs' +})" + +stars="$(jq -er '.data.repository.stargazerCount | if type == "number" and . > 0 then . else error("invalid stars") end' <<<"${github_metrics}")" +forks="$(jq -er '.data.repository.forkCount | if type == "number" and . > 0 then . else error("invalid forks") end' <<<"${github_metrics}")" +commits="$(jq -er '.data.repository.defaultBranchRef.target.history.totalCount | if type == "number" and . > 0 then . else error("invalid commits") end' <<<"${github_metrics}")" +docker_pulls="$(jq -er '.results[] | select(.name == "rustfs") | .pull_count | if type == "number" and . > 0 then . else error("invalid Docker pulls") end' <<<"${docker_metrics}")" + +{ + echo "HOMEPAGE_GITHUB_STARS=${stars}" + echo "HOMEPAGE_GITHUB_FORKS=${forks}" + echo "HOMEPAGE_GITHUB_COMMITS=${commits}" + echo "HOMEPAGE_DOCKER_PULLS=${docker_pulls}" +} >>"${GITHUB_ENV}"