Skip to content

fix(sync): fail closed on zero recognized Project statuses#31

Merged
thewrz merged 4 commits into
mainfrom
fix/issue-5
Jul 19, 2026
Merged

fix(sync): fail closed on zero recognized Project statuses#31
thewrz merged 4 commits into
mainfrom
fix/issue-5

Conversation

@thewrz

@thewrz thewrz commented Jul 19, 2026

Copy link
Copy Markdown
Contributor

Why

When the GitHub Projects v2 read comes back with items but none of them resolve to a recognized Status value, the sync previously proceeded silently -- treating "we couldn't read the board's Status field" the same as "the board legitimately has no matching lanes." That masks a misconfigured status_field name and can cause cards to be created without the correct lane, with no operator signal that anything went wrong.

What

Sync now fails closed when a project has items but zero of them carry a recognized Status: it logs a distinct WARN naming cfg['gh_project']['status_field'] and skips lane resolution for new cards in that pass (falling back to lane_id=None, an already-supported path in create_card) rather than guessing. The pre-existing outright-failure WARN (project unreadable at all) is left byte-for-byte unchanged since a log consumer may key on it. Also excluded Pull Request rows from Projects v2 item parsing so PR rows never masquerade as issue items during this check.

Design decisions

Implemented per the pre-spike design with no deviations found necessary during the real build (spike was judged unnecessary and skipped, and nothing surfaced during implementation that contradicted it). Confirmed decisions as executed:

  1. zero_status_despite_items is keyed off project_items (parsed, URL-keyed) rather than raw_items, since raw_items can hold draft/PR rows lacking content.url that never populate project_items -- a project with only such rows must not trip the new WARN.
  2. Kept the fix inline in main() rather than extracting a project_read_gate() helper -- ~10 lines across two existing blocks, single call site, no abstraction to justify.
  3. Preserved the pre-existing outright-failure WARN string byte-for-byte (a log consumer may key on it) and added a distinct second WARN naming cfg['gh_project']['status_field'] for the zero-status case, since the two failure modes have different operator remediations.
  4. In the card-creation loop, stage is still computed unconditionally (informational/log-only during a failed read) but lane is only resolved via resolve_lane_for_stage when not project_read_failed; otherwise lane=None and create_card's already-supported lane_id=None path fires (no new deferred-creation queue/state).
  5. No CLAUDE.md exists in this repo (checked, absent) -- relied on the issue spec, existing code conventions (docstring style, WARN/print formatting, Conventional Commits), and the repo's own recent commit history for style.

Testing

  • Unit tests pass (python3 -m pytest -q -- 184 passed)
  • Integration tests pass (covered within the same suite; no separate integration harness in this repo)
  • Manual verification: run sync against a live board with a misconfigured status_field and confirm the new WARN fires and cards fall back to lane_id=None
  • CI green

🤖 Co-authored by Claude Sonnet 5. Closes #5

Summary by CodeRabbit

  • Bug Fixes

    • Pull requests and draft issues are no longer treated as issues in project synchronization, even when they include URLs.
    • Runs now avoid moving cards when project statuses cannot be read or recognized, preventing unintended bulk lane changes.
    • Newly created cards remain unassigned when lane information is unavailable and can be assigned on a later successful run.
  • Documentation

    • Updated run behavior documentation to clarify status-reading and lane-assignment outcomes.

thewrz and others added 3 commits July 18, 2026 21:46
…card lanes too

A configured Project whose item-list read technically succeeds but yields zero
recognized statuses (misspelled GH_PROJECT_STATUS_FIELD, or a gh output-shape
change) previously slipped past the fail-closed gate: project_status came back
{} with move_lanes still True, so every issue fell back to label/PR-derived
stages -- the exact mass-move the gate exists to prevent, just reached through
a technically-successful call. Newly created cards were also unconditionally
laned from that same fallback even when the read had outright failed, since
only the existing-card loop respected project_read_failed.

Widen project_read_failed to also trip when the Project has issue-linked items
but none carry a recognized status, with a WARN naming the configured status
field. Gate the new-card creation loop on the same flag so cards created during
a failed read are laneless rather than fallback-laned. README now documents
both the widened gate and laneless creation.

Closes #5

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…tive guard

Closes review findings for issue #5:
- test_new_card_gets_no_lane_when_project_read_outright_fails now asserts the
  byte-for-byte outright-failure WARN string is actually printed on the
  (None, None) call-failed path, not just that lane assignment is skipped.
- New test_zero_issue_linked_items_does_not_trip_zero_status_warn pins the
  invariant that a Project with zero issue-linked items (project_items == {},
  e.g. all rows are drafts/PRs) must never trip the zero-status WARN and must
  leave move_lanes=True, guarding the `bool(project_items) and` conjunct in
  sync.main() against regression.

Verified both tests fail against the mutations described in the findings
(collapsing the WARN branches; dropping the bool(project_items) conjunct) and
pass against the current, already-correct sync.py -- these were test-coverage
gaps, not production bugs.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
gh project item-list populates content.url for a linked Pull Request just
like it does for an Issue, so parse_items's URL-only check was wrongly
counting PR rows as issue-linked items. That inflated project_items/
project_status in sync.py, corrupting zero_status_despite_items and the
fail-closed lane-move gate it feeds (issue #5's mass-move failure mode,
reachable through a different door). Exclude content.type in
{PullRequest, DraftIssue} explicitly; a missing type key still defaults
to Issue for backward compatibility with fixtures/gh paths that omit it.

