Composable monorepo CI fixes: - Add empty go.sum.tmpl files for pkg, service, worker, and cli components - Fix Dockerfile.tmpl glob patterns (COPY go.work.sum* is invalid in Kaniko) - Add deps step to CI that runs go work sync and go mod tidy before builds - Fix scalar-go dependency version (v0.1.2 doesn't exist, use v0.13.0) Health endpoint improvements: - Add registry health check (zot OCI /v2/ endpoint) - Add health metrics for CI, registry, and Git - Add /health/ci endpoint for Woodpecker health Visual verification scaffolding: - Add Playwright pod and scripts ConfigMap - Add vision.md and implementation breakdown plan Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
109 lines
3.4 KiB
YAML
109 lines
3.4 KiB
YAML
apiVersion: v1
|
|
kind: ConfigMap
|
|
metadata:
|
|
name: playwright-scripts
|
|
namespace: rdev
|
|
labels:
|
|
app.kubernetes.io/name: playwright
|
|
app.kubernetes.io/part-of: rdev
|
|
data:
|
|
capture.js: |
|
|
#!/usr/bin/env node
|
|
// capture.js - Playwright screenshot/video capture script
|
|
// Input: --url, --viewports (comma-separated), --output (dir),
|
|
// --wait-for (selector), --full-page, --video
|
|
// Output: JSON manifest to stdout
|
|
|
|
const { chromium } = require('playwright');
|
|
const path = require('path');
|
|
const fs = require('fs');
|
|
|
|
async function main() {
|
|
const args = parseArgs(process.argv.slice(2));
|
|
|
|
if (!args.url) {
|
|
console.error('Error: --url is required');
|
|
process.exit(1);
|
|
}
|
|
|
|
const outputDir = args.output || '/captures/default';
|
|
const viewports = args.viewports ? args.viewports.split(',') : ['1920x1080', '768x1024', '375x667'];
|
|
const waitFor = args['wait-for'] || 'body';
|
|
const fullPage = args['full-page'] === 'true';
|
|
const recordVideo = args.video === 'true';
|
|
|
|
// Ensure output directory exists
|
|
fs.mkdirSync(outputDir, { recursive: true });
|
|
|
|
const browser = await chromium.launch({ headless: true });
|
|
const result = { screenshots: {} };
|
|
|
|
try {
|
|
for (const viewport of viewports) {
|
|
const [width, height] = viewport.split('x').map(Number);
|
|
const viewportName = `${width}x${height}`;
|
|
|
|
const contextOptions = {
|
|
viewport: { width, height },
|
|
};
|
|
|
|
if (recordVideo && viewport === viewports[0]) {
|
|
contextOptions.recordVideo = {
|
|
dir: outputDir,
|
|
size: { width, height }
|
|
};
|
|
}
|
|
|
|
const context = await browser.newContext(contextOptions);
|
|
const page = await context.newPage();
|
|
|
|
await page.goto(args.url, { waitUntil: 'networkidle', timeout: 30000 });
|
|
await page.waitForSelector(waitFor, { timeout: 10000 }).catch(() => {});
|
|
|
|
const screenshotPath = path.join(outputDir, `${viewportName.replace('x', '_')}.png`);
|
|
await page.screenshot({ path: screenshotPath, fullPage });
|
|
result.screenshots[viewportName] = screenshotPath;
|
|
|
|
if (recordVideo && viewport === viewports[0]) {
|
|
await page.close();
|
|
const video = page.video();
|
|
if (video) {
|
|
const videoPath = await video.path();
|
|
const finalVideoPath = path.join(outputDir, 'recording.webm');
|
|
fs.renameSync(videoPath, finalVideoPath);
|
|
result.video = finalVideoPath;
|
|
}
|
|
}
|
|
|
|
await context.close();
|
|
}
|
|
} finally {
|
|
await browser.close();
|
|
}
|
|
|
|
console.log(JSON.stringify(result));
|
|
}
|
|
|
|
function parseArgs(argv) {
|
|
const args = {};
|
|
for (let i = 0; i < argv.length; i++) {
|
|
if (argv[i].startsWith('--')) {
|
|
const key = argv[i].slice(2);
|
|
const eqIdx = key.indexOf('=');
|
|
if (eqIdx !== -1) {
|
|
args[key.slice(0, eqIdx)] = key.slice(eqIdx + 1);
|
|
} else if (argv[i + 1] && !argv[i + 1].startsWith('--')) {
|
|
args[key] = argv[++i];
|
|
} else {
|
|
args[key] = 'true';
|
|
}
|
|
}
|
|
}
|
|
return args;
|
|
}
|
|
|
|
main().catch(err => {
|
|
console.error('Error:', err.message);
|
|
process.exit(1);
|
|
});
|