Skip to content

fix(database-integrations): throw typed error for malformed integrations YAML - #425

Open
tkislan wants to merge 7 commits into
mainfrom
fix/malformed-integrations-yaml-typed-error
Open

fix(database-integrations): throw typed error for malformed integrations YAML#425
tkislan wants to merge 7 commits into
mainfrom
fix/malformed-integrations-yaml-typed-error

Conversation

@tkislan

@tkislan tkislan commented Jul 20, 2026

Copy link
Copy Markdown
Contributor

Fixes #424

Problem

mergeApiIntegrationsIntoYaml(existingContent, apiIntegrations) failed with the cryptic Error: Document with errors cannot be stringified when existingContent was readable-but-malformed YAML — most commonly a file left with unresolved git merge conflict markers.

yaml.parseDocument() never throws; it returns a Document with .errors populated. So the ?? createNewDocument() fallback never fired, the errored document flowed through the whole merge, and the failure only surfaced at serialize time — with no indication of which file or which line was broken.

The same defect reached the CLI through readIntegrationsDocument, affecting integrations pull, add and edit. pull and add hit the serialize-time crash; edit failed earlier with a misleading semantic error (Integration with ID "…" not found), because its isSeq-guarded helpers silently returned empty on the errored document.

Approach

Fail fast at parse time behind a typed, exported error. No silent self-healing — the user is shown the path and the parse detail, and recovery is manual.

  • parseIntegrationsDocument throws IntegrationsYamlParseError when doc.errors is non-empty. Contract is now: null for blank, Document for valid, throws for malformed.
  • The CLI wraps it as MalformedIntegrationsFileError, carrying the file path plus the parse detail (line, column, code frame), and maps it to exit code 2 (invalid usage) in all three commands.
  • pull now reads the local file before the empty-response early return, so a malformed file is caught on every run — not just when the workspace has integrations to pull.

Both error classes are exported so downstream consumers (webapp git-sync, the VS Code extension) can catch them and rebuild via mergeApiIntegrationsIntoYaml(null, apiIntegrations) if they'd rather discard a corrupt file than surface the error.

Before / after

$ deepnote integrations edit some-id --file broken.yaml

# before
Error: Document with errors cannot be stringified          (exit 1)

# after                                                     (exit 2)
Invalid YAML in integrations file: broken.yaml

Implicit keys need to be on a single line at line 2, column 1:

integrations:
<<<<<<< HEAD
^

This usually comes from unresolved merge conflict markers (<<<<<<<, =======, >>>>>>>) or a manual-edit typo.
Open the file, fix the reported line, and re-run the command — or delete the file manually if you no longer need its contents.

Why doc.errors is the right gate

This moves a failure earlier on a shared code path, so it's worth being precise about the blast radius.

Document.toString() refuses on if (this.errors.length > 0) (yaml/dist/doc/Document.js:322) — the same predicate as the new gate. The set of documents that now throw at parse time is therefore identical by construction to the set that already threw at serialize time. Nothing that works today starts failing; a cryptic late failure becomes an actionable early one.

Verified empirically across the boundary cases: duplicate keys, tab-as-indent and bad scalar starts all already threw at serialize time (so they improve), while CRLF, trailing tabs and postgres://h:5432/db produce zero errors and are untouched. Warnings are deliberately not gated on — TAG_RESOLVE_FAILED (e.g. a: !custom v) lands in doc.warnings and stringifies fine; gating on those would have been a real regression.

One intentional behavior change: pull against a zero-integration workspace with a malformed local file previously exited 0 without ever reading the file. It now exits 2. This is deliberate, tested, and documented.

Bonus fix: updateDotEnv previously ran before the failing serialize, so a malformed file could leave a half-written .env behind. The relocated read closes that window — the new tests assert .env is never created.

Tests

26 new tests. mergeApiIntegrationsIntoYaml had zero coverage before this, so it gets baseline coverage alongside the regression cases.

  • integrations-document.test.ts (new) — blank → null, valid → round-trips, conflict markers and a: b: c → typed error exposing .errors and line info.
  • merge-into-yaml.test.ts (new) — fresh-document and comment-preserving merge snapshots, both malformed fixtures, InvalidIntegrationsTypeError, and a test that executes the documented catch-and-rebuild recovery recipe. One test asserts the message does not contain Document with errors cannot be stringified, pinning the actual regression.
  • CLIreadIntegrationsDocument unit coverage, plus end-to-end cases for all three commands asserting exit code 2, the YAML file byte-identical afterwards, and .env never created. The edit action test pins both branches of the new ternary (malformed → 2, missing file → 1).

