Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 7 additions & 2 deletions .github/workflows/deploy-cloudflare.yml
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ on:
types: [completed]
branches: [main]
workflow_dispatch:
schedule:
- cron: '17 3 * * *'

permissions:
contents: read
Expand All @@ -21,7 +23,8 @@ jobs:
github.event.workflow_run.conclusion == 'success' &&
github.event.workflow_run.event == 'push' &&
github.event.workflow_run.head_sha == github.sha) ||
(github.event_name == 'workflow_dispatch' && github.ref == 'refs/heads/main')
(github.event_name == 'workflow_dispatch' && github.ref == 'refs/heads/main') ||
github.event_name == 'schedule'
runs-on: ubuntu-latest
timeout-minutes: 20
environment:
Expand All @@ -43,7 +46,9 @@ jobs:
run: npm ci

- name: Build production assets
run: npm run build
run: npm run sponsors:generate && npm run build
env:
GITHUB_TOKEN: ${{ github.token }}

- name: Deploy to Cloudflare
run: npx wrangler deploy
Expand Down
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
"scripts": {
"ng": "ng",
"docs:generate": "tsx scripts/generate-docs.ts",
"sponsors:generate": "tsx scripts/generate-sponsors.ts",
"prestart": "npm run docs:generate",
"start": "ng serve",
"prestart:ja": "npm run docs:generate",
Expand All @@ -17,7 +18,7 @@
"build": "ng build && npm run static:prepare && pagefind --site dist/capacitor-plugins-docs/browser --output-subdir pagefind && tsx --test scripts/build-output.test.ts",
"watch": "ng build --watch --configuration development",
"pretest": "npm run docs:generate",
"test": "tsx --test scripts/docgen-readme.test.ts scripts/html-policy.test.ts scripts/markdown-headings.test.ts scripts/package-markdown.test.ts scripts/package-repository.test.ts scripts/bilingual-update-blocker.test.ts scripts/site-contract.test.ts && ng test --watch=false",
"test": "tsx --test scripts/docgen-readme.test.ts scripts/html-policy.test.ts scripts/markdown-headings.test.ts scripts/package-markdown.test.ts scripts/package-repository.test.ts scripts/github-sponsors.test.ts scripts/bilingual-update-blocker.test.ts scripts/site-contract.test.ts && ng test --watch=false",
"fmt": "prettier --write .",
"fmt:check": "prettier --check .",
"deploy": "npm run build && wrangler deploy",
Expand Down
29 changes: 29 additions & 0 deletions scripts/build-output.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import assert from 'node:assert/strict';
import { access, constants, readdir, readFile, stat } from 'node:fs/promises';
import { join } from 'node:path';
import test from 'node:test';
import { CURRENT_SPONSORS, PAST_SPONSORS } from '../src/app/generated/sponsors.generated';

test('places locale-specific static 404 pages in the browser output', async () => {
const [english, japanese] = await Promise.all([
Expand Down Expand Up @@ -52,6 +53,34 @@ test('prerender output includes localized SEO metadata', async () => {
assert.match(html, /name="twitter:card" content="summary_large_image"/);
});

test('prerenders current and past public sponsors in both locales', async () => {
const [english, japanese] = await Promise.all([
readFile(
new URL('../dist/capacitor-plugins-docs/browser/support/index.html', import.meta.url),
'utf8',
),
readFile(
new URL('../dist/capacitor-plugins-docs/browser/ja/support/index.html', import.meta.url),
'utf8',
),
]);

for (const html of [english, japanese]) {
for (const sponsor of [...CURRENT_SPONSORS, ...PAST_SPONSORS]) {
assert.match(html, new RegExp(`href="${sponsor.profileUrl}"`));
}
assert.doesNotMatch(html, /\$\d/);
}
if (CURRENT_SPONSORS.length > 0) {
assert.match(english, />Current sponsors</);
assert.match(japanese, />現在のスポンサー</);
}
if (PAST_SPONSORS.length > 0) {
assert.match(english, />Past sponsors</);
assert.match(japanese, />過去のスポンサー</);
}
});

test('Japanese home prerender uses slashless canonical SEO URLs', async () => {
const html = await readFile(
new URL('../dist/capacitor-plugins-docs/browser/ja/index.html', import.meta.url),
Expand Down
10 changes: 10 additions & 0 deletions scripts/generate-sponsors.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import { writeFile } from 'node:fs/promises';
import { join } from 'node:path';
import { fetchPublicSponsors, sponsorsModule } from './github-sponsors';

const outputPath = join(process.cwd(), 'src/app/generated/sponsors.generated.ts');
const sponsors = await fetchPublicSponsors('rdlabo', process.env['GITHUB_TOKEN'] ?? '');
await writeFile(outputPath, sponsorsModule(sponsors));
console.log(
`Generated ${sponsors.current.length} current and ${sponsors.past.length} past public sponsors.`,
);
118 changes: 118 additions & 0 deletions scripts/github-sponsors.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
import assert from 'node:assert/strict';
import test from 'node:test';
import { fetchPublicSponsors, sponsorsModule } from './github-sponsors';

function githubResponse(body: unknown): Response {
return new Response(JSON.stringify(body), {
status: 200,
headers: { 'Content-Type': 'application/json' },
});
}

test('fetches only valid public sponsors and sorts by tier without publishing amounts', async () => {
const requests: { activeOnly: boolean; cursor: string | null }[] = [];
const smallSponsor = {
sponsorEntity: {
login: 'small',
name: null,
avatarUrl: 'https://avatars.githubusercontent.com/u/1?v=4',
url: 'https://github.com/small',
},
tier: { monthlyPriceInDollars: 5 },
};
const largeSponsor = {
sponsorEntity: {
login: 'large',
name: 'Large Sponsor',
avatarUrl: 'https://avatars.githubusercontent.com/u/2?v=4',
url: 'https://github.com/large',
},
tier: { monthlyPriceInDollars: 100 },
};
const pastSponsor = {
sponsorEntity: {
login: 'one-time',
name: 'One-time Sponsor',
avatarUrl: 'https://avatars.githubusercontent.com/u/3?v=4',
url: 'https://github.com/one-time',
},
tier: null,
};
const fetchImplementation: typeof fetch = async (_input, init) => {
const body = JSON.parse(String(init?.body)) as {
variables: { activeOnly: boolean; cursor: string | null };
};
const { activeOnly, cursor } = body.variables;
requests.push({ activeOnly, cursor });
const nodes = activeOnly
? cursor
? [largeSponsor]
: [
smallSponsor,
{
sponsorEntity: {
login: 'unsafe',
name: 'Unsafe image',
avatarUrl: 'https://example.com/avatar.png',
url: 'https://github.com/unsafe',
},
tier: { monthlyPriceInDollars: 10_000 },
},
]
: [smallSponsor, largeSponsor, largeSponsor, pastSponsor];
return githubResponse({
data: {
user: {
sponsorshipsAsMaintainer: {
nodes,
pageInfo: {
hasNextPage: activeOnly && cursor === null,
endCursor: activeOnly && cursor === null ? 'next' : null,
},
},
},
},
});
};

const sponsors = await fetchPublicSponsors('rdlabo', 'test-token', fetchImplementation);

assert.deepEqual(requests, [
{ activeOnly: true, cursor: null },
{ activeOnly: false, cursor: null },
{ activeOnly: true, cursor: 'next' },
]);
assert.deepEqual(sponsors, {
current: [
{
login: 'large',
name: 'Large Sponsor',
avatarUrl: 'https://avatars.githubusercontent.com/u/2?v=4',
profileUrl: 'https://github.com/large',
},
{
login: 'small',
name: 'small',
avatarUrl: 'https://avatars.githubusercontent.com/u/1?v=4',
profileUrl: 'https://github.com/small',
},
],
past: [
{
login: 'one-time',
name: 'One-time Sponsor',
avatarUrl: 'https://avatars.githubusercontent.com/u/3?v=4',
profileUrl: 'https://github.com/one-time',
},
],
});
assert.doesNotMatch(sponsorsModule(sponsors), /monthlyPriceInDollars/);
});

test('fails closed when GitHub rejects the request', async () => {
const fetchImplementation: typeof fetch = async () => new Response('', { status: 401 });
await assert.rejects(
() => fetchPublicSponsors('rdlabo', 'bad-token', fetchImplementation),
/HTTP 401/,
);
});
Loading