|
10 | 10 | type: choice |
11 | 11 | options: |
12 | 12 | - '0.5' |
| 13 | + - '0.75' |
13 | 14 | - '1' |
| 15 | + - '1.25' |
14 | 16 | - '1.5' |
15 | | - - '3' |
| 17 | + - '2' |
| 18 | + num_workers: |
| 19 | + description: Number of Chromium workers to run in parallel |
| 20 | + required: false |
| 21 | + default: '8' |
| 22 | + type: choice |
| 23 | + options: |
| 24 | + - '1' |
| 25 | + - '2' |
| 26 | + - '4' |
| 27 | + - '6' |
| 28 | + - '8' |
16 | 29 |
|
17 | 30 | permissions: |
18 | 31 | contents: write |
19 | 32 |
|
20 | 33 | jobs: |
21 | 34 | screenshot: |
22 | | - runs-on: ubuntu-latest |
| 35 | + runs-on: blacksmith-8vcpu-ubuntu-2404 |
23 | 36 | steps: |
24 | 37 | - name: Checkout |
25 | 38 | uses: actions/checkout@v6 |
@@ -47,201 +60,19 @@ jobs: |
47 | 60 | - name: Start server and take screenshots |
48 | 61 | env: |
49 | 62 | SCREENSHOT_ZOOM_LEVEL: ${{ inputs.zoom_level || '1' }} |
| 63 | + SCREENSHOT_NUM_WORKERS: ${{ inputs.num_workers || '4' }} |
| 64 | + SCREENSHOT_OUT_DIR: ${{ github.workspace }}/.screenshots |
50 | 65 | run: | |
51 | | - # Start static server in background |
52 | | - bunx --bun serve . -p 1313 > /dev/null 2>&1 & |
53 | | - SERVER_PID=$! |
54 | | -
|
55 | | - # Wait for server to be ready |
56 | | - echo "Waiting for server to start..." |
57 | | - for i in {1..30}; do |
58 | | - if curl -s http://localhost:1313/ > /dev/null 2>&1; then |
59 | | - echo "Server is ready!" |
60 | | - break |
61 | | - fi |
62 | | - sleep 1 |
63 | | - done |
64 | | -
|
65 | | - # Create screenshot directories and capture slides |
66 | | - node << 'EOF' |
67 | | - const { chromium } = require('playwright'); |
68 | | - const fs = require('fs'); |
69 | | - const path = require('path'); |
70 | | - const zoomLevel = Number(process.env.SCREENSHOT_ZOOM_LEVEL || '1'); |
71 | | -
|
72 | | - if (!Number.isFinite(zoomLevel) || zoomLevel <= 0) { |
73 | | - throw new Error(`Invalid SCREENSHOT_ZOOM_LEVEL: ${process.env.SCREENSHOT_ZOOM_LEVEL}`); |
74 | | - } |
75 | | -
|
76 | | - // URLs to capture: [url, folderName] or [url, folderName, waitTimeMs] |
77 | | - // Optional waitTimeMs: wait after load before screenshot (for animated slides) |
78 | | - // Main slides 1-17, plus basement slides at 8 and 16 |
79 | | - const urls = [ |
80 | | - ['/', 'slide-01'], |
81 | | - ['/#/2', 'slide-02'], |
82 | | - ['/#/3', 'slide-03'], |
83 | | - ['/#/4', 'slide-04'], |
84 | | - // ['/#/5', 'slide-05'], |
85 | | - // ['/#/6', 'slide-06'], |
86 | | - // ['/#/7', 'slide-07'], |
87 | | - ['/#/8', 'slide-08-01'], |
88 | | - ['/#/8/2', 'slide-08-02'], |
89 | | - // ['/#/9', 'slide-09'], |
90 | | - ['/#/10', 'slide-10'], |
91 | | - ['/#/11', 'slide-11'], |
92 | | - ['/#/12', 'slide-12'], |
93 | | - // ['/#/13', 'slide-13'], |
94 | | - // ['/#/14', 'slide-14'], |
95 | | - // ['/#/15', 'slide-15'], |
96 | | - ['/#/16', 'slide-16-01'], |
97 | | - ['/#/16/2', 'slide-16-02'], |
98 | | - ['/#/16/3', 'slide-16-03'], |
99 | | - // ['/#/17', 'slide-17'], |
100 | | - ]; |
101 | | -
|
102 | | - // Define resolutions: [width, height, label] |
103 | | - const resolutions = [ |
104 | | - // Mobile |
105 | | - [375, 667, 'iphone-se'], |
106 | | - [390, 844, 'iphone-12-13'], |
107 | | - [430, 932, 'iphone-14-15-16-17-pro-max'], |
108 | | - // Mobile Landscape |
109 | | - [667, 375, 'iphone-se-landscape'], |
110 | | - [844, 390, 'iphone-12-13-landscape'], |
111 | | - [932, 430, 'iphone-14-15-16-17-pro-max-landscape'], |
112 | | - // Tablet |
113 | | - [768, 1024, 'ipad-portrait'], |
114 | | - [1024, 768, 'ipad-landscape'], |
115 | | - // Desktop |
116 | | - [1280, 720, 'desktop-1280x720'], |
117 | | - [1366, 768, 'desktop-1366x768'], |
118 | | - [1440, 900, 'desktop-1440x900'], |
119 | | - [1920, 1080, 'desktop-1920x1080'], |
120 | | - [2560, 1440, 'desktop-2560x1440'], |
121 | | - ]; |
122 | | -
|
123 | | - async function takeScreenshots(url, folder, waitTime = 0) { |
124 | | - const browser = await chromium.launch(); |
125 | | - const screenshots = []; |
126 | | - const fullUrl = `http://localhost:1313${url}`; |
127 | | -
|
128 | | - if (waitTime > 0) { |
129 | | - // Load once, wait for animation, then resize for each resolution |
130 | | - console.log(`Loading ${url} at ${zoomLevel}x zoom and waiting ${waitTime}ms for animation...`); |
131 | | - const page = await browser.newPage(); |
132 | | - const [firstWidth, firstHeight, firstLabel] = resolutions[0]; |
133 | | - await page.setViewportSize({ width: firstWidth, height: firstHeight }); |
134 | | - await page.goto(fullUrl, { waitUntil: 'networkidle' }); |
135 | | - await page.waitForTimeout(500); |
136 | | - await page.evaluate((zoom) => { |
137 | | - document.documentElement.style.zoom = String(zoom); |
138 | | - }, zoomLevel); |
139 | | - await page.waitForTimeout(100); |
140 | | - await page.waitForTimeout(waitTime); |
141 | | - |
142 | | - // Now take screenshots at all resolutions by resizing |
143 | | - for (const [width, height, label] of resolutions) { |
144 | | - console.log(`Taking ${width}x${height} (${label}) for ${url} at ${zoomLevel}x zoom`); |
145 | | - await page.setViewportSize({ width, height }); |
146 | | - await page.evaluate((zoom) => { |
147 | | - document.documentElement.style.zoom = String(zoom); |
148 | | - }, zoomLevel); |
149 | | - // Small delay to let layout adjust after resize |
150 | | - await page.waitForTimeout(100); |
151 | | -
|
152 | | - const filename = `${width}x${height}-${label}.png`; |
153 | | - const filepath = path.join(folder, filename); |
154 | | - await page.screenshot({ path: filepath, fullPage: false }); |
155 | | - screenshots.push({ filename, width, height, label }); |
156 | | - } |
157 | | - |
158 | | - await page.close(); |
159 | | - } else { |
160 | | - // Reload for each resolution |
161 | | - for (const [width, height, label] of resolutions) { |
162 | | - console.log(`Taking ${width}x${height} (${label}) for ${url} at ${zoomLevel}x zoom`); |
163 | | - const page = await browser.newPage(); |
164 | | - await page.setViewportSize({ width, height }); |
165 | | - await page.goto(fullUrl, { waitUntil: 'networkidle' }); |
166 | | - await page.waitForTimeout(500); |
167 | | - await page.evaluate((zoom) => { |
168 | | - document.documentElement.style.zoom = String(zoom); |
169 | | - }, zoomLevel); |
170 | | - await page.waitForTimeout(100); |
171 | | -
|
172 | | - const filename = `${width}x${height}-${label}.png`; |
173 | | - const filepath = path.join(folder, filename); |
174 | | - await page.screenshot({ path: filepath, fullPage: false }); |
175 | | - await page.close(); |
176 | | -
|
177 | | - screenshots.push({ filename, width, height, label }); |
178 | | - } |
179 | | - } |
180 | | -
|
181 | | - await browser.close(); |
182 | | - return screenshots; |
183 | | - } |
184 | | -
|
185 | | - function getFolderTitle(folderName) { |
186 | | - return folderName |
187 | | - .split('-') |
188 | | - .map(word => word.charAt(0).toUpperCase() + word.slice(1)) |
189 | | - .join(' '); |
190 | | - } |
191 | | -
|
192 | | - async function generateReadme(folder, screenshots) { |
193 | | - const timestamp = new Date().toISOString().replace(/:/g, '-').split('.')[0] + 'Z'; |
194 | | - const readmePath = path.join(folder, 'README.md'); |
195 | | -
|
196 | | - let content = `# ${getFolderTitle(folder)}\n\n`; |
197 | | - content += `Generated: ${new Date().toISOString()}\n\n`; |
198 | | - content += `| Resolution | Device | Screenshot |\n`; |
199 | | - content += `|------------|--------|------------|\n`; |
200 | | -
|
201 | | - for (const { filename, width, height, label } of screenshots) { |
202 | | - content += `| ${width} × ${height} | ${label} |  |\n`; |
203 | | - } |
204 | | -
|
205 | | - fs.writeFileSync(readmePath, content); |
206 | | - } |
207 | | -
|
208 | | - (async () => { |
209 | | - try { |
210 | | - const allFolders = []; |
211 | | - console.log(`Using screenshot zoom level: ${zoomLevel}x`); |
212 | | -
|
213 | | - for (const entry of urls) { |
214 | | - const [url, folderName, waitTime = 0] = entry; |
215 | | - fs.mkdirSync(folderName, { recursive: true }); |
216 | | - allFolders.push(folderName); |
217 | | -
|
218 | | - const waitMsg = waitTime > 0 ? ` (wait ${waitTime}ms)` : ''; |
219 | | - console.log(`=== Capturing ${folderName} for ${url}${waitMsg} ===`); |
220 | | - const screenshots = await takeScreenshots(url, folderName, waitTime); |
221 | | - await generateReadme(folderName, screenshots); |
222 | | - console.log(`Captured ${screenshots.length} screenshots for ${folderName}`); |
223 | | - } |
224 | | -
|
225 | | - console.log('All screenshots saved successfully'); |
226 | | - fs.writeFileSync('_screenshot_folders.json', JSON.stringify(allFolders)); |
227 | | - } catch (error) { |
228 | | - console.error('Error taking screenshots:', error); |
229 | | - process.exit(1); |
230 | | - } |
231 | | - })(); |
232 | | - EOF |
233 | | -
|
234 | | - # Stop server |
235 | | - kill $SERVER_PID 2>/dev/null || true |
| 66 | + ./scripts/capture-screenshots.sh |
236 | 67 |
|
237 | 68 | - name: Create orphan branch and commit screenshots |
238 | 69 | run: | |
239 | 70 | git config --global user.name "github-actions[bot]" |
240 | 71 | git config --global user.email "github-actions[bot]@users.noreply.github.com" |
241 | 72 |
|
242 | | - FOLDERS=$(node -e "console.log(require('./_screenshot_folders.json').join(' '))") |
243 | | - rm _screenshot_folders.json |
244 | | - mv $FOLDERS /tmp/ |
| 73 | + FOLDERS=$(bun -e "console.log(JSON.parse(require('fs').readFileSync('.screenshots/_screenshot_folders.json', 'utf8')).join(' '))") |
| 74 | + rm .screenshots/_screenshot_folders.json |
| 75 | + mv .screenshots/slide-* /tmp/ |
245 | 76 |
|
246 | 77 | git checkout main || git checkout master || true |
247 | 78 | git branch -D screenshots 2>/dev/null || true |
|
0 commit comments