Files
CloudFlare-ImgBed/.github/workflows/sync-star-history.yml
T

250 lines
8.8 KiB
YAML

# Render with pinned official source and persist date/count history in Pages.
# The workflow deploys Pages artifacts only; it never creates repository commits.
name: Render Star History
on:
schedule:
- cron: "17 3 * * 1"
workflow_dispatch:
inputs:
force_bootstrap:
description: "Rebuild all Star History data from the GitHub API instead of the deployed Pages cache"
required: false
default: false
type: boolean
permissions:
contents: read
deployments: write
pages: write
id-token: write
concurrency:
group: star-history-pages
cancel-in-progress: true
env:
STAR_HISTORY_REPOSITORIES: marseventh/cloudflare-imgbed,marseventh/sanyue-imghub
STAR_HISTORY_SOURCE_REF: bcddc9d532b10bac7e0187a741288bf9cab17616
STAR_HISTORY_SOURCE_DIR: .star-history-upstream
STAR_HISTORY_SITE_DIR: .star-history-site
STAR_HISTORY_PAGES_URL: https://marseventh.github.io/CloudFlare-ImgBed
jobs:
render-and-deploy:
name: Render with official source and deploy to Pages
if: ${{ github.repository == 'MarSeventh/CloudFlare-ImgBed' }}
runs-on: ubuntu-latest
timeout-minutes: 15
environment:
name: github-pages
url: ${{ steps.deployment.outputs.page_url }}
steps:
- name: Check out this repository
uses: actions/checkout@v7
with:
persist-credentials: false
- name: Check out official Star History source
uses: actions/checkout@v7
with:
repository: star-history/star-history
ref: ${{ env.STAR_HISTORY_SOURCE_REF }}
path: ${{ env.STAR_HISTORY_SOURCE_DIR }}
persist-credentials: false
- name: Set up pnpm
uses: pnpm/action-setup@v6
with:
version: 9.15.9
run_install: false
- name: Set up Node.js
uses: actions/setup-node@v7
with:
node-version: "24"
cache: pnpm
cache-dependency-path: |
.star-history-upstream/pnpm-lock.yaml
.star-history-upstream/backend/pnpm-lock.yaml
- name: Install official renderer dependencies
shell: bash
run: |
set -euo pipefail
(cd "${STAR_HISTORY_SOURCE_DIR}" && pnpm install --frozen-lockfile)
(cd "${STAR_HISTORY_SOURCE_DIR}/backend" && pnpm install --frozen-lockfile)
- name: Refresh data and render official light/dark charts locally
id: render
shell: bash
env:
# Normal refreshes read the deployed Pages cache and current repository
# totals. A missing Pages cache is initialized once from timestamped
# stargazers; the hosted Star History chart service is never queried.
GITHUB_JOB_TOKEN: ${{ github.token }}
FORCE_BOOTSTRAP: ${{ github.event_name == 'workflow_dispatch' && inputs.force_bootstrap == true }}
run: |
set -euo pipefail
if [[ -z "${GITHUB_JOB_TOKEN}" ]]; then
echo "::error::The workflow GITHUB_TOKEN is unavailable."
exit 1
fi
data_file="${RUNNER_TEMP}/star-history-data.json"
server_log="${RUNNER_TEMP}/star-history-backend.log"
cleanup() {
if [[ -n "${server_pid:-}" ]]; then
kill "${server_pid}" 2>/dev/null || true
wait "${server_pid}" 2>/dev/null || true
fi
}
trap cleanup EXIT
refresh_args=(
--repository "${STAR_HISTORY_REPOSITORIES}"
--output "${data_file}"
--token-env GITHUB_JOB_TOKEN
)
# Pages JSON is normally the runtime source of truth. A first
# deployment or an explicitly forced bootstrap reconstructs history
# from GitHub's timestamped Stargazers API.
if [[ "${FORCE_BOOTSTRAP}" == "true" ]]; then
echo "[data] forced API bootstrap requested"
else
refresh_args+=(
--deployed-url "${STAR_HISTORY_PAGES_URL}/star-history-data.json"
)
fi
python3 .github/star-history/star_history.py refresh-data \
"${refresh_args[@]}"
# Seed the pinned official backend cache while leaving its JSDOM,
# XYChart, xkcd styling, theme handling, and SVGO renderer unchanged.
python3 .github/star-history/star_history.py patch-upstream \
--source-dir "${STAR_HISTORY_SOURCE_DIR}"
(cd "${STAR_HISTORY_SOURCE_DIR}/backend" && pnpm run build)
(
cd "${STAR_HISTORY_SOURCE_DIR}/backend"
STAR_HISTORY_DATA_PATH="${data_file}" pnpm exec tsx main.ts
) >"${server_log}" 2>&1 &
server_pid=$!
for attempt in {1..60}; do
if curl --fail --silent http://127.0.0.1:8080/healthz >/dev/null; then
break
fi
if ! kill -0 "${server_pid}" 2>/dev/null; then
cat "${server_log}" >&2
exit 1
fi
if [[ "${attempt}" == 60 ]]; then
cat "${server_log}" >&2
exit 1
fi
sleep 1
done
mkdir -p "${STAR_HISTORY_SITE_DIR}"
cp "${data_file}" "${STAR_HISTORY_SITE_DIR}/star-history-data.json"
if ! python3 .github/star-history/star_history.py render \
--repository "${STAR_HISTORY_REPOSITORIES}" \
--output-dir "${STAR_HISTORY_SITE_DIR}" \
--fallback-base-url "${STAR_HISTORY_PAGES_URL}"; then
cat "${server_log}" >&2
exit 1
fi
touch "${STAR_HISTORY_SITE_DIR}/.nojekyll"
(
cd "${STAR_HISTORY_SITE_DIR}"
sha256sum \
star-history-data.json \
star-history-dark.svg \
star-history-light.svg | LC_ALL=C sort > manifest.sha256
)
# A byte-stable same-day count skips the Pages deployment entirely.
current_dir="$(mktemp -d)"
changed=true
if curl --fail --silent --location \
"${STAR_HISTORY_PAGES_URL}/manifest.sha256" \
--output "${current_dir}/manifest.sha256" && \
cmp --silent \
"${STAR_HISTORY_SITE_DIR}/manifest.sha256" \
"${current_dir}/manifest.sha256"; then
changed=false
echo "The deployed Pages site is already current."
fi
echo "changed=${changed}" >> "${GITHUB_OUTPUT}"
- name: Configure GitHub Pages
if: steps.render.outputs.changed == 'true'
uses: actions/configure-pages@v6
- name: Upload Pages artifact
if: steps.render.outputs.changed == 'true'
uses: actions/upload-pages-artifact@v5
with:
path: ${{ env.STAR_HISTORY_SITE_DIR }}
- name: Deploy Pages artifact
if: steps.render.outputs.changed == 'true'
id: deployment
uses: actions/deploy-pages@v5
# Deployment records are not Git commits. Keep only the newest record so
# the repository's Deployments list does not grow every scheduled run.
- name: Delete old GitHub Pages deployment records
if: ${{ success() && steps.render.outcome == 'success' }}
uses: actions/github-script@v9
with:
github-token: ${{ github.token }}
script: |
const { owner, repo } = context.repo;
const environment = "github-pages";
const deployments = await github.paginate(
github.rest.repos.listDeployments,
{ owner, repo, environment, per_page: 100 },
);
deployments.sort((left, right) => {
const difference = Date.parse(right.created_at) - Date.parse(left.created_at);
return difference || right.id - left.id;
});
if (deployments.length <= 1) {
core.info(`Keeping ${deployments.length} ${environment} deployment record(s).`);
return;
}
const [currentDeployment, ...oldDeployments] = deployments;
for (const oldDeployment of oldDeployments) {
const { data: statuses } = await github.rest.repos.listDeploymentStatuses({
owner,
repo,
deployment_id: oldDeployment.id,
per_page: 1,
});
if (statuses[0]?.state !== "inactive") {
await github.rest.repos.createDeploymentStatus({
owner,
repo,
deployment_id: oldDeployment.id,
state: "inactive",
environment,
auto_inactive: false,
description: `Superseded by deployment ${currentDeployment.id}.`,
});
}
await github.rest.repos.deleteDeployment({
owner,
repo,
deployment_id: oldDeployment.id,
});
}