Skip to content

Commit 529bbdf

Browse files
Add challenge.html preview rendering to challenge creation workflow (#229)
* Add challenge.html preview rendering to challenge creation workflow Renders the new challenge.html with Playwright + MathJax after Claude creates a challenge, commits a preview.png to the PR branch, and posts it as a PR comment so reviewers can see the rendered output. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Add test workflow for challenge preview rendering Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 16d6c50 commit 529bbdf

3 files changed

Lines changed: 186 additions & 0 deletions

File tree

Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
const { chromium } = require("playwright");
2+
const fs = require("fs");
3+
const http = require("http");
4+
5+
const challengeHtmlPath = process.argv[2];
6+
const outputPath = process.argv[3] || "challenge-preview.png";
7+
8+
if (!challengeHtmlPath) {
9+
console.error("Usage: node render-challenge.js <challenge.html> [output.png]");
10+
process.exit(1);
11+
}
12+
13+
const fragment = fs.readFileSync(challengeHtmlPath, "utf-8");
14+
15+
const fullHtml = `<!DOCTYPE html>
16+
<html>
17+
<head>
18+
<meta charset="utf-8">
19+
<script>
20+
window.MathJax = {
21+
tex: { inlineMath: [['\\\\(','\\\\)']], displayMath: [['\\\\[','\\\\]']] },
22+
startup: {
23+
pageReady: () => MathJax.startup.defaultPageReady().then(() => {
24+
document.body.dataset.mathReady = 'true';
25+
})
26+
}
27+
};
28+
</script>
29+
<script src="https://cdn.jsdelivr.net/npm/mathjax@3/es5/tex-svg.js"></script>
30+
<style>
31+
body {
32+
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
33+
max-width: 800px;
34+
margin: 0 auto;
35+
padding: 32px;
36+
background: #fff;
37+
color: #1a1a1a;
38+
font-size: 15px;
39+
line-height: 1.6;
40+
}
41+
h2 { border-bottom: 1px solid #e0e0e0; padding-bottom: 6px; margin-top: 28px; }
42+
code { background: #f4f4f4; padding: 2px 5px; border-radius: 3px; font-size: 0.9em; }
43+
pre { background: #f4f4f4; padding: 12px; border-radius: 6px; overflow-x: auto; }
44+
ul { padding-left: 24px; }
45+
li { margin-bottom: 4px; }
46+
svg { max-width: 100%; height: auto; }
47+
</style>
48+
</head>
49+
<body>
50+
${fragment}
51+
</body>
52+
</html>`;
53+
54+
(async () => {
55+
const server = http.createServer((req, res) => {
56+
res.writeHead(200, { "Content-Type": "text/html; charset=utf-8" });
57+
res.end(fullHtml);
58+
});
59+
60+
await new Promise((resolve) => server.listen(0, "127.0.0.1", resolve));
61+
const port = server.address().port;
62+
63+
const browser = await chromium.launch();
64+
const page = await browser.newPage({ viewport: { width: 900, height: 600 } });
65+
66+
await page.goto(`http://127.0.0.1:${port}`, { waitUntil: "networkidle" });
67+
68+
try {
69+
await page.waitForFunction(
70+
() => document.body.dataset.mathReady === "true",
71+
{ timeout: 15000 }
72+
);
73+
} catch {
74+
console.warn("MathJax did not signal ready within 15s, continuing anyway");
75+
}
76+
77+
await page.waitForTimeout(500);
78+
79+
const body = page.locator("body");
80+
await body.screenshot({ path: outputPath, type: "png" });
81+
console.log("Screenshot saved to " + outputPath);
82+
83+
await browser.close();
84+
server.close();
85+
})();

.github/workflows/create-challenge.yml

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -90,3 +90,50 @@ jobs:
9090
- The challenge should require the solver to think about memory access patterns, synchronization, work distribution, or algorithm design — not just write a one-line kernel.
9191
9292
Before opening the PR, verify every item in the "Checklist" section of CLAUDE.md. Run `pre-commit run --all-files` to lint.
93+
94+
- name: Render challenge.html preview
95+
if: steps.claude.outputs.branch_name != ''
96+
run: |
97+
git checkout "${{ steps.claude.outputs.branch_name }}"
98+
git pull origin "${{ steps.claude.outputs.branch_name }}"
99+
CHALLENGE_HTML=$(git diff --name-only origin/main..HEAD -- 'challenges/**/challenge.html' | head -1)
100+
if [ -z "$CHALLENGE_HTML" ]; then
101+
echo "No challenge.html found in diff, skipping render"
102+
exit 0
103+
fi
104+
echo "Rendering $CHALLENGE_HTML"
105+
npx --yes playwright install --with-deps chromium
106+
node .github/scripts/render-challenge.js "$CHALLENGE_HTML" /tmp/challenge-preview.png
107+
echo "challenge_html=$CHALLENGE_HTML" >> "$GITHUB_ENV"
108+
109+
- name: Commit preview and comment on PR
110+
if: env.challenge_html != ''
111+
env:
112+
GH_TOKEN: ${{ github.token }}
113+
run: |
114+
BRANCH="${{ steps.claude.outputs.branch_name }}"
115+
PR_NUMBER=$(gh pr list --head "$BRANCH" --json number --jq '.[0].number')
116+
if [ -z "$PR_NUMBER" ]; then
117+
echo "No PR found for branch $BRANCH, skipping"
118+
exit 0
119+
fi
120+
121+
CHALLENGE_DIR=$(dirname "${{ env.challenge_html }}")
122+
PREVIEW_PATH="${CHALLENGE_DIR}/preview.png"
123+
cp /tmp/challenge-preview.png "$PREVIEW_PATH"
124+
125+
git config user.name "github-actions[bot]"
126+
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
127+
git add "$PREVIEW_PATH"
128+
git commit -m "Add challenge.html preview image"
129+
git push
130+
131+
REPO="${{ github.repository }}"
132+
SHA=$(git rev-parse HEAD)
133+
IMG_URL="https://raw.githubusercontent.com/${REPO}/${SHA}/${PREVIEW_PATH}"
134+
BODY=$(cat <<EOF
135+
## Challenge Preview
136+
![challenge-preview](${IMG_URL})
137+
EOF
138+
)
139+
gh pr comment "$PR_NUMBER" --body "$BODY"
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
name: Test Challenge Preview Render
2+
3+
on:
4+
workflow_dispatch:
5+
inputs:
6+
challenge_path:
7+
description: "Path to challenge.html (e.g. challenges/medium/81_int4_matmul/challenge.html)"
8+
required: true
9+
default: "challenges/medium/81_int4_matmul/challenge.html"
10+
pr_number:
11+
description: "PR number to comment on"
12+
required: true
13+
14+
jobs:
15+
render-preview:
16+
runs-on: ubuntu-latest
17+
timeout-minutes: 5
18+
permissions:
19+
contents: write
20+
pull-requests: write
21+
22+
steps:
23+
- name: Checkout repository
24+
uses: actions/checkout@v4
25+
26+
- name: Render challenge.html preview
27+
run: |
28+
npx --yes playwright install --with-deps chromium
29+
node .github/scripts/render-challenge.js "${{ inputs.challenge_path }}" /tmp/challenge-preview.png
30+
31+
- name: Comment on PR with preview
32+
env:
33+
GH_TOKEN: ${{ github.token }}
34+
run: |
35+
CHALLENGE_DIR=$(dirname "${{ inputs.challenge_path }}")
36+
PREVIEW_PATH="${CHALLENGE_DIR}/preview.png"
37+
cp /tmp/challenge-preview.png "$PREVIEW_PATH"
38+
39+
git config user.name "github-actions[bot]"
40+
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
41+
git checkout -B preview-test
42+
git add "$PREVIEW_PATH"
43+
git commit -m "Add challenge.html preview image (test)"
44+
git push -f origin preview-test
45+
46+
REPO="${{ github.repository }}"
47+
SHA=$(git rev-parse HEAD)
48+
IMG_URL="https://raw.githubusercontent.com/${REPO}/${SHA}/${PREVIEW_PATH}"
49+
BODY=$(cat <<EOF
50+
## Challenge Preview
51+
![challenge-preview](${IMG_URL})
52+
EOF
53+
)
54+
gh pr comment "${{ inputs.pr_number }}" --body "$BODY"

0 commit comments

Comments
 (0)