Repository navigation
feat(cli): summarize the deploy trace when a deploy fails - #294
Conversation
Every deploy writes a JSONL trace to `~/.void/logs/`, and a failure printed
its path. On a CI runner that path dies with the job, so the cause was
unreachable exactly where it mattered most:
deploy: Deploy failed: deploy in progress
Deployment: dpl_7zgitxdrxxz9
Detailed log: /home/runner/.void/logs/deploy-...jsonl <- gone
`runDeploy` now closes the logger, reads the file back, and appends a summary
to the error message: every `error` record with its flattened cause chain,
then the last 20 records as a timeline.
The trace was a file the user opened by hand; it is now published to whatever
reads stderr. Three value patterns (signed query parameters, bearer tokens,
inline `NAME=value` credential assignments) plus whole-field masking by key
name scrub it on the way out. A build command comes from `void.json`, so
`SENTRY_AUTH_TOKEN=... npm run build` reaches the log as an ordinary field.
The summary is a postmortem aid and must never replace the failure it
describes. `formatErrorNodeInline` coerces every part of a parsed record
rather than trusting its declared shape, and the enrichment is wrapped:
`message` is read-only on a frozen `Error` and getter-only on a
`DOMException`, so the assignment alone can throw.
Not covered: paths that call `process.exit` directly, such as a Class A build
failure. Those bypass the catch, and `execSync` already streams the build
error with `stdio: 'inherit'`.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YZ88Y4fzBDSBBserEfvBrB
Bugbot couldn't run - usage limit reachedBugbot is counted against Cursor usage for this user or team, and this run hit a usage or spend limit. A user or team admin can review and increase usage limits in the Cursor dashboard. (requestId: serverGenReqId_cc7d2c84-2ab7-431d-8e98-a617bc8255b9) |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 6165b8effb
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
The name-based pattern misses the case where the credential rides in a URI: `DATABASE_URL=postgres://user:pw@host/db` names no credential term, and a bare `--registry https://u:pw@host` is not an assignment at all. Both reach the summary through `build_start`, and from there into a CI log. Userinfo is now masked wherever a URI appears. The scheme and host survive, because those are the parts that make the record worth reading. A userinfo with no colon is one opaque value, which in that position is usually a token, so it is masked whole. Also correct the docs. They said a failed deploy prints a summary, without qualification. Pre-flight gates and build failures exit before the catch and print no summary — each already prints its own message, and a build streams its output straight to the terminal. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01YZ88Y4fzBDSBBserEfvBrB
Bugbot couldn't run - usage limit reachedBugbot is counted against Cursor usage for this user or team, and this run hit a usage or spend limit. A user or team admin can review and increase usage limits in the Cursor dashboard. (requestId: serverGenReqId_10c58172-4271-448c-9688-4b649fbdc404) |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 4786d26736
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
Drop the pattern that masked `NAME=value` assignments whose name contained TOKEN, SECRET, AUTH, and similar terms. Name matching buys little and misses more. GitHub Actions masks every value registered under `secrets.*` before it reaches the log, and a credential written as a literal in `void.json` is committed to the repository already, so the summary is not where it first becomes visible. The masking that stays is the masking a secret masker cannot do: a password spliced into a connection URI is not the whole secret value, so no pipeline keyed on that value recognizes it. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01YZ88Y4fzBDSBBserEfvBrB
Bugbot couldn't run - usage limit reachedBugbot is counted against Cursor usage for this user or team, and this run hit a usage or spend limit. A user or team admin can review and increase usage limits in the Cursor dashboard. (requestId: serverGenReqId_81f2a3f5-416b-4444-8c0b-30cdaf7344f2) |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 2c49b36be1
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
Drop the connection-URI userinfo pattern. The summary no longer parses credentials out of values the user supplied. This draws the line at ownership. Void masks the credentials it emits itself — a presigned URL it built, an auth header it set, a field it named. Everything else belongs to the CI platform, which holds the secrets and masks them before the log is written; GitHub Actions does this for everything under `secrets.*`. The consequence is documented: a password inside a `DATABASE_URL` in a build command now prints as written. The docs say so, and say to register such values as CI secrets instead. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01YZ88Y4fzBDSBBserEfvBrB
Bugbot couldn't run - usage limit reachedBugbot is counted against Cursor usage for this user or team, and this run hit a usage or spend limit. A user or team admin can review and increase usage limits in the Cursor dashboard. (requestId: serverGenReqId_4d582a06-0659-4421-a362-c9d939110acf) |
The problem
Every
void deploywrites a JSONL trace to~/.void/logs/. On failure the CLI printed its path:That runner is destroyed with the job, so the file is unreachable exactly where the trace matters most. Seen in vite-plus#2511.
The change
runDeploycloses the logger, reads the file back, and appends a summary to the error message.Two blocks: every
errorrecord with its flattened cause chain, then the last 20 records as a timeline. It prints on every failure, in CI and locally.Redaction
Void masks the credentials it emits itself, because the summary reaches CI logs that the trace file never did.
?X-Amz-Signature=…,&token=…Authorization: Bearer …apiToken,sessionCookie,privateKeyMasking your own values is left to your CI platform, which holds the secrets and masks them before the log is written. GitHub Actions does this for everything under
secrets.*.Void does not guess at credential-shaped variable names, and it does not parse credentials out of values you supplied. A password inside a
DATABASE_URLin your build command prints as written. The docs say so, and say to register such values as CI secrets instead.authis likewise absent from the key list — it matches metadata keys such asauthMethodfar more often than credentials.Safety
A postmortem aid must never replace the failure it describes.
formatErrorNodeInlinecoerces every part of a parsed record instead of trusting its declared shape. A record witherr.message: 123used to throwTypeError: t.replace is not a function.try/catch.messageis read-only on a frozenErrorand getter-only on aDOMException, which an aborted fetch produces, so the assignment alone can throw. Losing the summary is fine; losing the deploy error is not.Not covered
Pre-flight failures print no summary. There are 73
exit()sites reachable insiderunDeploy— a missing project, a rejected flag combination, an unsupported--backend cloudflarefeature. Each one callslog.error()with a full, hand-written message first, and each fires before any deploy work happens, so there is no trace to summarize.deps.exitis an injected seam the tests rely on.A build failure prints no summary either:
execSyncruns withstdio: 'inherit', so the compiler output is already on the terminal in full.Routing those sites through one finalization boundary would change exit codes and output across the Cloudflare backend, auth, and validation. That belongs in its own PR.
Files
packages/void/src/cli/log-summary.tsnullon any failure.packages/void/src/cli/log-file.tsErrChainNode; addformatErrorNodeInlinefor already-serialized nodes.packages/void/src/cli/deploy.tsrunDeploy's catch.packages/void/test/unit/deploy-log-summary.test.tsdocs/reference/cli.mdVerification
vp run check— cleanvp lint— cleanvp fmt— no changesvp test run packages/void— 3787 passed, 14 skippedOne browser file,
packages/react/test/integration/react-pages-client.test.ts, timed out on hydration in a full local run (31 of 93 tests,page.waitForSelector('#app[data-hydrated="true"]')).deploy.tsalready importednode:fsandpicocolors, andlog-summary.tshas no top-level side effects, so this diff adds nothing executable to the graph that test loads.🤖 Generated with Claude Code
https://claude.ai/code/session_01YZ88Y4fzBDSBBserEfvBrB
Note
Medium Risk
The summary is appended to stderr/CI logs, so incomplete redaction could leak credentials. Failures in summarization are swallowed so they cannot replace the original deploy error.
Overview
On a failed
void deploy, the CLI now flushes the JSONL trace, reads it back, and appends a summary to the error message so the cause is visible in CI after the runner (and~/.void/logs/) is gone.The summary has two blocks: up to five
errorrecords with flattened cause chains (skipping the terminaldeploy_endecho), then the last 20 records as a timeline. Pre-flight and build failures still print no summary.Credentials the CLI emits itself are masked: signed query params, bearer tokens, and fields whose keys look like secrets. User-supplied values (e.g. a password inside
DATABASE_URLin a build command) are left as-is for the CI secret masker. Summarization is wrapped so a read-onlyError.messageor a corrupt log cannot hide the original failure.Reviewed by Cursor Bugbot for commit 38b9eab. Bugbot is set up for automated code reviews on this repo. Configure here.