Also strengthens test_sync_main.py's project_read_failed lane-gating
tests: they previously passed only because the mocked board_layout
returned [], so resolve_lane_for_stage produced None regardless of
whether the `if not project_read_failed` / `if move_lanes` gates were
present. New tests mock resolve_lane_for_stage to return a real lane and
assert it is never even called when the Project read failed, directly
pinning both gates instead of relying on an empty-lanes coincidence.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Jul 19, 2026

Copy link
Copy Markdown

Review Change Stack

Important

Review skipped

Auto incremental reviews are disabled on this repository.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 9f6c7cd6-4a4f-4674-9ac9-0b6e056052fb

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Projects v2 parsing now excludes non-issue content types. Synchronization distinguishes failed and zero-recognized-status reads, blocks lane changes in those cases, and creates new cards without lanes when assignment is deferred. Tests cover parsing, orchestration, warnings, and card writes.

Changes

Projects v2 synchronization

Layer / File(s) Summary
Projects v2 issue filtering
ghproject.py, tests/test_sync.py
parse_items excludes PullRequest and DraftIssue rows by content.type while preserving rows with missing types.
Fail-closed status gating and card creation
sync.py, README.md
Failed or zero-recognized-status reads block lane changes, warn for zero recognized statuses, and defer lane assignment for new cards; documentation reflects this behavior.
Orchestration regression coverage
tests/test_sync_main.py
Mocks and assertions cover existing-card handling, card creation, warnings, lane resolution, failed reads, zero statuses, and merge-base behavior.

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

Sequence Diagram(s)

sequenceDiagram
  participant sync_main
  participant GitHubProjectsV2
  participant resolve_lane_for_stage
  participant agileplace_create_card
  sync_main->>GitHubProjectsV2: read project items and statuses
  GitHubProjectsV2-->>sync_main: statuses or failed/empty result
  sync_main->>resolve_lane_for_stage: resolve lane only after a usable read
  resolve_lane_for_stage-->>sync_main: lane
  sync_main->>agileplace_create_card: create card with lane or deferred assignment
Loading

Possibly related issues

  • Issue 20 — The changes implement tri-state Projects v2 lane gating and orchestration tests for the requested boundary cases.
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 38.10% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title concisely describes the main behavior change: failing closed when zero Project statuses are recognized.
Linked Issues check ✅ Passed The PR matches issue #5 by failing closed on zero recognized statuses, blocking fallback lanes on failed reads, updating README, and adding tests.
Out of Scope Changes check ✅ Passed The PullRequest/DraftIssue parsing change is aligned with the stated PR objectives, so no clear out-of-scope changes stand out.

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

@thewrz

thewrz commented Jul 19, 2026

Copy link
Copy Markdown
Contributor Author

@coderabbitai full review

@coderabbitai

coderabbitai Bot commented Jul 19, 2026

Copy link
Copy Markdown
✅ Action performed

Full review finished.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 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 `@tests/test_sync_main.py`:
- Line 150: Remove unused mock bindings from the _run_main_once unpacking: in
tests/test_sync_main.py lines 150-150, replace patch_card_mock with _, and at
lines 187-187 replace the unused state and run_mock bindings with _. Keep all
used return values unchanged.
🪄 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: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: bd21e685-78de-471b-8b17-d1b30b4a84db

📥 Commits

Reviewing files that changed from the base of the PR and between 6cb3233 and e6ed5df.

📒 Files selected for processing (5)
  • README.md
  • ghproject.py
  • sync.py
  • tests/test_sync.py
  • tests/test_sync_main.py

Comment thread tests/test_sync_main.py Outdated
CodeRabbit nitpick: drop the never-read patch_card_mock binding in the
first-rollout test and the unused state/run_mock bindings in the
zero-recognized-statuses test (Ruff RUF059).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@thewrz

thewrz commented Jul 19, 2026

Copy link
Copy Markdown
Contributor Author

@coderabbitai full review

@coderabbitai

coderabbitai Bot commented Jul 19, 2026

Copy link
Copy Markdown
✅ Action performed

Full review finished.


You're currently rate limited under our Fair Usage Limits Policy. Your recent PR review activity is in the 95th percentile or higher among CodeRabbit users, so adaptive limits apply. Your next review will be available in 36 minutes.

@thewrz
thewrz marked this pull request as ready for review July 19, 2026 06:39
@thewrz
thewrz merged commit 87fd13f into main Jul 19, 2026
1 check passed
thewrz added a commit that referenced this pull request Jul 19, 2026
…cts)

Both this branch and main (via merged PRs #30/#31) extended the same
test_sync_main.py helpers and appended new test sections. Resolution keeps
this branch's superset _mock_io/_run_main_once signatures (open_pr_return,
lanes_return, card, existing_cards with _UNSET sentinels), main's
underscore-bound unused unpacks (RUF059), and both branches' test sections.
create_card mock returns {} per main's convention so freshly created cards
are not re-processed by the step-2 lane pass in these wiring tests.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
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.

Fail closed when the Projects v2 read yields zero recognized statuses; don't lane new cards from fallback on a failed read

1 participant