Repository navigation
ci: split into two jobs and embed the render preview - #16
Conversation
Two workflow files, two builds, two browsers, and two places to look when
something breaks. The preview was not decoration either — the tests prove the
geometry is computed right and cannot show whether the result looks like
anything a person would want to read. That needs an image, so it belongs in the
same pipeline as the assertions and fails the same way.
One file now, one job:
format → types → coverage → build → UI tests → render → post
Branches to master skip the last three: a push to master has no pull request to
comment into.
The job keeps a single checkout, install and build instead of two, and the
timeout goes from 10 to 15 minutes to leave room for the browser the preview now
needs after the UI suite has already started one.
Permissions are declared at job level, which means the earlier steps inherit
them. That is a real trade-off — narrowest scope would be a separate job — but
it costs a second checkout/install/build and this repository's steps are all its
own code.
render-preview.yml is deleted; its history stays queryable in the Actions tab.
The artifact link in the comment is the only way to look at the render when inline embedding is unavailable, and until now clicking it downloaded a zip containing one PNG. GitHub added unzipped artifacts in February: `actions/upload-artifact` v7 with `archive: false` stores the file as-is, so a browser that can display the type opens it directly. That turns the link from "download, unzip, look" into "look". scripts/probe-browser.ts is deleted. It existed to prove a browser was available, that text could be measured, and that screenshots were deterministic — all three are now exercised on every run by the UI suite and the preview script. What it left behind was a fourth copy of the Chromium launch flags, and a stale copy at that: the missing `--user-data-dir` was fixed in the preview script while this one sat unchanged, which is exactly the failure mode that copying causes.
`gh run download` fails on non-zipped artifacts with 'zip: not a valid zip file' — the CLI still assumes every artifact is an archive (cli/cli#13012). `gh api .../artifacts/<id>/zip` returns the raw body, which for an unzipped artifact is the PNG. Verified by downloading this run's artifact and checking the file type.
The comment has been linking to a file the reader had to fetch. Now it shows the picture. The URL had to be one GitHub serves as an image, and that ruled out everything tried so far: - artifacts need auth, and their URLs redirect to HTML - release assets come back as `application/octet-stream` with `Content-Disposition: attachment`, so they are treated as a download - `gh --attach` needs the web upload endpoint, which an API token cannot call A file on a branch is served by raw.githubusercontent.com as `image/png`. That is the only form that renders, and it is what reg-actions and awesome-copilot both document. The image is committed to a dedicated `render-preview` branch — force pushed, one file, no history — from a scratch directory so the checked-out tree is never touched. `?v=<short sha>` busts the CDN cache, otherwise a re-render would show the previous image for a few minutes. This also removes the attach-and-fall-back dance entirely: there is no longer any path where the comment posts without an image. The artifact upload stays, as a 30-day copy that does not depend on the branch.
One job was wrong. The browser-only work forced `contents: write` onto every
step, including the ones that only read code and run assertions.
Two jobs now, each asking for what it uses:
checks format -> types -> test:coverage -> build (contents: read)
ui build -> test:ui -> render -> publish -> comment (contents: write)
The second builds its own dist/ rather than uploading and downloading it between
jobs — a round trip through an artifact is slower than the three seconds the
build takes.
Publishing also skips itself when the render is byte-identical to what the branch
already holds. Rendering is deterministic, so documentation-only pushes now leave
no dangling blob behind.
The comment explained what the pipeline was rather than what to look at. It now says the image comes from the latest code on the branch, names the specific failures worth scanning for, and links to the run that produced it.
One shared `preview.png` meant every run overwrote the file that every earlier comment pointed at. Open an old pull request and its screenshot had silently become a picture of newer code. The filename carries the full sha now, so a comment and the commit it describes stay together. That also removes the `?v=` cache-buster — a new name is a new URL. The branch grows by one file per commit instead of staying at one, which is the cost of not rewriting history. Worth watching; if it becomes noise, the old approach of pruning to the last N is a small change.
Keeping every preview meant keeping a commit per push, and a preview is useful while the pull request is open and not much after. The old force-push behaviour is right; the per-commit filename is what was actually needed. The branch now holds one commit with one file, named after the commit it renders. An old pull request whose commit has been garbage-collected shows a broken image — clearly gone, rather than silently swapped for a newer picture, which is what a shared filename did. Also drops the clone-and-compare step: force-pushing a fresh repo is both simpler and correct, since nothing is being appended to.
The branch is force-pushed, so a preview survives until its commit is garbage-collected. Say so where the image is, rather than leaving somebody to discover it as a broken link months later.
hizzgdev
left a comment
There was a problem hiding this comment.
Review: ci: split into two jobs and embed the render preview
Overall a well-researched and thoughtful CI restructure. The permissions split alone is worth the change.
🟢 Strengths
- Least privilege:
checksnow hascontents: readonly. The old single-job model gave write access to steps that didn't need it (format check, type check, tests). - Image URL strategy: The trial-and-error behind choosing
raw.githubusercontent.comfrom a branch is well-documented — and the right answer. Artifacts need auth, releases get wrong Content-Disposition,gh pr comment --attachgoes through a different API path that the token can't use. - Commit-named files (
preview-<sha>.png): Prevents the subtle bug where a sharedpreview.pngquietly rewrites history in every earlier PR comment. Old previews become broken images when the commit is GC'd — which is the right failure mode (broken > wrong). - Comment marker (
<!-- jsmind-render-preview -->): Clean approach for idempotent re-runs. Delete-then-post means a failure leaves the old image, not nothing. - Consolidation: One
ci.ymlinstead of two workflows — simpler to reason about. Deletingprobe-browser.tsis good housekeeping; its checks are now exercised by every CI run. - UI tests get their own timeout (3 min): A stalled browser start no longer silently eats the whole job budget.
- Inline documentation: Every non-obvious decision has a comment explaining the trade-off (force-push vs accumulate, build duplication vs artifact transfer, why the pub/sub step is separate, etc).
🟡 Suggestions
COMMIT_SHAandFULL_SHAare identical in the "Post it" step — both set togithub.event.pull_request.head.sha. OnlyFULL_SHAis needed for the image URL andCOMMIT_SHAis only used for the${:0:7}short form. Consider collapsing to one var.- Build is duplicated between
checksanduijobs. The comment correctly notes that artifact transfer would be slower (~3s build vs upload+download). Might be worth a one-liner comment in thechecksjob acknowledging the duplication so a future reader doesn't try to "fix" it by adding a job dependency.
🔴 Issues
None.
| env: | ||
| GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} | ||
| PR_NUMBER: ${{ github.event.pull_request.number }} | ||
| COMMIT_SHA: ${{ github.event.pull_request.head.sha }} |
There was a problem hiding this comment.
COMMIT_SHA and FULL_SHA (line 184) are both github.event.pull_request.head.sha — identical. FULL_SHA is used in the image URL; COMMIT_SHA only appears as ${COMMIT_SHA:0:7} → short. You can drop COMMIT_SHA and use ${FULL_SHA:0:7} instead.
There was a problem hiding this comment.
Good catch — collapsed in 50b6599.
COMMIT_SHA is gone; the step now uses a single FULL_SHA, sliced to ${FULL_SHA:0:7} for the prose and used whole for the filename. Added a comment noting the two uses so it does not look like it wants splitting again.
| run: npm run test:coverage | ||
|
|
||
| - name: Build | ||
| run: npm run build |
There was a problem hiding this comment.
Both the checks and ui jobs run npm run build. The ui job at L96-98 explains why (artifact transfer is slower than rebuilding). Consider adding a short note here — even just # The ui job builds again on its own (see note there). — so a future reader doesn't try to "optimise" by adding a job dependency and artifact download.
There was a problem hiding this comment.
Added in 50b6599 — the note is now in checks as well, next to the build itself:
# The ui job builds again on its own rather than depending on this one and
# downloading dist/ as an artifact. That round trip costs more than the
# roughly three seconds a build takes — see the note there before adding
# a `needs:` between the jobs.
You are right that the explanation only in the other job was useless to whoever is looking at this one.
| node --experimental-strip-types --no-warnings scripts/render-preview.ts \ | ||
| examples/v2-features.json preview.png | ||
|
|
||
| # The comment embeds this image with markdown, which needs a URL GitHub |
There was a problem hiding this comment.
👍 The L129-138 comment block and the PR body documenting the trial-and-error for image hosting (artifacts → releases → --attach → branch+raw URL) is excellent context. Future maintainers will thank you.
COMMIT_SHA and FULL_SHA were both `github.event.pull_request.head.sha`. One variable now, used whole for the filename and sliced for the prose. The build in `checks` is intentional — the `ui` job rebuilds rather than depending on this one and downloading dist/ through an artifact. The note was only in the other job, where a reader looking at `checks` would not see it, so it reads like duplication worth "fixing".
Render previewRendered from the latest code on this branch, at Worth a look for anything odd - overlapping nodes, missing edges, text Workflow run |

