diff --git a/.github/workflows/sponsors.yml b/.github/workflows/sponsors.yml index 433caed..bd29fef 100644 --- a/.github/workflows/sponsors.yml +++ b/.github/workflows/sponsors.yml @@ -1,8 +1,9 @@ name: Sponsors -# Keeps docs/sponsors.json current so the website's sponsor wall features -# sponsors automatically — new sponsors appear within a day of sponsoring, -# lapsed ones roll off, no manual list edits. +# Keeps docs/sponsors.json and the README sponsor block current so the website's +# sponsor wall and the repo page feature sponsors automatically — new sponsors +# appear within a few hours of sponsoring, lapsed ones roll off, no manual list +# edits. # # The Sponsors GraphQL API rejects anonymous reads, so this runs here with a # token. If the default Actions token can't read the org's sponsorships, add @@ -11,12 +12,13 @@ name: Sponsors on: schedule: - - cron: '43 4 * * *' # daily, 04:43 UTC + - cron: '43 */3 * * *' # every 3 hours — a new sponsor shouldn't wait a day workflow_dispatch: # refresh on demand push: branches: [main] paths: - 'scripts/sponsors.mjs' + - 'scripts/lib/sponsorsReadme.mjs' - '.github/workflows/sponsors.yml' permissions: @@ -44,7 +46,7 @@ jobs: run: | # Stage first: on the very first run the file is untracked, and # `git diff` alone would report no change and skip the seed commit. - git add -A docs/sponsors.json + git add -A docs/sponsors.json README.md if git diff --cached --quiet; then echo "No change." exit 0 diff --git a/README.md b/README.md index dfa122b..115970e 100644 --- a/README.md +++ b/README.md @@ -237,7 +237,13 @@ Quickdraw is MIT-licensed with no paid tier — [sponsors](https://github.com/sp are what keep it that way. Sponsors get their logo on [the website](https://tryquickdraw.com/sponsors/) and here: -No sponsors yet — your logo could be the first. + + + + + +
tldraw
tldraw
+ ## Star history diff --git a/scripts/lib/sponsorsReadme.mjs b/scripts/lib/sponsorsReadme.mjs new file mode 100644 index 0000000..19811e9 --- /dev/null +++ b/scripts/lib/sponsorsReadme.mjs @@ -0,0 +1,39 @@ +// Renders the sponsor block that lives between the markers in README.md, so +// the repo page shows the same wall as the website. Pure string-in/string-out +// — scripts/sponsors.mjs does the file I/O. + +export const START = '' +export const END = '' + +const SPONSORS_PAGE = 'https://tryquickdraw.com/sponsors/' +const EMPTY = `No sponsors yet — your logo could be the first.` + +const esc = (s) => + String(s).replace(/&/g, '&').replace(//g, '>').replace(/"/g, '"') + +// GitHub strips styles from README HTML, so layout is a plain table (the +// all-contributors trick): avatar on top, name under it, PER_ROW to a row. +const PER_ROW = 6 + +export function renderSponsorsBlock(sponsors) { + if (!sponsors.length) return EMPTY + const cells = sponsors + .slice() + .sort((a, b) => (b.monthly ?? 0) - (a.monthly ?? 0) || (a.since ?? '').localeCompare(b.since ?? '')) + .map((s) => { + const avatar = s.avatar + (s.avatar.includes('?') ? '&' : '?') + 's=128' + return `${esc(s.name)}
${esc(s.name)}
` + }) + const rows = [] + for (let i = 0; i < cells.length; i += PER_ROW) rows.push(`\n${cells.slice(i, i + PER_ROW).join('\n')}\n`) + return `\n${rows.join('\n')}\n
` +} + +// Returns the README with the block between the markers replaced, or null +// when the markers are missing — the caller decides how loud to be about it. +export function replaceSponsorsBlock(readme, sponsors) { + const a = readme.indexOf(START) + const b = readme.indexOf(END) + if (a === -1 || b === -1 || b < a) return null + return `${readme.slice(0, a + START.length)}\n${renderSponsorsBlock(sponsors)}\n${readme.slice(b)}` +} diff --git a/scripts/sponsors.mjs b/scripts/sponsors.mjs index 05a6db6..2415ea3 100644 --- a/scripts/sponsors.mjs +++ b/scripts/sponsors.mjs @@ -3,6 +3,8 @@ // website's sponsor wall updates itself — nobody edits a list by hand when a // sponsorship starts or lapses. Same shape as star-history: CI runs this on a // schedule and commits the snapshot; the static build just reads the file. +// The same run rewrites the sponsor block between the markers in README.md, +// so the repo page never lags the website. // // Reading sponsorships needs an authenticated token. The Actions token can // usually read an org's public sponsors; if GitHub rejects it, add a classic @@ -11,12 +13,14 @@ // // GITHUB_TOKEN= node scripts/sponsors.mjs -import { writeFileSync, mkdirSync } from 'node:fs' +import { writeFileSync, mkdirSync, readFileSync } from 'node:fs' import { fileURLToPath } from 'node:url' import { dirname, join } from 'node:path' +import { replaceSponsorsBlock } from './lib/sponsorsReadme.mjs' const LOGIN = process.env.SPONSORS_LOGIN ?? 'quickdrawjs' -const OUT = join(dirname(fileURLToPath(import.meta.url)), '..', 'docs') +const ROOT = join(dirname(fileURLToPath(import.meta.url)), '..') +const OUT = join(ROOT, 'docs') const token = process.env.GITHUB_TOKEN ?? process.env.GH_TOKEN if (!token) { @@ -88,9 +92,32 @@ const sponsors = (conn.nodes ?? []) since: n.createdAt?.slice(0, 10) ?? null, })) -mkdirSync(OUT, { recursive: true }) -writeFileSync( - join(OUT, 'sponsors.json'), - JSON.stringify({ generatedAt: new Date().toISOString(), login: LOGIN, sponsors }, null, 2) + '\n', -) -console.log(`sponsors: wrote ${sponsors.length} sponsor(s) to docs/sponsors.json`) +// Only rewrite the snapshot when the wall actually changed — a fresh +// generatedAt alone would mean a commit (and a site deploy) on every run. +const snapshotPath = join(OUT, 'sponsors.json') +let previous = null +try { + previous = JSON.parse(readFileSync(snapshotPath, 'utf8')).sponsors +} catch { + // first run — no snapshot yet +} +if (JSON.stringify(previous) === JSON.stringify(sponsors)) { + console.log(`sponsors: ${sponsors.length} sponsor(s), unchanged`) +} else { + mkdirSync(OUT, { recursive: true }) + writeFileSync( + snapshotPath, + JSON.stringify({ generatedAt: new Date().toISOString(), login: LOGIN, sponsors }, null, 2) + '\n', + ) + console.log(`sponsors: wrote ${sponsors.length} sponsor(s) to docs/sponsors.json`) +} + +const readmePath = join(ROOT, 'README.md') +const readme = readFileSync(readmePath, 'utf8') +const next = replaceSponsorsBlock(readme, sponsors) +if (next === null) { + console.warn('sponsors: README.md has no sponsors markers — left it alone') +} else if (next !== readme) { + writeFileSync(readmePath, next) + console.log('sponsors: updated the README sponsor block') +}