Menu

#198 feat(github): replace Issues Search API with Projects v2 GraphQL

closed
nobody
released (243)
2026-06-08
2026-06-08
Anonymous
No

Originally created by: Akarsh-Hegde

Summary

  • Replaces the REST /search/issues endpoint with the GitHub Projects v2 GraphQL API
  • Users pick their GitHub Projects (personal + org) during meridian setup — project node IDs stored as GITHUB_PROJECT_IDS
  • GITHUB_TOKEN is now the only required env var; GITHUB_ORG and GITHUB_REPOS removed
  • Project Status field (Todo/In Progress/Done) maps to status_category so tasks show the right state in the dashboard

No-PAT auth via the gh CLI

_try_gh_token pulls the token straight from the gh CLI and grants any missing scopes through gh's browser flow — no personal access token needed. It falls back to a manual PAT prompt only when gh is unavailable. _pick_github_projects then lists personal + org projects to choose from.

A prior revision referenced _try_gh_token but never defined it, so setup fell through to the PAT prompt. It's now defined in both install.sh (source installs) and scripts/install-from-bundle.sh (bundle installs).

Required scopes: repo, read:org, read:project

Corrected from the over-provisioned project (write) to read:project. meridian only reads Projects v2 via GraphQL and posts worklog / task-update issue comments under repo — it never mutates a project board. Verified three ways: GitHub's own INSUFFICIENT_SCOPES response, the GraphQL docs, and a codebase grep (the only mutation is Linear's commentCreate).

Verified end-to-end

Daemon synced with a browser-issued gho_ OAuth token (no PAT): github tasks refreshed upserted_count=3 — the 3 viewer-assigned issues, with the Project Status column correctly mapping #194 → in_progress.

⚠️ Testing on another machine

Merging this is necessary but not sufficient for meridian setup to use the new flow:

  • npm/bundle install (curl … bootstrap.sh | bash): meridian setup is owned by npm's meridian.js, which re-extracts the published release bundle — it will keep showing the old GITHUB_ORG prompt until a new release is cut.
  • To test the branch now: install from source — git clone && ./install.sh (or check out this branch and re-run ./install.sh).

    :::bash
    meridian setup # gh token auto-extracted, project picker shown

    or manually, in the .env the daemon reads:

    GITHUB_TOKEN=gho_… (or ghp_…)
    GITHUB_PROJECT_IDS=PVT_kwDOEMqA_c4BZ-xP

The daemon reads the .env in its WorkingDirectory: ~/.meridian/app/.env for bundle installs, the repo .env for source installs. Write the config to the right one.

