Skip to content

fix(flue-review): hydrate from the GitHub tarball instead of JS git - #2386

Merged
ascorbic merged 3 commits into
mainfrom
fix/flue-review-tarball-hydration
Aug 8, 2026
Merged

fix(flue-review): hydrate from the GitHub tarball instead of JS git#2386
ascorbic merged 3 commits into
mainfrom
fix/flue-review-tarball-hydration

Conversation

@ascorbic

@ascorbic ascorbic commented Aug 8, 2026

Copy link
Copy Markdown
Collaborator

What does this PR do?

Fixes the repo-wide review-bot outage (every review since ~16:50 UTC today silently stalling in hydrating until watchdog timeout): replaces isomorphic-git hydration with the GitHub tarball API.

Root cause: hydration shallow-cloned the repo with JS git inside the workflow Durable Object. isomorphic-git's pack indexing (pure-JS inflate of the whole packfile) stopped completing once emdash's shallow pack crossed ~16MB — today's merges (two ~9k-line translation catalogs among them) pushed it over. Silent stall: no error, no logs, watchdog kill, deterministic on every PR and every worker build. July's intermittent FLueError failures were the near-threshold era of the same cliff. Eliminated en route, each by direct test: trace collection (#2384 — innocent, can re-land), R2 availability (packfile put: 610ms), GitHub pack serving (native clone: 7s), PR content, and run concurrency.

The fix: the reviewer never needed git — it needs the files at the PR head plus the unified diff, which was already API-fetched. Hydration now fetches GET /repos/{o}/{r}/tarball/{headSha} (head SHAs of fork PRs are reachable in the base repo), streams through the runtime-native DecompressionStream("gzip"), and untars into the workspace: ustar + GNU longname + pax path handling, symlinks preserved, archive root stripped, one entry buffered at a time. Mode bits dropped (no shell, nothing executes). Hydration now completes in ~2s where the old path ran forever.

Also retained: the stage/R2 instrumentation added during diagnosis (every hydration stage and R2 op logs start/end), so the next stall identifies itself instead of costing a day.

Verification — the worker is already running this code (deployed ahead of the PR per maintainer instruction, given the bot cannot review anything while down): parser validated in Node against the real tarball with a byte-for-byte path-set comparison vs system tar (3,273 entries, zero missing/extra, symlinks intact), then end-to-end in production — the bot reviewed #2375 successfully at 21:31 UTC on exactly this code. This PR's own review below is the fix reviewing itself.

Closes #

Type of change

  • Bug fix

Checklist

  • I have read CONTRIBUTING.md
  • pnpm typecheck passes — n/a-with-note: the package's known pre-existing env-dependent baseline; the changed file introduces no new errors
  • pnpm lint passes — oxlint clean on changed files
  • pnpm test passes — n/a: package has no unit suite for the workflow; verification was the byte-exact Node parser comparison + the production self-test above
  • pnpm format has been run
  • I have added/updated tests for my changes — see verification note
  • User-visible strings in the admin UI are wrapped for translation — n/a
  • I have added a changeset — n/a, unpublished infra worker
  • New features link to an approved Discussion — n/a, bug fix

AI-generated code disclosure

  • This PR includes AI-generated code — model/tool: Claude Fable 5 (Claude Code)

Screenshots / test output

hydrate step: tarball response   224ms
hydrate step: untarred 3252 files 45654332 bytes   ~2s
review posted on #2375: 21:31:18 UTC (first success in 10 hours)
parser vs system tar: 3273 paths, 0 missing, 0 extra

Try this PR

Open a fresh playground →

A full working EmDash site, deployed from this branch. Each visit gets its own session-scoped sandbox: no login needed and no shared state. Try the admin, edit content, hit the public site.

Tracks fix/flue-review-tarball-hydration. Updated automatically when the playground redeploys.

isomorphic-git pack indexing in the workflow DO stopped completing once
the repo's shallow pack passed ~16MB, silently stalling every review in
the hydrating stage until the watchdog killed it. The tarball of the PR
head SHA needs no git objects at all: stream it through the runtime's
native gzip DecompressionStream and untar into the workspace (ustar +
GNU longname + pax paths, symlinks included, archive root stripped).
The diff was already API-fetched; nothing downstream used the git repo.

Also keeps the hydration stage/R2 instrumentation added while
diagnosing: every stage and R2 operation logs start/end so the next
stall identifies itself.
Copilot AI lite review requested due to automatic review settings August 8, 2026 21:39
@changeset-bot

changeset-bot Bot commented Aug 8, 2026

Copy link
Copy Markdown

⚠️ No Changeset found

Latest commit: 06ffa4f

Merging this PR will not cause a version bump for any packages. If these changes should not result in a new version, you're good to go. If these changes should result in a version bump, you need to add a changeset.

This PR includes no changesets

When changesets are added to this PR, you'll see the packages that this PR includes changesets for and the associated semver types

Click here to learn what changesets are, and how to add one.

Click here if you're a maintainer who wants to add a changeset to this PR

@cloudflare-workers-and-pages

cloudflare-workers-and-pages Bot commented Aug 8, 2026

Copy link
Copy Markdown

Deploying with  Cloudflare Workers  Cloudflare Workers

The latest updates on your project. Learn more about integrating Git with Workers.

Status Name Latest Commit Updated (UTC)
✅ Deployment successful!
View logs
emdash-demo-cache 06ffa4f Aug 08 2026, 09:57 PM

@pkg-pr-new

pkg-pr-new Bot commented Aug 8, 2026

Copy link
Copy Markdown

Open in StackBlitz

@emdash-cms/admin

npm i https://pkg.pr.new/@emdash-cms/admin@2386

@emdash-cms/auth

npm i https://pkg.pr.new/@emdash-cms/auth@2386

@emdash-cms/auth-atproto

npm i https://pkg.pr.new/@emdash-cms/auth-atproto@2386

@emdash-cms/blocks

npm i https://pkg.pr.new/@emdash-cms/blocks@2386

@emdash-cms/cloudflare

npm i https://pkg.pr.new/@emdash-cms/cloudflare@2386

@emdash-cms/contentful-to-portable-text

npm i https://pkg.pr.new/@emdash-cms/contentful-to-portable-text@2386

emdash

npm i https://pkg.pr.new/emdash@2386

create-emdash

npm i https://pkg.pr.new/create-emdash@2386

@emdash-cms/gutenberg-to-portable-text

npm i https://pkg.pr.new/@emdash-cms/gutenberg-to-portable-text@2386

@emdash-cms/plugin-cli

npm i https://pkg.pr.new/@emdash-cms/plugin-cli@2386

@emdash-cms/plugin-types

npm i https://pkg.pr.new/@emdash-cms/plugin-types@2386

@emdash-cms/registry-client

npm i https://pkg.pr.new/@emdash-cms/registry-client@2386

@emdash-cms/registry-lexicons

npm i https://pkg.pr.new/@emdash-cms/registry-lexicons@2386

@emdash-cms/registry-verification

npm i https://pkg.pr.new/@emdash-cms/registry-verification@2386

@emdash-cms/sandbox-workerd

npm i https://pkg.pr.new/@emdash-cms/sandbox-workerd@2386

@emdash-cms/x402

npm i https://pkg.pr.new/@emdash-cms/x402@2386

@emdash-cms/plugin-ai-moderation

npm i https://pkg.pr.new/@emdash-cms/plugin-ai-moderation@2386

@emdash-cms/plugin-atproto

npm i https://pkg.pr.new/@emdash-cms/plugin-atproto@2386

@emdash-cms/plugin-audit-log

npm i https://pkg.pr.new/@emdash-cms/plugin-audit-log@2386

@emdash-cms/plugin-color

npm i https://pkg.pr.new/@emdash-cms/plugin-color@2386

@emdash-cms/plugin-embeds

npm i https://pkg.pr.new/@emdash-cms/plugin-embeds@2386

@emdash-cms/plugin-field-kit

npm i https://pkg.pr.new/@emdash-cms/plugin-field-kit@2386

@emdash-cms/plugin-forms

npm i https://pkg.pr.new/@emdash-cms/plugin-forms@2386

@emdash-cms/plugin-webhook-notifier

npm i https://pkg.pr.new/@emdash-cms/plugin-webhook-notifier@2386

commit: 06ffa4f

@cloudflare-workers-and-pages

cloudflare-workers-and-pages Bot commented Aug 8, 2026

Copy link
Copy Markdown

Deploying with  Cloudflare Workers  Cloudflare Workers

The latest updates on your project. Learn more about integrating Git with Workers.

Status Name Latest Commit Updated (UTC)
✅ Deployment successful!
View logs
emdash-demo-do 06ffa4f Aug 08 2026, 09:56 PM

@cloudflare-workers-and-pages

cloudflare-workers-and-pages Bot commented Aug 8, 2026

Copy link
Copy Markdown

Deploying with  Cloudflare Workers  Cloudflare Workers

The latest updates on your project. Learn more about integrating Git with Workers.

Status Name Latest Commit Updated (UTC)
✅ Deployment successful!
View logs
emdash-playground 06ffa4f Aug 08 2026, 09:56 PM

@emdashbot emdashbot Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The approach is the right fix: the outage was caused by pure-JS git pack indexing stalling as the repo grew, and the reviewer only ever needed the file tree at the PR head plus the canonical diff. Fetching the GitHub tarball and streaming it through the runtime-native DecompressionStream"gzip") is simpler, faster, and removes the problematic code path entirely. It fits the Cloudflare Shell model (no exec, just workspace files).

