A browser-only PR review tool for OctopusDeploy/Library. Successor to Hyponome: same feature set, no server, deployed as a static site to GitHub Pages.
Live at https://octopusdeploy.github.io/Library/pr-review/.
-
Lists open pull requests on
OctopusDeploy/Library. -
For each changed file, shows the before/after side-by-side in Monaco's
DiffEditor. The before/after contents are fetched directly fromraw.githubusercontent.com(a static CDN that does not count against the GitHub API rate limit), not reconstructed from the unified-diff patch. -
For step-template JSONs, surfaces one tab per embedded script that changed in the PR, each with its own side-by-side syntax-highlighted diff. Supported properties:
Octopus.Action.Script.ScriptBody(syntax fromOctopus.Action.Script.Syntax)Octopus.Action.Terraform.TemplateOctopus.Action.Terraform.VariableValuesOctopus.Action.CustomScripts.{PreDeploy,Deploy,PostDeploy}.{ps1,sh,csx,fsx,…}— language inferred from the extension.
Scripts that exist in both before and after but didn't actually change in this PR don't get a tab (no point looking at an unchanged diff).
| Hyponome | PR Review | |
|---|---|---|
| Runtime | ASP.NET Core (server) | Static SPA (browser only) |
| GitHub access | Octokit, server-side PAT | fetch to api.github.com, unauthenticated |
| Hosting | Docker container | GitHub Pages |
| Diff viewer | Ace + ace-diff | Monaco DiffEditor |
| UI | Bootstrap 3 + jQuery | React 18 + plain CSS |
None. The site uses the GitHub REST API unauthenticated, which is rate-limited to 60 requests per hour per IP address. Each PR detail view consumes roughly 3 requests (PR + files + occasionally the raw diff for files with omitted patches), so casual browsing of ~15 PRs per hour fits comfortably.
When the limit is hit, the site renders an explanatory error state with the reset time. There is no PAT prompt and no token storage — keeping the deployment truly static and avoiding any browser-stored secrets.
cd pr-review
npm install
npm run devOpens at http://localhost:5173/Library/pr-review/. Hot reload works as
expected via Vite.
Useful scripts:
| Script | Purpose |
|---|---|
npm run dev |
Vite dev server with HMR |
npm run build |
Type-check, then build a production bundle into dist/ |
npm run typecheck |
TS only, no emit |
npm run preview |
Serve the production build locally |
Pushes to master/main that touch pr-review/** or the workflow file run
.github/workflows/pr-review-deploy.yml, which builds the site and publishes
it to GitHub Pages.
One-time setup (only needed the first time):
- In repo Settings → Pages, set Source to GitHub Actions.
- Run the workflow once (push or Run workflow in the Actions tab).
The site lands at https://octopusdeploy.github.io/Library/pr-review/. If
you change the deployment path, also update base in vite.config.ts and
the _site/pr-review path in the workflow.
src/
├── main.tsx Entry — mounts <App/>.
├── App.tsx HashRouter + header + footer.
├── styles.css All app styling (CSS custom properties, dark mode).
├── routes/
│ ├── PullRequestList.tsx Open PRs list.
│ └── PullRequestDetail.tsx PR header + file panels.
├── api/
│ ├── github.ts fetch-based client. Lists PRs/files via api.github.com,
│ │ reads file contents from raw.githubusercontent.com.
│ └── types.ts Hand-trimmed shapes for the endpoints we use.
├── lib/
│ ├── extractScript.ts Pull every changed Octopus script out of a step-template
│ │ JSON (ScriptBody, Terraform, custom scripts).
│ └── languageDetect.ts Filename → Monaco language id; binary detection.
└── components/
├── PullRequestHeader.tsx Title / branches / author.
├── FilePanel.tsx Tabbed per-file panel (File diff + one tab per script).
├── ErrorState.tsx Generic + rate-limit-aware error rendering.
└── TimeAgo.tsx Relative timestamps.
The repo target (OctopusDeploy/Library) is hardcoded in src/api/github.ts.
A fork that wants to point this at a different repo edits one constant.
GitHub Pages is static hosting — any deep link like /pulls/123 would 404 on
refresh. HashRouter keeps the route entirely in the URL fragment
(#/pulls/123), which the server never sees. Two-route SPA, internal tool,
zero extra deployment complexity.
Navigating from one PR to another sometimes logs:
Uncaught Error: TextModel got disposed before DiffEditorWidget model got reset
This is a known race inside @monaco-editor/react where Monaco's lazy CDN
load completes after React has already unmounted the previous DiffEditor.
It's thrown asynchronously during teardown, doesn't affect any rendered UI,
and can be ignored. Each DiffEditor already gets a stable key per
file.sha/file.filename to force a clean remount, which suppresses the
worst of it; the remaining warning is upstream.