Skip to content

Commit 85d3ad8

Browse files
authored
Add CLI documentation auto-generator (#67)
1 parent 63a05e7 commit 85d3ad8

File tree

16 files changed

+2388
-454
lines changed

16 files changed

+2388
-454
lines changed

β€Ž.github/workflows/ci.ymlβ€Ž

Lines changed: 361 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,10 @@ on:
77
paths-ignore:
88
- '*.md'
99

10+
permissions:
11+
contents: write
12+
pull-requests: write
13+
1014
jobs:
1115
build:
1216
name: Build
@@ -16,6 +20,9 @@ jobs:
1620
cancel-in-progress: true
1721
steps:
1822
- uses: actions/checkout@v4
23+
with:
24+
ref: ${{ github.head_ref }}
25+
token: ${{ secrets.GITHUB_TOKEN }}
1926

2027
- uses: pnpm/action-setup@v4
2128
with:
@@ -32,13 +39,362 @@ jobs:
3239
- name: Lint
3340
run: pnpm lint
3441

42+
- name: Install Sprites CLI
43+
run: |
44+
curl -fsSL https://sprites.dev/install.sh | bash
45+
echo "$HOME/.local/bin" >> $GITHUB_PATH
46+
47+
- name: Setup Sprites auth
48+
run: sprite auth setup --token "$SPRITES_TEST_TOKEN"
49+
env:
50+
SPRITES_TEST_TOKEN: ${{ secrets.SPRITES_TEST_TOKEN }}
51+
52+
- name: Generate CLI Docs
53+
run: pnpm generate:cli-docs
54+
55+
- name: Commit generated CLI docs
56+
run: |
57+
git config user.name "github-actions[bot]"
58+
git config user.email "github-actions[bot]@users.noreply.github.com"
59+
git add src/content/docs/cli/commands.mdx
60+
if git diff --cached --quiet; then
61+
echo "No changes to CLI docs"
62+
else
63+
git commit -m "Update auto-generated CLI documentation"
64+
git push
65+
fi
66+
3567
- name: Build
3668
run: pnpm build
3769

38-
- name: Setup flyctl
39-
uses: superfly/flyctl-actions/setup-flyctl@master
70+
- name: Comment CLI Test Results on PR
71+
if: always() && github.event_name == 'pull_request'
72+
uses: actions/github-script@v7
73+
with:
74+
script: |
75+
const fs = require('fs');
76+
const path = './cli-test-report.json';
77+
78+
let body = '### CLI Documentation Generator\n\n';
79+
80+
if (!fs.existsSync(path)) {
81+
body += '⚠️ No test report found (tests may have been skipped)\n';
82+
} else {
83+
const report = JSON.parse(fs.readFileSync(path, 'utf8'));
4084
41-
- name: Build Fly app
42-
run: flyctl deploy --build-only
85+
const allPassed = report.testsFailed === 0;
86+
const emoji = allPassed ? 'βœ…' : '❌';
87+
88+
body += `| Metric | Value |\n`;
89+
body += `|--------|-------|\n`;
90+
body += `| CLI Version | \`${report.cliVersion}\` |\n`;
91+
body += `| Commands Generated | ${report.commandsGenerated.length} |\n`;
92+
body += `| Tests | ${emoji} ${report.testsPassed}/${report.testsRun} passed |\n`;
93+
94+
if (report.errors.length > 0) {
95+
body += `\n<details><summary>❌ ${report.errors.length} Error(s)</summary>\n\n`;
96+
for (const error of report.errors) {
97+
body += `- **${error.command}** (${error.phase}): ${error.message}\n`;
98+
}
99+
body += `\n</details>\n`;
100+
}
101+
102+
if (report.commandsGenerated.length > 0) {
103+
body += `\n<details><summary>πŸ“š Commands documented</summary>\n\n`;
104+
body += report.commandsGenerated.map(c => `- \`${c}\``).join('\n');
105+
body += `\n</details>\n`;
106+
}
107+
}
108+
109+
const { data: comments } = await github.rest.issues.listComments({
110+
owner: context.repo.owner,
111+
repo: context.repo.repo,
112+
issue_number: context.issue.number,
113+
});
114+
const existing = comments.find(c => c.body.includes('### CLI Documentation Generator'));
115+
116+
if (existing) {
117+
await github.rest.issues.updateComment({
118+
owner: context.repo.owner,
119+
repo: context.repo.repo,
120+
comment_id: existing.id,
121+
body,
122+
});
123+
} else {
124+
await github.rest.issues.createComment({
125+
owner: context.repo.owner,
126+
repo: context.repo.repo,
127+
issue_number: context.issue.number,
128+
body,
129+
});
130+
}
131+
132+
preview:
133+
name: Preview Deployment
134+
runs-on: ubuntu-latest
135+
needs: build
136+
if: github.event_name == 'pull_request'
137+
outputs:
138+
url: ${{ steps.deploy.outputs.url }}
139+
environment:
140+
name: preview
141+
url: ${{ steps.deploy.outputs.url }}
142+
env:
143+
FLY_API_TOKEN: ${{ secrets.FLY_PREVIEW_API_TOKEN }}
144+
FLY_REGION: iad
145+
FLY_ORG: ${{ vars.FLY_PREVIEW_ORG }}
146+
147+
steps:
148+
- uses: actions/checkout@v4
149+
with:
150+
ref: ${{ github.head_ref }}
151+
fetch-depth: 0
152+
153+
- name: Deploy preview app
154+
id: deploy
155+
uses: superfly/[email protected]
156+
with:
157+
config: fly.preview.toml
158+
vmsize: shared-cpu-1x
159+
memory: 256
160+
161+
- name: Comment on PR
162+
uses: actions/github-script@v7
163+
with:
164+
script: |
165+
const url = '${{ steps.deploy.outputs.url }}';
166+
const sha = context.sha.substring(0, 7);
167+
const body = `### Preview Deployment\n\n| Name | URL |\n|------|-----|\n| Preview | ${url} |\n\nCommit: \`${sha}\``;
168+
169+
const { data: comments } = await github.rest.issues.listComments({
170+
owner: context.repo.owner,
171+
repo: context.repo.repo,
172+
issue_number: context.issue.number,
173+
});
174+
const existing = comments.find(c => c.body.includes('### Preview Deployment'));
175+
176+
if (existing) {
177+
await github.rest.issues.updateComment({
178+
owner: context.repo.owner,
179+
repo: context.repo.repo,
180+
comment_id: existing.id,
181+
body,
182+
});
183+
} else {
184+
await github.rest.issues.createComment({
185+
owner: context.repo.owner,
186+
repo: context.repo.repo,
187+
issue_number: context.issue.number,
188+
body,
189+
});
190+
}
191+
192+
e2e-test:
193+
name: E2E Tests
194+
runs-on: ubuntu-latest
195+
needs: preview
196+
continue-on-error: true
197+
198+
steps:
199+
- uses: actions/checkout@v4
200+
201+
- uses: pnpm/action-setup@v4
202+
with:
203+
version: latest
204+
205+
- uses: actions/setup-node@v4
206+
with:
207+
node-version: 22
208+
cache: pnpm
209+
210+
- name: Install dependencies
211+
run: pnpm install --frozen-lockfile
212+
213+
- name: Install Cypress binary
214+
run: pnpm cypress install
215+
216+
- name: Wait for preview to be ready
217+
run: |
218+
echo "Waiting for preview deployment to be ready..."
219+
for i in {1..30}; do
220+
if curl -s -o /dev/null -w "%{http_code}" "${{ needs.preview.outputs.url }}" | grep -q "200"; then
221+
echo "Preview is ready!"
222+
exit 0
223+
fi
224+
echo "Attempt $i: Preview not ready yet, waiting 10s..."
225+
sleep 10
226+
done
227+
echo "Preview did not become ready in time"
228+
exit 1
229+
230+
- name: Run Cypress tests
231+
uses: cypress-io/github-action@v6
232+
with:
233+
browser: chrome
234+
install: false
43235
env:
44-
FLY_API_TOKEN: ${{ secrets.FLY_API_TOKEN }}
236+
CYPRESS_BASE_URL: ${{ needs.preview.outputs.url }}
237+
238+
- name: Upload screenshots on failure
239+
uses: actions/upload-artifact@v4
240+
if: failure()
241+
with:
242+
name: cypress-screenshots
243+
path: cypress/screenshots
244+
retention-days: 7
245+
246+
- name: Upload videos on failure
247+
uses: actions/upload-artifact@v4
248+
if: failure()
249+
with:
250+
name: cypress-videos
251+
path: cypress/videos
252+
retention-days: 7
253+
254+
- name: Comment test results on PR
255+
if: always()
256+
uses: actions/github-script@v7
257+
with:
258+
script: |
259+
const status = '${{ job.status }}';
260+
const emoji = status === 'success' ? 'βœ…' : '❌';
261+
const body = `### E2E Test Results\n\n${emoji} Tests ${status}\n\nRan against: ${{ needs.preview.outputs.url }}`;
262+
263+
const { data: comments } = await github.rest.issues.listComments({
264+
owner: context.repo.owner,
265+
repo: context.repo.repo,
266+
issue_number: context.issue.number,
267+
});
268+
const existing = comments.find(c => c.body.includes('### E2E Test Results'));
269+
270+
if (existing) {
271+
await github.rest.issues.updateComment({
272+
owner: context.repo.owner,
273+
repo: context.repo.repo,
274+
comment_id: existing.id,
275+
body,
276+
});
277+
} else {
278+
await github.rest.issues.createComment({
279+
owner: context.repo.owner,
280+
repo: context.repo.repo,
281+
issue_number: context.issue.number,
282+
body,
283+
});
284+
}
285+
286+
lighthouse:
287+
name: Lighthouse CI
288+
runs-on: ubuntu-latest
289+
needs: preview
290+
continue-on-error: true
291+
292+
steps:
293+
- uses: actions/checkout@v4
294+
295+
- name: Wait for preview to be ready
296+
run: |
297+
echo "Waiting for preview deployment to be ready..."
298+
for i in {1..30}; do
299+
if curl -s -o /dev/null -w "%{http_code}" "${{ needs.preview.outputs.url }}" | grep -q "200"; then
300+
echo "Preview is ready!"
301+
exit 0
302+
fi
303+
echo "Attempt $i: Preview not ready yet, waiting 10s..."
304+
sleep 10
305+
done
306+
echo "Preview did not become ready in time"
307+
exit 1
308+
309+
- name: Get changed pages
310+
id: changed-pages
311+
uses: actions/github-script@v7
312+
with:
313+
script: |
314+
const { data: files } = await github.rest.pulls.listFiles({
315+
owner: context.repo.owner,
316+
repo: context.repo.repo,
317+
pull_number: context.issue.number,
318+
});
319+
320+
const baseUrl = '${{ needs.preview.outputs.url }}';
321+
const urls = new Set([baseUrl]); // Always test homepage
322+
323+
for (const file of files) {
324+
// Match .mdx files in src/content/docs/
325+
const match = file.filename.match(/^src\/content\/docs\/(.+)\.mdx$/);
326+
if (match && file.status !== 'removed') {
327+
let path = match[1];
328+
// index.mdx maps to root, others map to their path
329+
if (path === 'index') {
330+
urls.add(baseUrl);
331+
} else {
332+
urls.add(`${baseUrl}/${path}/`);
333+
}
334+
}
335+
}
336+
337+
const urlList = Array.from(urls).join('\n');
338+
console.log('URLs to test:\n' + urlList);
339+
core.setOutput('urls', urlList);
340+
341+
- name: Run Lighthouse CI
342+
id: lighthouse
343+
uses: treosh/lighthouse-ci-action@v12
344+
with:
345+
urls: ${{ steps.changed-pages.outputs.urls }}
346+
configPath: ./lighthouserc.json
347+
budgetPath: ./budget.json
348+
uploadArtifacts: true
349+
temporaryPublicStorage: true
350+
351+
- name: Comment Lighthouse results on PR
352+
if: always()
353+
uses: actions/github-script@v7
354+
with:
355+
script: |
356+
const manifest = ${{ steps.lighthouse.outputs.manifest }};
357+
const links = ${{ steps.lighthouse.outputs.links }};
358+
359+
let body = '### Lighthouse Results\n\n';
360+
body += '| URL | Performance | Accessibility | Best Practices | SEO |\n';
361+
body += '|-----|-------------|---------------|----------------|-----|\n';
362+
363+
for (const result of manifest) {
364+
const url = new URL(result.url).pathname || '/';
365+
const perf = Math.round(result.summary.performance * 100);
366+
const a11y = Math.round(result.summary.accessibility * 100);
367+
const bp = Math.round(result.summary['best-practices'] * 100);
368+
const seo = Math.round(result.summary.seo * 100);
369+
370+
const perfEmoji = perf >= 90 ? '🟒' : perf >= 50 ? '🟠' : 'πŸ”΄';
371+
const a11yEmoji = a11y >= 90 ? '🟒' : a11y >= 50 ? '🟠' : 'πŸ”΄';
372+
const bpEmoji = bp >= 90 ? '🟒' : bp >= 50 ? '🟠' : 'πŸ”΄';
373+
const seoEmoji = seo >= 90 ? '🟒' : seo >= 50 ? '🟠' : 'πŸ”΄';
374+
375+
const reportLink = links[result.url] ? `[${url}](${links[result.url]})` : url;
376+
body += `| ${reportLink} | ${perfEmoji} ${perf} | ${a11yEmoji} ${a11y} | ${bpEmoji} ${bp} | ${seoEmoji} ${seo} |\n`;
377+
}
378+
379+
const { data: comments } = await github.rest.issues.listComments({
380+
owner: context.repo.owner,
381+
repo: context.repo.repo,
382+
issue_number: context.issue.number,
383+
});
384+
const existing = comments.find(c => c.body.includes('### Lighthouse Results'));
385+
386+
if (existing) {
387+
await github.rest.issues.updateComment({
388+
owner: context.repo.owner,
389+
repo: context.repo.repo,
390+
comment_id: existing.id,
391+
body,
392+
});
393+
} else {
394+
await github.rest.issues.createComment({
395+
owner: context.repo.owner,
396+
repo: context.repo.repo,
397+
issue_number: context.issue.number,
398+
body,
399+
});
400+
}

0 commit comments

Comments
Β (0)