What I checked:

  • Full diff and the two changed files (review.ts and cloudflare-shell.ts).
  • Call sites and the existing test suite under infra/flue-review/test.
  • AGENTS.md conventions (comments, tests, scope, etc.).

Headline conclusion: the fix works, but the new tar parser needs path-traversal hardening and a regression test. I also found stale/narrative comments left over from the incident.

Blocking-ish concerns

  1. Tar path traversal (untarInto) — the code trusts archive entry paths. A tarball entry whose path contains .. or an absolute path could write outside /repo; symlink targets are passed through verbatim and pose the same risk. GitHub tarballs are unlikely to contain such entries, but the parser is the boundary between an external PR and the review workspace, so it should reject escapes explicitly.
  2. Missing automated regression testinfra/flue-review/test already uses vitest, yet the new parser has no test. A crafted tarball covering regular files, directories, symlinks, GNU longname, pax, zero-byte entries, and an entry with .. should be exercised in CI so a parser regression cannot silently take the bot down again.

Non-blocking

  • The top-of-file comment in review.ts still describes JS-git hydration.
  • The "Temporary ... (2026-08-08 review-stall incident)" comments are incident narrative; they will age poorly and should be trimmed to a one-line rationale or removed.

No Lingui/Tailwind/SQL/query-count issues are touched by this infra-only change.


