Stop committing the renderer bundle - #122
Merged
juanmaguitar merged 1 commit intoAug 5, 2026
Merged
Conversation
The esbuild output at src/renderer/index.js and index.css was committed so the app could run without a build step. Regenerating it was a manual step at the end of any renderer change, and forgetting it was silent: the file is not reviewed (2.6 MB of generated output), the linter ignores it, and no job compared it against a fresh build. It had drifted. Rebuilding on a clean `npm ci`, with the committed lockfile and the same esbuild, produced 323 insertions and 177 deletions — 33 hunks from index.jsx, 4 from setup-steps.cjs, 1 from update-plan.cjs, and ~60 inside vendored @wordpress/* modules. Most visibly, the `isUpdating` flag added by the update path (#111) was absent, so the Install, Build and Dev buttons were not gated while a site update ran. The signed artifacts were never affected: all three Buildkite steps already ran `npm run build:once` before `npm run dist`. That left the committed file with no consumer in the shipping path — pure liability. So delete it and make the build unskippable, rather than adding CI to police a file nobody needs. `npm install` is already mandatory (main.js needs electron-store, isomorphic-git, ...), so hanging the build off the existing entry points costs contributors nothing. - The output is gitignored and built by `postinstall`, `start` and every `dist` script. Prefixing `start` also closes a latent race: `concurrently` could hand Electron a bundle the first watch build had not written yet, which until now the committed file happened to mask. - Buildkite's separate "Build renderer" steps go, so the build lives in one place and covers a local `npm run dist` too. - src/renderer/bundle.js and bundle.css are removed. Nothing loaded them; they were left over from when the output had a different name. src/renderer/index.js stays in the eslint ignores. It is no longer committed, but it still sits next to its own source after any build — dropping the ignore would lint 55k lines of generated code on a developer machine and nothing on a fresh CI checkout. Fixes #120 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
juanmaguitar
force-pushed
the
juanmaguitar/issue-120-committed-renderer-bundle
branch
from
August 5, 2026 17:12
efc68cd to
89f2030
Compare
|
0 findings — 0🔴 0🟡 0🔵. No This PR stops committing the esbuild renderer output ( Checked against the five dimensions:
This is a first-pass review (no prior findings to reconcile). Style / process notes (non-blocking)None beyond what's already covered above. |
juanmaguitar
deleted the
juanmaguitar/issue-120-committed-renderer-bundle
branch
August 5, 2026 17:18
This was referenced Aug 6, 2026
juanmaguitar
added a commit
that referenced
this pull request
Aug 6, 2026
Closes #116. `url:open` passed whatever it received to `shell.openExternal`, which hands an address to whatever application the OS has registered for its scheme — a wider action than "show this page in the browser". A `file:` address opens an arbitrary local path in its associated application (on Windows that can mean running it rather than viewing it), and any other registered scheme is reachable the same way. Every caller passes an http/https address today, so nothing misuses it. It matters as the second half of a chain: the renderer displays content the app does not author, and the URL the app auto-opens on server start is parsed out of the Playground server's stdout. This guard is the step that keeps any future influence over that string from becoming an action on the contributor's machine. ## What changed - **`src/external-url.js`** (new) — `ALLOWED_URL_SCHEMES = ['http:', 'https:']`, plus `isAllowedExternalUrl()` and `openExternalUrl()`. The scheme is read off a parsed `new URL()` rather than the raw string, so casing and leading whitespace (`FILE:`, ` file:`) normalize before the comparison instead of being a way around it; an address Node can't parse is refused rather than handed to the OS to interpret. What gets opened is the parser's own `href`, not the caller's string. That second half matters as much as the allow-list: the URL parser strips tabs and newlines from anywhere in the input, including the middle of the scheme, so `ht\ntp://example.com/x` validates as `http:` — and forwarding the raw text would hand the OS an address nothing had checked. For every caller here the two forms differ only by the trailing slash the parser adds to a bare origin. - **`src/main.js`** — the handler delegates to that module, passing `shell.openExternal` and an `onRefused` that logs through the existing `logEvent` (scope `url`). A refusal is visible in the log file people attach to bug reports, not silent, so a future caller that trips the guard is diagnosable. The logged address is truncated, since by hypothesis it is the influenced input. The logic lives in its own module, per the repo convention (`src/bind-loopback.js`), so both sides of the guard are testable without an Electron process. No user-facing behaviour changes: every existing caller passes http/https. ## Testing `test/external-url.test.cjs` — 11 tests, using a stub in place of `shell.openExternal` so "did this reach the OS?" is an assertion rather than something the test takes on trust: - The four address shapes the app actually passes (Trac, the feedback form, the site on an ephemeral loopback port, wp-admin) still open, and reach the stub in their normalized form. - A control-character case (`ht\ntp://example.com/x`) pins the normalization: it opens `http://example.com/x`, not the string carrying the newline. This test fails against the first commit of this PR, which forwarded the raw input. - `file:///etc/passwd` and `file:///C:/Windows/System32/cmd.exe` never reach the stub. The Windows one is the case that matters most: the OS association for a `.exe` is "run it". - `javascript:`, `data:`, `mailto:`, and OS-registered third-party schemes (`ms-msdt:`, `vscode:`) are refused too. - Junk input (`''`, whitespace, `null`, `undefined`, a number, an object, an array, `'not a url'`) is refused rather than thrown. - A refused address cannot forge a log line: control characters are escaped rather than passed through, so an address carrying a newline can't close its entry and open another one in the app's own timestamp-and-scope format. Truncation runs after escaping, since escaping is what decides the final length. - One test pins the allow-list itself, so widening it has to be a deliberate change that shows up in a diff. Suite: **158 passing, 0 failing**, on `node --test` and again on Electron's bundled Node via `npm run test:electron`. `npm run lint` is clean across the repo. ### To verify by hand Nothing in the UI can reach the refusal path by design, so the manual pass is about confirming the allowed side is unaffected. On a signed build from this branch: 1. Start a site — it should still auto-open in the browser as before. 2. Click the site URL and the **wp-admin** link in the site header — both open. 3. Open a site with the database tool and click through to **adminer** — opens. 4. Generate a patch and click the **core.trac.wordpress.org** link in the "Next steps" text — opens. 5. Click the **feedback form** link — opens. 6. Optional, to see the guard fire: from the renderer devtools console, run `window.api.openExternal('file:///etc/passwd')`. It should resolve `false`, nothing should open, and the log file (Help → the log path) should contain a `url` line reading `refused to open file:///etc/passwd — only http:, https: are allowed`. Also updates `.github/instructions/code-review.instructions.md`: it named this handler as its known-open calibration example, which stops being true here. Replaced with what the fix teaches, so the next review looks for both halves rather than re-reporting something already closed. Rebased on trunk after #122/#125/#127; the review standard's calibration example moved with it to `.github/instructions/`. 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
juanmaguitar
added a commit
that referenced
this pull request
Aug 7, 2026
Closes #109. First step of #110, following [Option A](#110 (comment)). ## Why The app knows *where* a site is and *what state its build is in*, but not *what the contributor is working on* — and Core work is organised around Trac tickets, not directories. That gap is why a generated patch carries no ticket number (#107), why there is nowhere to show a ticket's existing patches, and why applying someone else's patch (#11) has no context to start from. ## What this does A site can now be associated with a Trac ticket: - asked optionally at creation — *"What are you working on?"* - set, changed or cleared later from a new panel on the site screen - stored in `siteMeta`, so it survives restarts Parsing accepts what a contributor actually types or pastes: a bare number, a number with a `#`, and any core Trac ticket URL with a comment anchor, trailing slash or `?format=` query still attached. Anything else is rejected with a message aimed at the contributor rather than at a log. The panel sits between the action row and the Terminal; nothing else on the page moves. ## What this deliberately does not do **No network.** Linking a ticket never depends on Trac being reachable. That is a design decision, not an omission. While building this I measured every documented Trac read path — `?format=csv`, the ticket HTML, `raw-attachment/ticket/<id>/<file>`, and even `robots.txt` — and all of them currently return **403 behind a proof-of-work interstitial** for any client that is not a browser (SHA-256 hashcash, `_hcc` cookie, escalating to an "I am human" checkbox on repeat hits; a spoofed browser User-Agent gets a bare nginx 403 instead). This also breaks core's own `grunt patch`, which has parsed that page for years. So *showing* a ticket's patches needs a fetch strategy of its own and lands separately. Two things shape it: - A Contributor Day room shares one NAT IP — exactly the pattern that escalates Trac's challenge to interactive, and that exhausts unauthenticated GitHub's 60 requests/hour. Any network path has to degrade to opening the contributor's own browser, where the challenge clears itself. - The real fix is upstream: [meta #8202](https://meta.trac.wordpress.org/ticket/8202) proposes a `ticket/` JSON endpoint returning exactly the attachment data (filename, author, date, size) that panel wants. The follow-up should keep its fetch source swappable so it can adopt that endpoint when it lands. Also out of scope here: applying patches (#11), the ticket number in the patch header (#107), tickets as branches (#108), opening PRs from the app (#118). ## Testing - `test/trac-ticket.test.cjs` — 14 tests over every accepted and rejected input form. `parseTicketRef` is a pure, dependency-free module so it runs under `node --test` with no DOM, following the `setup-steps.cjs` / `update-plan.cjs` convention. - `npm test` and `npm run test:electron`: **172/172 pass** on both. - `npm run lint` (now repo-wide after #119): clean. Manual: create a site with and without a ticket; link, unlink and re-link from the panel; paste `62281`, `#62281` and a URL with `#comment:3`; restart the app and confirm the ticket survives; confirm "Open in Trac" opens the external browser. ### Fixed after manual testing Manual testing surfaced a wrong-message bug: a bare word like `abc` (and `62281abc`, `#abc`) was rejected with *"Only core.trac.wordpress.org tickets are supported"* — pointing the contributor at a host they never named. `new URL('https://abc')` succeeds because a bare word is a legal hostname, so non-URL input slipped past into the host check instead of falling to the generic message. Fixed to only treat input as a URL when it has a scheme, a path separator or a dotted host; everything else now gets *"Enter a ticket number like 62281, or a core.trac.wordpress.org ticket URL."* A github.com URL still correctly names the host as the reason. The reproducing test asserts the message, not just rejection — the gap the original suite missed. Rebased onto trunk after #122, so no generated bundle is included — the diff is five source files. 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com> Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
mokagio
added a commit
that referenced
this pull request
Aug 21, 2026
Three conflicts, all resolved in trunk's favour: - `.buildkite/pipeline.yml`: this branch ran `npm run build:once` as its own Buildkite step before `npm run dist:win`. Trunk's `dist:win` now chains `build:once` itself (#122), so the explicit step is redundant. The hardening this branch adds — `Invoke-NativeCommand` around the native calls, and the extracted `verify_windows_signature.ps1` — is kept. - `scripts/azure-sign.cjs`: trunk landed the same `/debug` passthrough with an explanatory comment; kept the comment. - `test/azure-sign.test.cjs`: trunk moved the suite to `tests/unit/`, so the `require` of `scripts/azure-sign.cjs` gains a path segment. `npm test` (1046 passing) and `npm run lint` are green on the merge result. --- Generated with the help of Claude Code, https://claude.com/claude-code Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes #120.
The esbuild output at
src/renderer/index.jsandindex.csswas committed so the app could run without a build step. Regenerating it was a manual step at the end of any renderer change, and forgetting it was silent — the file is not reviewed (2.6 MB of generated output), the linter ignores it, and no job compared it against a fresh build.It had drifted. Rebuilding on a clean
npm ci, with the committed lockfile and the same esbuild (0.23.1), produced 323 insertions / 177 deletions: 33 hunks fromindex.jsx, 4 fromsetup-steps.cjs, 1 fromupdate-plan.cjs, and ~60 inside vendored@wordpress/*modules. Most visibly, theisUpdatingflag from #111 was absent, so Install / Build / Dev were not gated while a site update ran.One correction to the issue, which decides the approach
The signed artifacts were never affected. All three Buildkite steps already ran
npm run build:oncebeforenpm run dist, so shipped builds were made from source and did have the gating. The stale bundle only bit someone runningelectron .from a checkout.That left the committed file with no consumer in the shipping path. So this deletes it and makes the build unskippable, rather than adding CI to police a file nobody needs. Drift becomes impossible rather than merely detected — the second of the two directions in the issue.
npm installis already mandatory (main.jsneedselectron-store,isomorphic-git, …), so hanging the build off the existing entry points costs contributors nothing.What changed
postinstall,start, and everydistscript.startalso closes a latent race:concurrentlycould hand Electron a bundle the first watch build had not written yet. The committed file happened to mask that.npm run disttoo.src/renderer/bundle.js/bundle.cssremoved. Nothing loaded them; leftovers from when the output had a different name.One deviation from the issue
The issue asks for the bundle lint ignores to go.
src/renderer/index.jsstays ignored: it is no longer committed, but it still sits next to its own source after any build, so dropping the ignore would lint 55k lines of generated code on a developer machine and nothing on a fresh CI checkout. Only thebundle.*ignores went.Testing
Verified locally:
npm installbuilds the bundle viapostinstall; both files resolve as ignored.npm test— 147 pass.eslint . --max-warnings=0— clean, checked both ways: with the generated bundle present (a developer machine after any build) and without it (CI'snpm ci --ignore-scriptsskipspostinstall, so the bundle never exists there). Keepingsrc/renderer/index.jsin the ignores matters more now that Finish the lint baseline and widen the CI check to the whole repo #119 lints the whole repo at zero tolerance — CI never sees the file, so only a local run would have tripped over it.|| isUpdatingon Install / Build / Dev, absent from what was committed.electron-builder --dirproduces an.appwhose asar containssrc/renderer/index.jsandindex.cssdespite the gitignore (electron-builder does not read.gitignore), and the packed bundle has the gating. Launched it — boots and renders.git statusclean, so the drift class is gone rather than fixed once.Verified on a signed artifact from Buildkite build #261, all three platforms green. The macOS
.dmgwas installed and exercised on both paths:That is the end-to-end check this issue was really about — the shipped renderer now matches its source.
Rebased onto trunk after #119 landed. The only conflict was in
.github/workflows/lint.yml, where #119 deleted the changed-files job that held a comment naming the oldbundle.js; trunk's version was taken wholesale, so that file is no longer part of this diff. Nothing in the rebase touches the app, so the manual testing above still stands.Note for reviewers with a local checkout: after pulling,
src/renderer/index.jsandindex.cssbecome untracked-and-ignored. Harmless —npm installregenerates them.🤖 Generated with Claude Code