77 paths-ignore :
88 - ' *.md'
99
10+ permissions :
11+ contents : write
12+ pull-requests : write
13+
1014jobs :
1115 build :
1216 name : Build
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+ 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