|
| 1 | +import { writeFileSync } from 'node:fs' |
| 2 | + |
| 3 | +const OUTPUT_FILE = './sponsors.json' |
| 4 | + |
| 5 | +/** |
| 6 | + * Fastify's sponsorship tiers, defined by their minimum monthly contribution in |
| 7 | + * dollars. A sponsor is assigned the highest tier whose threshold its monthly |
| 8 | + * contribution meets (amounts between tiers round down). Sponsors below tier 1 |
| 9 | + * are not considered tier sponsors. |
| 10 | + */ |
| 11 | +const TIERS = [ |
| 12 | + { level: 1, monthly: 5 }, |
| 13 | + { level: 2, monthly: 50 }, |
| 14 | + { level: 3, monthly: 100 }, |
| 15 | + { level: 4, monthly: 300 }, |
| 16 | +] |
| 17 | + |
| 18 | +/** |
| 19 | + * Fetches all sponsors of an organization from GitHub Sponsors and Open Collective, |
| 20 | + * keeps only the recurring tier sponsors (monthly contribution within a tier), |
| 21 | + * sorts them by monthly amount descending and flags those that stopped paying. |
| 22 | + * The result is logged and written to a JSON file for later inspection. |
| 23 | + * @param {{ client: import('../github-api.js').default, logger: import('pino').Logger }} deps - Dependencies. |
| 24 | + * @param {{ org: string }} options - Command options. |
| 25 | + * @returns {Promise<void>} |
| 26 | + */ |
| 27 | +export default async function sponsors ({ client, logger }, { org }) { |
| 28 | + logger.info('Running sponsors command for organization: %s', org) |
| 29 | + |
| 30 | + const githubSponsors = await client.getGithubSponsors(org) |
| 31 | + logger.info('Fetched %s GitHub sponsorships', githubSponsors.length) |
| 32 | + |
| 33 | + const openCollectiveSponsors = await client.getOpenCollectiveSponsors(org) |
| 34 | + logger.info('Fetched %s Open Collective backers', openCollectiveSponsors.length) |
| 35 | + |
| 36 | + // Keep only recurring contributions that reach at least tier 1, tag each with |
| 37 | + // its tier, and order the combined list by monthly contribution descending. |
| 38 | + const sponsorList = [...githubSponsors, ...openCollectiveSponsors] |
| 39 | + .filter((sponsor) => tierFor(sponsor.monthlyAmount) !== null) |
| 40 | + .map((sponsor) => ({ ...sponsor, tierLevel: tierFor(sponsor.monthlyAmount) })) |
| 41 | + .sort((a, b) => b.monthlyAmount - a.monthlyAmount) |
| 42 | + |
| 43 | + logger.info('Tier sponsors (%s), ordered by monthly amount:', sponsorList.length) |
| 44 | + for (const sponsor of sponsorList) { |
| 45 | + logger.info( |
| 46 | + '- tier %s · $%s/mo · %s [%s]%s', |
| 47 | + sponsor.tierLevel, |
| 48 | + sponsor.monthlyAmount, |
| 49 | + displayName(sponsor), |
| 50 | + sponsor.source, |
| 51 | + sponsor.lapsed ? ' · ⚠ lapsed' : '' |
| 52 | + ) |
| 53 | + } |
| 54 | + |
| 55 | + const flagged = sponsorList.filter((sponsor) => sponsor.lapsed) |
| 56 | + if (flagged.length > 0) { |
| 57 | + logger.warn('%s lapsed tier sponsor(s) need attention:', flagged.length) |
| 58 | + for (const sponsor of flagged) { |
| 59 | + logger.warn( |
| 60 | + ' ⚠ tier %s · %s [%s] — last charged %s', |
| 61 | + sponsor.tierLevel, |
| 62 | + displayName(sponsor), |
| 63 | + sponsor.source, |
| 64 | + sponsor.lastChargedAt ?? 'unknown' |
| 65 | + ) |
| 66 | + } |
| 67 | + } |
| 68 | + |
| 69 | + const result = { |
| 70 | + tiers: TIERS, |
| 71 | + sponsors: sponsorList, |
| 72 | + flagged, |
| 73 | + } |
| 74 | + |
| 75 | + writeFileSync(OUTPUT_FILE, JSON.stringify(result, null, 2)) |
| 76 | + logger.info('Sponsors written to %s', OUTPUT_FILE) |
| 77 | +} |
| 78 | + |
| 79 | +/** |
| 80 | + * Returns the tier level for a given normalized monthly contribution, or null when |
| 81 | + * the contribution is missing or below the lowest tier. |
| 82 | + * @param {number|null} monthlyAmount - The normalized monthly contribution in dollars. |
| 83 | + * @returns {number|null} The tier level (1-4) or null. |
| 84 | + */ |
| 85 | +function tierFor (monthlyAmount) { |
| 86 | + if (monthlyAmount === null || monthlyAmount < TIERS[0].monthly) { |
| 87 | + return null |
| 88 | + } |
| 89 | + let level = null |
| 90 | + for (const tier of TIERS) { |
| 91 | + if (monthlyAmount >= tier.monthly) { |
| 92 | + level = tier.level |
| 93 | + } |
| 94 | + } |
| 95 | + return level |
| 96 | +} |
| 97 | + |
| 98 | +/** |
| 99 | + * Builds a human-friendly label for a sponsor. |
| 100 | + * @param {import('../github-api.js').Sponsor} sponsor - The sponsor. |
| 101 | + * @returns {string} The display label. |
| 102 | + */ |
| 103 | +function displayName (sponsor) { |
| 104 | + return sponsor.name ? `${sponsor.login} (${sponsor.name})` : sponsor.login |
| 105 | +} |
0 commit comments