做了什么
把 CI 拆成两个 job,并让 PR 上真正显示渲染结果而不是一个链接。
两个 job
checks只读。之前所有步骤挤在一个 job 里,导致跑断言的那几步也拿到了contents: write—— 它们不需要,也不该有。ui自己 build 一次,而不是在 job 之间传dist/:走 artifact 走一圈来回,比这 3 秒慢。图片怎么内嵌的
这一步试错最多,结论值得记下来。GitHub 只接受一种图片 URL:
gh pr comment --attach/upload/policies/assets,API token 调不动application/octet-stream+Content-Disposition: attachment,GitHub 不把它当图片image/png,能渲染所以图片提交到一个
render-preview分支,评论里用raw.githubusercontent.com引用。顺带纠正一个我一开始搞错的判断:
--attach失败时我以为整个仓库只读,实测发现gh release create和git push都能成 —— token 有写权限,只是--attach走的是另一条路。文件名带 commit
preview-<sha>.png,不是共享的preview.png。共享文件名有个隐蔽的 bug:每个 PR 都覆盖同一张图,老 PR 的评论会被后来的渲染悄悄改掉,显示的是别人的代码。
分支用 force push
始终只有 1 个 commit、1 个文件。超期的图片在对应 commit 被 gc 后失效 —— 老 PR 显示裂图,而不是错误的图。评论里注明了「不定期清理,需要就下载」。
预览的价值在 review 当下,过了就没用了。
评论内容
改了措辞:说明图来自分支上的最新代码,列出值得看一眼的具体问题(节点重叠、连线缺失、文字溢出、布局歪斜),并链到产生它的 workflow run。
scripts/probe-browser.ts一并删除 —— 它验证的三件事(有浏览器、能量文字、截图确定性)现在每次 CI 都在做,留下的只有第四份 Chromium 启动参数表,而且是最旧的那份。没动的部分
npm run ci/ci2、pre-push的行为已知代价
预览图不长期保存。要长期留图得换成累积 push,代价是分支 commit 链一直变长。