Known gap (pre-existing, not addressed here)

An unresolved YAML alias (password: *missing) produces errors: 0, warnings: 0 yet still throws at toString(). Same shape as this issue — late crash, cryptic message, partial .env write — but a different trigger, and behavior is identical before and after this change. Worth a follow-up issue; out of scope here.

Verification

Gate Result
pnpm test 2524 passed / 144 files
pnpm run typecheck clean
pnpm lintAndFormat clean
pnpm spell-check clean
pnpm build clean
CLI smoke malformed → exit 2 + file unmodified; missing → exit 1

Docs updated per AGENTS.md: new skills/deepnote/references/cli-integrations.md (registered in SKILL.md) and a note under integrations pull in the CLI README.

🤖 Generated with Claude Code

https://claude.ai/code/session_01PyawjygDS8h5qdX1ZWJoRH

Summary by CodeRabbit

  • Bug Fixes

    • Improved handling of malformed local integrations YAML (including unresolved merge conflicts) with clearer recovery guidance.
    • integrations pull|add|edit now consistently exit with code 2 and preserve the integrations YAML and any secret .env file (no partial writes).
  • Documentation

    • Updated integrations pull/add/edit docs with new defaults, shared options, interactive behavior notes, and explicit invalid-YAML guarantees.
    • Added a dedicated CLI “Integrations commands” reference and updated the CLI quick-reference table.
  • Tests

    • Expanded coverage for invalid YAML parsing, error details, exit codes, and “leave files untouched” behavior.

…ons YAML

`mergeApiIntegrationsIntoYaml` failed with the cryptic `Error: Document with
errors cannot be stringified` when given readable-but-malformed YAML, such as a
file left with unresolved git merge conflict markers. `yaml.parseDocument()`
never throws — it returns a Document with `.errors` populated — so the
`?? createNewDocument()` fallback never fired and the failure surfaced much
later, at serialize time, with no indication of which file or line was broken.

Move the failure to parse time behind a typed, exported error:

- `parseIntegrationsDocument` now throws `IntegrationsYamlParseError` when
  `doc.errors` is non-empty. This is the same predicate `Document.toString()`
  uses to refuse, so the set of inputs that now fail early is identical by
  construction to the set that already failed late — nothing that works today
  starts failing.
- The CLI wraps it as `MalformedIntegrationsFileError`, which names the file
  path and carries the parse detail (line, column and a code frame), and exits
  with code 2 (invalid usage) across `pull`, `add` and `edit`.
- `pull` now reads the local file before the empty-response early return, so a
  malformed file is reported on every run rather than only when the workspace
  has integrations. This also removes a partial-write window: previously a
  malformed file could leave a half-written `.env` behind, since `updateDotEnv`
  ran before the failing serialize.

Both errors are exported so downstream consumers (webapp git-sync, the VS Code
extension) can catch them and rebuild from scratch via
`mergeApiIntegrationsIntoYaml(null, apiIntegrations)` if they prefer that to
surfacing the error.

Fixes #424

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PyawjygDS8h5qdX1ZWJoRH
@codecov

codecov Bot commented Jul 20, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 96.29630% with 1 line in your changes missing coverage. Please review.
✅ Project coverage is 87.59%. Comparing base (d339651) to head (866e28b).

Files with missing lines Patch % Lines
packages/cli/src/commands/integrations.ts 92.85% 1 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main     #425      +/-   ##
==========================================
+ Coverage   87.36%   87.59%   +0.23%     
==========================================
  Files         181      182       +1     
  Lines        9494     9515      +21     
  Branches     2699     2634      -65     
==========================================
+ Hits         8294     8335      +41     
+ Misses       1199     1179      -20     
  Partials        1        1              

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

tkislan and others added 2 commits July 20, 2026 14:55
…ons README

`MalformedIntegrationsFileError` was exported from its own module but not
re-exported from the package entry point, so it was unreachable as
`@deepnote/cli` — leaving downstream consumers unable to catch it by type, which
is the whole point of throwing a typed error. Verified against the built
artifacts: the class is now present in `dist/index.d.ts`, `dist/index.d.cts` and
the runtime bundles, and `instanceof` holds across the package boundary.

`IntegrationsYamlParseError` needs no change — `export * from './loading'`
already surfaces it at the `@deepnote/database-integrations` root, and every
other custom error in that package is reachable the same way.

README corrections for `deepnote integrations`, checked line by line against the
command definitions in `cli.ts:845-867`:

- `--file` default was documented as `integrations.yaml`; the real default is
  `.deepnote.env.yaml` (`DEFAULT_INTEGRATIONS_FILE`).
- `--url` default was the vague "Deepnote API"; it is `https://api.deepnote.com`.
- `integrations add` and `integrations edit` were missing from the README
  entirely despite both shipping. Documented both, including the optional `[id]`
  argument to `edit` and the shared invalid-YAML failure behaviour.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PyawjygDS8h5qdX1ZWJoRH
…ons tests

Two review points, applied across every file this branch touches rather than
only the two lines they were raised on.

Removed GitHub issue references from code comments. The fixtures now describe
the scenario they reproduce — an integrations file left with unresolved git
merge conflict markers, and a hand-edit typo — which is what a reader needs;
the issue number belongs in the commit history, not the source.

Replaced force casts in tests with `assert` type guards. Each site previously
did `expect(error).toBeInstanceOf(X)` followed by `const e = error as X`,
because the matcher does not narrow. `assert(error instanceof X)` from vitest
both asserts and narrows, so the cast and the now-redundant matcher call are
gone and the assertions read against `error` directly. This matches the
existing convention in `fetch-integrations.test.ts`.

Verified the assertions were not weakened by the swap: a probe asserting a
deliberately wrong error type fails with an AssertionError, confirming `assert`
is a real runtime check and not just a compile-time narrowing hint.

10 casts removed across 5 files; no `as` outside `as const` remains on this
branch.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PyawjygDS8h5qdX1ZWJoRH
@tkislan

tkislan commented Jul 27, 2026

Copy link
Copy Markdown
Contributor Author

@coderabbitai full review

@coderabbitai

coderabbitai Bot commented Jul 27, 2026

Copy link
Copy Markdown
Contributor
✅ Action performed

Full review finished.

@coderabbitai

coderabbitai Bot commented Jul 27, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

📝 Walkthrough

Walkthrough

The integrations YAML parser now reports structured parse errors with YAML diagnostics. CLI commands wrap these errors with file paths, return exit code 2, validate before pull early returns, and leave YAML and secret files unchanged. Tests cover parsing, merging, pull, add, and edit behavior. Documentation now describes integrations commands, defaults, secret handling, and malformed-YAML behavior.

Estimated code review effort: 3 (Moderate) | ~25 minutes

Possibly related PRs

🚥 Pre-merge checks | ✅ 6
✅ Passed checks (6 passed)
Check name Status Explanation
Linked Issues check ✅ Passed The PR addresses #424 by failing early on malformed YAML and surfacing typed, file-aware errors through the merge path and CLI.
Out of Scope Changes check ✅ Passed The changes are scoped to malformed integrations YAML handling, related CLI behavior, tests, and docs.
Docstring Coverage ✅ Passed Docstring coverage is 80.00% which is sufficient. The required threshold is 80.00%.
Updates Docs ✅ Passed OSS docs were updated in README, SKILL.md, and cli-integrations.md; the private roadmap page couldn’t be verified here.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed Accurately summarizes the main change: malformed integrations YAML now throws a typed error.

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai 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.

🧹 Nitpick comments (2)
packages/database-integrations/src/loading/integrations-document.test.ts (1)

25-32: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Use ts-dedent for multiline YAML fixtures. Preserve each fixture’s trailing newline when converting.

  • packages/database-integrations/src/loading/integrations-document.test.ts#L25-L32: wrap VALID_YAML with ts-dedent.
  • packages/database-integrations/src/loading/merge-into-yaml.test.ts#L43-L58: wrap EXISTING_WITH_COMMENTS with ts-dedent.

As per coding guidelines, use ts-dedent for clean multiline template strings.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/database-integrations/src/loading/integrations-document.test.ts`
around lines 25 - 32, Wrap the VALID_YAML fixture in
integrations-document.test.ts with ts-dedent while preserving its trailing
newline; likewise wrap EXISTING_WITH_COMMENTS in merge-into-yaml.test.ts with
ts-dedent and preserve its trailing newline. Ensure both test files use the
existing ts-dedent import pattern.

Source: Coding guidelines

packages/cli/src/commands/integrations.test.ts (1)

31-51: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

CONFLICT_MARKERS_YAML fixture duplicated verbatim across three test files. Same root cause: no shared fixture module for malformed-integrations-YAML test data.

  • packages/cli/src/commands/integrations.test.ts#L31-L51: extract this fixture into a shared test helper (e.g. packages/cli/src/commands/integrations/tests/fixtures.ts) and import it here.
  • packages/cli/src/commands/integrations/edit-integration.test.ts#L16-L28: import the shared fixture instead of redefining it.
  • packages/cli/src/commands/integrations/tests/add-integration.pgsql.test.ts#L18-L30: import the shared fixture instead of redefining it.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/cli/src/commands/integrations.test.ts` around lines 31 - 51, The
CONFLICT_MARKERS_YAML fixture is duplicated across three integration test files;
extract one shared fixture and reuse it everywhere. In
packages/cli/src/commands/integrations.test.ts#L31-L51, create the shared
fixture module and import it; in
packages/cli/src/commands/integrations/edit-integration.test.ts#L16-L28 and
packages/cli/src/commands/integrations/tests/add-integration.pgsql.test.ts#L18-L30,
remove the local definitions and import the shared CONFLICT_MARKERS_YAML
fixture.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@packages/cli/src/commands/integrations.test.ts`:
- Around line 31-51: The CONFLICT_MARKERS_YAML fixture is duplicated across
three integration test files; extract one shared fixture and reuse it
everywhere. In packages/cli/src/commands/integrations.test.ts#L31-L51, create
the shared fixture module and import it; in
packages/cli/src/commands/integrations/edit-integration.test.ts#L16-L28 and
packages/cli/src/commands/integrations/tests/add-integration.pgsql.test.ts#L18-L30,
remove the local definitions and import the shared CONFLICT_MARKERS_YAML
fixture.

In `@packages/database-integrations/src/loading/integrations-document.test.ts`:
- Around line 25-32: Wrap the VALID_YAML fixture in
integrations-document.test.ts with ts-dedent while preserving its trailing
newline; likewise wrap EXISTING_WITH_COMMENTS in merge-into-yaml.test.ts with
ts-dedent and preserve its trailing newline. Ensure both test files use the
existing ts-dedent import pattern.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 17dce952-4392-443a-81d7-a30547ab992f

📥 Commits

Reviewing files that changed from the base of the PR and between 50a4af7 and 1764f54.

📒 Files selected for processing (14)
  • packages/cli/README.md
  • packages/cli/src/commands/integrations.test.ts
  • packages/cli/src/commands/integrations.ts
  • packages/cli/src/commands/integrations/add-integration.ts
  • packages/cli/src/commands/integrations/edit-integration.test.ts
  • packages/cli/src/commands/integrations/edit-integration.ts
  • packages/cli/src/commands/integrations/tests/add-integration.pgsql.test.ts
  • packages/cli/src/index.ts
  • packages/database-integrations/src/loading/integrations-document.test.ts
  • packages/database-integrations/src/loading/integrations-document.ts
  • packages/database-integrations/src/loading/merge-into-yaml.test.ts
  • packages/database-integrations/src/loading/merge-into-yaml.ts
  • skills/deepnote/SKILL.md
  • skills/deepnote/references/cli-integrations.md

coderabbitai[bot]
coderabbitai Bot previously approved these changes Jul 27, 2026
…rations tests

The malformed-YAML fixture was defined identically in three CLI test files.
Extract it into `commands/integrations/test-helpers.ts`, matching the existing
`commands/test-helpers.ts` convention, and import it in all three.

The `database-integrations` copies are left in place: sharing across the package
boundary would mean exporting a test fixture through the package's public API.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01A7XdccD84jRqXcKctzJfd3
coderabbitai[bot]
coderabbitai Bot previously approved these changes Jul 27, 2026
@tkislan
tkislan marked this pull request as ready for review July 27, 2026 15:30
@tkislan
tkislan requested a review from a team as a code owner July 27, 2026 15:30
@tkislan
tkislan requested review from m1so, mfranczel and voyti and removed request for voyti July 29, 2026 12:01
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

mergeApiIntegrationsIntoYaml throws "Document with errors cannot be stringified" on a malformed existing integrations file

2 participants