Test plan

  • [x] No-PAT gh browser flow grants read:project and writes GITHUB_TOKEN
  • [x] Project picker lists org projects (Meridiona / Meridian project)
  • [x] Daemon sync with gho_ token → upserted_count=3
  • [x] Project Status maps correctly (#194 → in_progress)
  • [ ] Fresh-machine meridian setup (source install) end-to-end
  • [ ] Tasks appear in the FleetView Tasks panel

🤖 Generated with Claude Code

Related

Tickets: #200

Discussion

  • Anonymous

    Anonymous - 2026-06-08

    Originally posted by: adityaharishch

    Code Review: 8 Findings

    🔴 Critical Issues

    1. IssueContent deserialization fails on non-Issue items

    • File: src/intelligence/providers/github.rs:1761
    • Problem: GitHub GraphQL returns empty {} for non-Issue project items (PRs, drafts). The IssueContent struct has all required fields without #[serde(default)], causing deserialization to fail.
    • Impact: Any Projects v2 board with mixed content (almost all real boards) will skip that project entirely.
    • Fix: Make non-required fields optional or filter items before deserialization.

    2. Partial project fetch success causes task deletion

    • File: src/intelligence/providers/github.rs:1908
    • Problem: When syncing multiple projects, if Project A succeeds and Project B fails, prune() is called with only A's task keys, deleting B's previously-synced tasks.
    • Impact: Transient API failures (500s, rate-limits) delete unrelated project tasks until recovery (up to 5 min).
    • Fix: Only call prune() when all projects succeed, or track which projects failed to exclude their tasks.

    🟡 Maintenance Concerns

    3. Bash function duplication (install.sh:106 & 139)

    • _try_gh_token() and _pick_github_projects() duplicated verbatim in scripts/install-from-bundle.sh
    • Author's own comment: "keep both copies in sync"—but no consolidation. Any bug fix requires dual edits.

    4. Title-stamping logic diverges (segment.rs:720 vs indexer.rs:532)

    • indexer.rs trims and filters empty; segment.rs caps without guards
    • Will diverge on future updates to TITLE_CAP or filtering logic

    🟠 Efficiency Issues

    5. Redundant JSON deserialization (install.sh:161)

    • GraphQL response parsed twice: once for project IDs, once for labels
    • Single combined python call could extract both in one pass

    6. Sequential project fetches (github.rs:1873)

    • Loop through project_ids serially; could use tokio::join_all() for parallelism
    • Marginal impact for typical 1-3 projects

    7. Window recalculated every drain tick (summariser/mod.rs:1255)

    • Yesterday + today recomputed every loop iteration despite fixed batch size
    • Move calculation outside loop

    8. In-memory attempt ledger resets on restart (summariser/mod.rs:1213)

    • Dead-letter cap can be bypassed if daemon crashes
    • Author acknowledges as intentional trade-off

    Recommendation: Address the two critical GitHub issues (#1, [#2]) before merge. Duplication and efficiency findings are lower priority.

     

    Related

    Tickets: #2

  • Anonymous

    Anonymous - 2026-06-08

    Originally posted by: Akarsh-Hegde

    Thanks for the review. Both critical findings are fixed in b5df2c0.

    🔴 Critical — fixed

    1. Non-Issue items broke deserialisation
    content is now Option<serde_json::Value>, parsed into IssueContent per ISSUE item — a PR/draft {} (or redacted null) is skipped instead of failing the whole project's parse. Added a mixed_content_project_deserialises regression test (Issue + PR {} + draft null).

    2. Partial fetch deleted unrelated tasks
    Added an all_ok guard: prune() (and the empty-keys full-clear) now run only when every project fetched successfully. A partial failure upserts what it got and preserves the rest until the next clean sync.

    Verified: builds, clippy clean, 212 tests pass.

    Lower-priority — disposition

    • #3 Bash duplication — documented with "keep in sync" comments; consolidating into a shared sourced lib is a larger refactor, deferred.
    • #5 Redundant JSON parse / [#6] Sequential fetches — marginal for the typical 1–3 projects; deferred as ranked.
    • #4 Title-stamp divergence, [#7] drain-window recompute, [#8] in-memory ledger — these live in main's coding-agent code and entered this branch via the main merge; they aren't changes this PR introduces, so they're out of scope here (and [#8] is the acknowledged intentional trade-off). Happy to file a follow-up if you'd like them tracked.
     

    Related

    Tickets: #6
    Tickets: #7
    Tickets: #8

  • Anonymous

    Anonymous - 2026-06-08

    Originally posted by: Akarsh-Hegde

    Update — all remaining review items handled.

    Done on this PR (440cdd8)

    • #3 Bash duplication — extracted _try_gh_token + _pick_github_projects into scripts/lib-github-setup.sh, sourced by both install.sh and scripts/install-from-bundle.sh, and added to package-release.sh's bundle cp-list so fresh installs ship it. Obsolete "keep in sync" comments removed.
    • #5 Redundant JSON parse_pick_github_projects now parses the GraphQL response in a single python3 pass (id<TAB>label per line) instead of twice.
    • #6 Sequential fetches — projects are fetched concurrently via futures::join_all (already a direct dep), preserving the any_ok/all_ok semantics.

    Split to [#200] (it's main's code, not this PR's)

    Evaluated, intentionally left as-is

    • #7 drain-window recomputedays is computed once per drain() call (every sweep), not per row, and is Utc::now()-based on purpose: hoisting it out of the loop freezes it at the daemon's startup date and breaks the midnight rollover the window exists to handle. The suggestion would be a regression. Cost today: two format() calls per ~10-min tick.
    • #8 in-memory ledger — rows that actually hit MAX_ROW_ATTEMPTS are persisted via db::write_dead_letter (task_method='subprocess_error', excluded from fetch_pending across restarts). Only partial 1–2 counts reset on restart, which is the documented "restart retries cleanly" intent — so the cap isn't bypassable for its purpose. Happy to DB-persist the attempt counter too if you'd prefer that across crashes — just say the word.
     

    Related

    Tickets: #200

  • Anonymous

    Anonymous - 2026-06-08

    Ticket changed by: Akarsh-Hegde

    • status: open --> closed
     
  • Anonymous

    Anonymous - 2026-06-08

    Originally posted by: adityaharishch

    🎉 This PR is included in version 1.32.0 🎉

    The release is available on:

    Your semantic-release bot 📦🚀

     

Log in to post a comment.