Findings

  • [needs fixing] infra/flue-review/.flue/workflows/review.ts:271

    untarInto builds the destination path by joining destDir with the archive-provided relative, but it never validates that relative stays inside destDir. A tarball entry named ../foo, /etc/passwd, or dir/../../bar would be written wherever the Workspace resolves it. The same applies to symlink targets passed straight to workspace.symlink. The Workspace may or may not normalize .., so the parser must defend the boundary itself.

    		const relative = stripRoot(fullName);
    		if (!relative) continue;
    		if (relative.startsWith("/") || relative.split("/").includes("..")) {
    			throw new Error(`tar entry path escapes workspace: ${fullName}`);
    		}
    		const dest = `${destDir}/${relative}`;
    

    For symlinks, also validate that the resolved target stays under destDir (reject absolute targets and targets with .. that escape).

  • [needs fixing] infra/flue-review/.flue/workflows/review.ts:175

    This package already has a vitest suite (infra/flue-review/test/*.test.ts), but the new tar parser has no automated test. AGENTS.md requires failing-test-first for bug fixes, and a parser regression here would silently re-break every review. Add a unit test that constructs a small tarball (regular files, directories, symlinks, zero-byte file, GNU longname entry, pax path entry) and asserts that the mocked Workspace receives the expected normalized paths and bytes — including a negative case that rejects a .. path.

  • [suggestion] infra/flue-review/.flue/workflows/review.ts:5

    The header comment still says hydration is done "via JS git" with a shallow clone. The PR replaces that with the GitHub tarball, so this comment is now misleading.

    // (DO SQLite + R2 for large files) from the GitHub tarball of the PR head, and
    
  • [suggestion] infra/flue-review/.flue/workflows/review.ts:159

    Incident-date narrative in a comment will go stale quickly. AGENTS.md says comments are for future readers, not reviewers or incident timelines. Trim to the non-obvious rationale (these logs intentionally bracket stages so a hang appears as an unmatched start line) and remove the date/incident references.

  • [suggestion] infra/flue-review/.flue/workflows/review.ts:291

    This comment is mainly historical narrative about the 2026-08-08 outage. Future readers need the "why" (JS git pack indexing stalls on large repos; tarball avoids it), not the date. Trim the incident references.

  • [suggestion] infra/flue-review/.flue/sandboxes/cloudflare-shell.ts:223

    The "Temporary R2 diagnostics (2026-08-08 review-stall incident)" comment references a specific incident date and frames the code as temporary. If it is meant to be removed soon, consider tracking that in an issue/comment in the PR rather than the code; if it is staying, describe the operator-facing behavior without the incident narrative.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR fixes flue-review workflow “hydrating” stalls by replacing JS git-based hydration with a GitHub tarball download + streaming untar into the durable Workspace, and adds additional hydration/R2 instrumentation to make future stalls diagnosable.

Changes:

  • Replace isomorphic-git shallow clone/fetch/checkout with GET /repos/{owner}/{repo}/tarball/{headSha} hydration.
  • Stream-decompress (DecompressionStream("gzip")) and incrementally untar into the Workspace (including longname + pax path overrides).
  • Add structured logging around hydration steps and R2 bucket operations.

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated 4 comments.

File Description
infra/flue-review/.flue/workflows/review.ts Switch hydration to GitHub tarball + add untar implementation and hydration step logging.
infra/flue-review/.flue/sandboxes/cloudflare-shell.ts Add R2 operation instrumentation wrapper used by the Workspace.
Suppressed comments (1)

infra/flue-review/.flue/workflows/review.ts:277

  • Symlink entries are created with an unvalidated linkTarget. Absolute or traversal targets (e.g. ../../...) can create links that point outside the hydrated repo, which is risky for any later file reads/writes that may follow symlinks.
		} else if (type === "2") {
			await ensureDir(parentOf(dest));
			await workspace.symlink(linkTarget, dest);

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment on lines +269 to +272
const relative = stripRoot(fullName);
if (!relative) continue;
const dest = `${destDir}/${relative}`;

Comment on lines +159 to +162
// Temporary hydration diagnostics (2026-08-08 review-stall incident): brackets
// every stage so a hang shows as a start line with no matching end line. R2
// operations are instrumented in the shared getDefaultWorkspace. Remove once
// the stall is diagnosed.
Comment on lines +306 to 309
const url = `https://api.github.com/repos/${payload.owner}/${payload.repo}/tarball/${payload.headSha}`;
const response = await fetch(url, {
headers: { "User-Agent": "emdash-flue-review", Accept: "application/vnd.github+json" },
});
Comment on lines +223 to +225
// Temporary R2 diagnostics (2026-08-08 review-stall incident). Wrapped once and
// cached per underlying bucket: Workspace fingerprints construction options per
// storage, so every construction site must receive the identical instance.
@github-actions github-actions Bot added review/awaiting-author Reviewed; waiting on the author to respond cla: signed and removed review/needs-review No maintainer or bot review yet labels Aug 8, 2026
Review findings on the tarball hydration: the parser is the boundary
between an externally-supplied archive and the workspace, so entry
paths and symlink targets now reject absolute paths and any traversal
that would resolve outside the destination. The parser moves to
lib/untar.ts behind a narrow target interface with a vitest suite
covering files, directories, symlinks, GNU longname, pax paths,
truncation, and the escape rejections. Stale JS-git and incident
narrative trimmed from comments.
@ascorbic ascorbic added the bot:review Trigger an emdashbot code review on this PR label Aug 8, 2026
@github-actions github-actions Bot added review/needs-rereview Author pushed changes since the last review and removed review/awaiting-author Reviewed; waiting on the author to respond labels Aug 8, 2026

@emdashbot emdashbot Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The approach remains sound: replacing isomorphic-git pack indexing with a GitHub tarball fetch directly fixes the deterministic hydration stall and removes the problematic code path. This re-review confirms all concerns from the previous round have been addressed.

What changed since the last review:

  • The tar parser is now extracted into infra/flue-review/.flue/lib/untar.ts, with explicit path and symlink-target validation that rejects absolute paths and any .. sequence that escapes the destination.
  • A regression test suite (infra/flue-review/test/untar.test.ts) covers regular files, directories, symlinks, zero-byte files, GNU longname, pax path overrides, ustar prefix, path-traversal rejections, absolute-path rejections, symlink-target escapes, and truncated-archive handling.
  • Incident-narrative and JS-git references in comments have been trimmed to evergreen rationale.

I statically re-read the new untar.ts, review.ts, and cloudflare-shell.ts, checked for leftover incident narrative, and confirmed the tests are present. The trust-boundary validation, the idempotent HYDRATED marker, R2 instrumentation, and the streaming gzip-to-tar pipeline all look correct. The comment style now matches AGENTS.md guidance. No Lingui/Tailwind/SQL/query-count/content-table concerns are touched by this infra-only change.

All blocking items from the prior review are resolved; I have no further findings.

@emdashbot emdashbot Bot removed the bot:review Trigger an emdashbot code review on this PR label Aug 8, 2026
@github-actions github-actions Bot added review/approved Approved; no new commits since and removed review/needs-rereview Author pushed changes since the last review labels Aug 8, 2026
@ascorbic ascorbic added the bot:review Trigger an emdashbot code review on this PR label Aug 8, 2026
@ascorbic
ascorbic enabled auto-merge (squash) August 8, 2026 21:59
@ascorbic
ascorbic merged commit b30f7ac into main Aug 8, 2026
49 checks passed
@ascorbic
ascorbic deleted the fix/flue-review-tarball-hydration branch August 8, 2026 22:00

@emdashbot emdashbot Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The approach is sound and directly fixes the outage: replacing isomorphic-git's pure-JS pack indexing with a streaming GitHub tarball fetch removes the deterministic hydration stall and simplifies the worker. This re-review confirms the prior round's concerns have been addressed — the tar parser is now isolated, path/symlink validation is present, tests cover the common archive formats and traversal attacks, and the incident narrative in comments has been trimmed.

I statically re-read infra/flue-review/.flue/lib/untar.ts, .flue/workflows/review.ts, .flue/sandboxes/cloudflare-shell.ts, and test/untar.test.ts. No core EmDash conventions are touched (no Lingui/Tailwind/SQL/content-table changes). Two parser gaps remain:

  1. The untar parser buffers an entire entry's content before writing, and it never caps the declared size. A malformed or merely very large tar entry can exhaust the Durable Object's memory during need(padded). Given that this parser is explicitly framed as a trust boundary, it should reject entries above a safe ceiling.
  2. The pax parser only reads the path keyword, not linkpath. GitHub tarballs use pax extended headers for symlinks whose targets exceed the 100-byte ustar linkname field, so a long symlink target would be silently truncated/wrong.

Both are straightforward to close and don't change the overall fix.


const rawName = readCString(header.subarray(0, 100));
const prefix = readCString(header.subarray(345, 500));
const size = parseInt(readCString(header.subarray(124, 136)).trim() || "0", 8);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[needs fixing] The octal size is parsed but never bounded, and need(padded) below buffers the entire entry before writeFileBytes. A malformed header—or a legitimate large binary—can declare a multi-gigabyte size and exhaust the DO's memory before the write happens. Add an explicit max-entry-size guard after parsing size.

Suggested change
const size = parseInt(readCString(header.subarray(124, 136)).trim() || "0", 8);
const size = parseInt(readCString(header.subarray(124, 136)).trim() || "0", 8);
// Guard against runaway memory use: one entry is buffered at a time.
const MAX_ENTRY_BYTES = 128 * 1024 * 1024;
if (size > MAX_ENTRY_BYTES) {
throw new Error(`tar entry too large: ${size} bytes`);
}

for (const line of text.split("\n")) {
const eq = line.indexOf("=");
if (eq > 0 && line.slice(line.indexOf(" ") + 1, eq) === "path") {
pendingPaxPath = line.slice(eq + 1);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[suggestion] The pax parser only honors the path keyword, so a symlink target longer than 100 bytes (stored by GitHub tarballs in a pax linkpath record) falls back to the truncated ustar linkTarget field. Track linkpath the same way as path and use it when present.

Suggested change
pendingPaxPath = line.slice(eq + 1);
// pax records: "<len> key=value\n"
const text = decoder.decode(content);
for (const line of text.split("\n")) {
const eq = line.indexOf("=");
if (eq > 0) {
const key = line.slice(line.indexOf(" ") + 1, eq);
if (key === "path") pendingPaxPath = line.slice(eq + 1);
if (key === "linkpath") pendingPaxLink = line.slice(eq + 1);
}
}

@emdashbot emdashbot Bot removed the bot:review Trigger an emdashbot code review on this PR label Aug 8, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

cla: signed review/approved Approved; no new commits since size/L

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants