feat(notion): add sync-notion.mjs — push tracker to Notion Kanban dashboard#670
feat(notion): add sync-notion.mjs — push tracker to Notion Kanban dashboard#670Schlaflied wants to merge 3 commits into
Conversation
…hboard Adds a zero-token script that reads data/applications.md and creates/ updates a Notion database so the job search pipeline is visible as a shareable Kanban board grouped by application status. Features: - Parses markdown tracker table (all columns: company, role, score, status, date, PDF flag, report link, notes) - Creates the Notion database on first run; persists database_id to config/profile.yml automatically - Upserts pages: creates new entries, updates existing ones by title - Optional: uploads interview-prep/*.md as Notion sub-pages - --dry-run: preview without any API writes - --since N: only sync entries from the last N days - --auth: test token + list accessible parent pages - Config via config/profile.yml under notion: key (token, parent_page_id, database_id, sync_interview_prep) Closes #(TBD)
|
Warning Rate limit exceeded
You’ve run out of usage credits. Purchase more in the billing tab. ⌛ How to resolve this issue?After the wait time has elapsed, a review can be triggered using the We recommend that you space out your commits to avoid hitting the rate limit. 🚦 How do rate limits work?CodeRabbit enforces hourly rate limits for each developer per organization. Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout. Please see our FAQ for further information. ℹ️ Review info⚙️ Run configurationConfiguration used: Path: .coderabbit.yaml Review profile: ASSERTIVE Plan: Pro Run ID: 📒 Files selected for processing (1)
📝 WalkthroughWalkthroughAdds ChangesNotion Sync Script
Sequence DiagramsequenceDiagram
participant User
participant CLI as sync-notion.mjs
participant Config as Config System
participant Parser as Markdown Parser
participant NotionAPI as Notion API
participant DB as Notion Database
User->>CLI: node sync-notion.mjs
CLI->>Config: load config/profile.yml
Config-->>CLI: token, parent_page_id, database_id
CLI->>Parser: parse data/applications.md
Parser-->>CLI: array of applications
CLI->>NotionAPI: POST /databases (create if needed)
NotionAPI->>DB: create database
DB-->>NotionAPI: database_id
NotionAPI-->>CLI: persist database_id to config
CLI->>NotionAPI: GET /databases/{id}/query
NotionAPI->>DB: fetch existing pages
DB-->>NotionAPI: page list
NotionAPI-->>CLI: build index by title
CLI->>NotionAPI: PATCH/POST /pages (upsert loop)
NotionAPI->>DB: update or create pages
DB-->>NotionAPI: success
NotionAPI-->>CLI: created/updated counts
CLI-->>User: print summary and Done
Estimated code review effort🎯 4 (Complex) | ⏱️ ~45 minutes 🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 8
🤖 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.
Inline comments:
In `@sync-notion.mjs`:
- Line 125: The findIndex call that assigns tableStart contains a duplicated
condition (l.includes('| # |') || l.includes('| # |')); remove the redundant
check so the predicate only tests unique conditions (e.g., a single
l.includes('| # |') or any intended alternate token). Update the expression
where tableStart is computed (the lines.findIndex(...) invocation) to use the
corrected predicate.
- Line 245: The filter currently excludes files starting with the misspelled
prefix 'archieved', so replace that string with the correct 'archived' in the
files assignment (the readdirSync(...).filter(...) expression that populates the
files variable for INTERVIEW_PREP) to ensure archived interview-prep markdown
files are properly skipped.
- Line 147: The property initializer using parseInt ("num: parseInt(num) ||
0,") must pass an explicit radix to avoid ambiguous parsing; update the call to
use a decimal radix (e.g., pass 10) or use Number(...) for clarity so the
expression becomes a parseInt call with radix 10 (or an equivalent decimal
conversion) while preserving the fallback to 0.
- Line 320: The filter using apps = apps.filter(a => (a.date || '') >=
cutoffStr) is fragile because it compares date strings lexically; update the
filter to parse a.date and cutoffStr into Date objects (or timestamps) and
compare numerically instead, handling malformed or missing a.date by treating
them as invalid (e.g., exclude or set to new Date(0)); locate the filter
expression in sync-notion.mjs and replace the string comparison with a numeric
comparison using Date.parse or new Date(...) for a.date and cutoffStr (ensure
you check isNaN on parsed values).
- Line 399: The code calls uploadInterviewPrep(parentPageId || databaseId) but a
databaseId cannot be used as a parent page ID; update the logic so when
syncInterviewPrep is enabled you require a valid parentPageId (do not fall back
to databaseId), and if parentPageId is missing either throw/log a clear error
and skip calling uploadInterviewPrep or fail fast; adjust the check where
uploadInterviewPrep is invoked (referencing uploadInterviewPrep, parentPageId,
databaseId, and syncInterviewPrep) to enforce this validation and avoid passing
a databaseId as the parent.
- Around line 186-200: In buildPageProperties, defensively truncate the company
and role strings (used for Company, Role and the Name title concatenation) to
the same safe maximum as Notes (e.g., 2000 chars) before building props so long
values cannot break Notion rich_text limits; update the Name construction to use
the truncated company/role variables and keep existing behavior for Notes,
Score, Report, Status, Date, Has PDF and #.
- Around line 93-109: The notionFetch function currently calls loadConfig() on
every request which causes repeated file I/O; change notionFetch to accept an
optional cfg parameter (e.g., notionFetch(path, opts = {}, cfg)) and use that
when provided, falling back to loadConfig() only if cfg is missing; then update
callers like main() and runAuth() to call loadConfig() once, store the cfg, and
pass it into their notionFetch calls to eliminate repeated config reloads.
- Around line 230-238: The parseMarkdownToBlocks code builds very long block
objects inline (for heading_1/2/3, bulleted_list_item, paragraph); extract a
small helper (e.g., makeRichTextBlock(type, content) or makeBlock(type,
contentPath)) that returns the Notion block shape with rich_text populated from
a plain string, then replace each long inline construction in
parseMarkdownToBlocks with calls to that helper for
heading_1/heading_2/heading_3, bulleted_list_item, and paragraph to collapse the
long lines and improve readability while preserving the same object keys
(object, type, heading_1/heading_2/heading_3, bulleted_list_item, paragraph, and
their rich_text structure).
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: ASSERTIVE
Plan: Pro
Run ID: db86699e-f6b9-412d-bf03-7bb37224e673
📒 Files selected for processing (1)
sync-notion.mjs
- notionFetch: add module-level config cache (_cachedCfg / getCfg) to eliminate repeated file I/O on every API call; accept optional cfg param - buildPageProperties: truncate company/role to 2000 chars before building rich_text and title properties to respect Notion API limits - parseMarkdownToBlocks: extract makeBlock(type, content) helper to replace long inline block object construction for all five block types - uploadInterviewPrep: require valid parentPageId — warn and skip when sync_interview_prep is enabled but parentPageId is missing; never fall back to databaseId which cannot be used as a page parent
There was a problem hiding this comment.
Actionable comments posted: 5
🤖 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.
Inline comments:
In `@sync-notion.mjs`:
- Line 196: The Name field is truncated in buildPageProperties() but the upsert
lookup uses the raw `${app.company} — ${app.role}`, causing mismatches and
duplicates; create a single title-normalization helper (e.g.,
normalizeNotionTitle(title)) that applies the same trimming/slicing logic
(including the 2000 char limit and any other normalization) and replace both the
Name assignment in buildPageProperties() and the key used for lookup/upsert (the
`${app.company} — ${app.role}` expression) to call normalizeNotionTitle(...) so
both lookup and persistence use the identical normalized title.
- Around line 148-150: The extracted reportUrl (from reportMatch/reportUrl) must
be normalized: if reportUrl is already absolute (starts with "http://" or
"https://") keep it; if it is a repo-relative path like "reports/foo.md" convert
it to the repository's absolute GitHub URL (e.g., construct using the repo
owner/name and branch constants used elsewhere in the module) before storing to
Notion; if you cannot resolve a repo base or the path is not transformable, skip
setting the Report field entirely. Update the extraction block that defines
reportMatch and reportUrl to perform this normalization (detect absolute URLs,
resolve relative paths against the repo base, or omit the field) so Notion only
receives shareable absolute links.
- Around line 254-274: The uploadInterviewPrep loop always creates new pages and
should instead detect existing uploads by a stable source key: before creating a
page in uploadInterviewPrep, call notionFetch to query the parentPageId's
children (or search pages) for a page whose identifying property (e.g., a
"Source" or "SourcePath" property or the normalized title) matches the current
file name; if found, skip creation or update that page's children rather than
POSTing a new page. When creating new pages via notionFetch('/pages', ...)
include a dedicated property (e.g., "Source" with the file name) so subsequent
runs can reliably match files to pages; keep using INTERVIEW_PREP,
parseMarkdownToBlocks and DRY_RUN as currently used and ensure the matching uses
the same title normalization logic (firstLine → title) or the new Source
property.
- Around line 361-365: The early return inside the DRY_RUN branch prevents
loading existing pages and computing a true dry-run plan; remove the immediate
"return" after logging apps so the read-only discovery path still runs (load
existing Notion pages and compute creates vs updates using the same logic that
follows), and instead gate actual write calls (the Notion create/update/upsert
calls) behind DRY_RUN so they are suppressed while still producing the
interview-prep preview and full plan; apply the same change to the other DRY_RUN
guard sites noted (the blocks around the other ranges) and keep checks on
DRY_RUN to avoid performing writes while allowing all read/preview logic to
execute.
- Around line 78-83: The syncInterviewPrep flag currently defaults to true when
the key is missing; change its logic so it is opt-in by setting
syncInterviewPrep only when the source value is explicitly true (e.g., replace
the current n.sync_interview_prep !== false check with a strict check like
n.sync_interview_prep === true). Update the returned object (the
token/parentPageId/databaseId/syncInterviewPrep block) so syncInterviewPrep uses
that explicit check against n.sync_interview_prep to avoid creating
interview-prep pages on first sync.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: ASSERTIVE
Plan: Pro
Run ID: b2468c36-59b9-484b-a6c2-18a7c683e341
📒 Files selected for processing (1)
sync-notion.mjs
- syncInterviewPrep: change default from opt-out (!== false) to opt-in (=== true) so first-time syncs don't unexpectedly upload prep notes - reportUrl: only pass absolute http/https URLs to Notion's url field; skip repo-relative paths like reports/foo.md which Notion would reject - uploadInterviewPrep: add fetchExistingPrepTitles to check for existing child pages before creating — skips files already uploaded on prior runs - DRY_RUN: remove early return; always fetch existing pages when databaseId is known so dry-run shows accurate create vs update breakdown per entry
|
@coderabbitai resolve |
✅ Actions performedComments resolved. Approval is disabled; enable |
Closes #667
What this adds
sync-notion.mjsreadsdata/applications.mdand creates/updates a Notion database so the job search pipeline is visible as a shareable Kanban board — no manual copy-paste, no screenshots.How it works
database_idback toconfig/profile.yml"{Company} — {Role}"title — creates new entries, updates existing onesinterview-prep/*.mdfiles as Notion sub-pages under the same parentDatabase schema created
"{Company} — {Role}"Flags
--dry-run--since N--authSetup
Scope
sync-notion.mjs(408 lines)js-yaml(already inpackage.json) + nativefetchTest plan
--authlists accessible pages and shows IDs--dry-runprints create/update plan without API callsparent_page_idcreates the database and writesdatabase_idto profile--since 7filters to last 7 days onlysync_interview_prep: trueuploads prep .md files as sub-pagestoken/parent_page_idexits with a clear error message🤖 Generated with Claude Code
Summary by CodeRabbit