Skip to content

test: verify Q.A live metadata and cleanup after bootstrap - #65

Open
v-Kaefer wants to merge 35 commits into
mainfrom
Q.A
Open

test: verify Q.A live metadata and cleanup after bootstrap#65
v-Kaefer wants to merge 35 commits into
mainfrom
Q.A

Conversation

@v-Kaefer

@v-Kaeferv-Kaefer commented Aug 11, 2026

Copy link
Copy Markdown
Owner

Linked Issue

Milestone

  • Mock Milestone

Related PRs

Summary

  • Final end-to-end smoke of the post-bootstrap governance pipeline from Q.A to main.
  • Verify inherited Related PR context, Guardrails, Promotion Sync, structured Live QA metadata, Project v2, and deployment cleanup.

How to test

  • Test type: live automated smoke
  • Steps: observe promotion Autofill and Guardrails, then require Live QA and cleanup to complete successfully.

Evidence

Known risks

  • None; disposable smoke marker only.

DoD checklist

  • Scope implemented as defined
  • Tests executed and documented
  • Evidence attached when applicable
  • No known critical breakage introduced

Summary by CodeRabbit

  • New Features

    • Added native Development linked-branch support for implementation pull requests.
    • Added automatic Project v2 membership and status synchronization for tasks, implementation pull requests, and promotion pull requests.
    • Added promotion synchronization for related metadata, including labels, assignees, milestones, backlinks, and status updates.
    • Added project discovery by configured number or title.
    • Added dry-run support for project and linked-branch operations.
  • Documentation

    • Updated governance and synchronization guidance, including lifecycle rules, security requirements, and live validation procedures.
  • Tests

    • Expanded live QA coverage for project membership, promotion synchronization, and linked branches.

@github-actions

github-actionsBot commented Aug 11, 2026

Copy link
Copy Markdown

@coderabbitai

coderabbitaiBot commented Aug 11, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

The PR adds Linked Branch creation, Project v2 project-item synchronization, promotion metadata synchronization, router integration, live QA coverage, cleanup handling, and updated governance documentation.

Changes

Governance and PR Synchronization

Layer / File(s)Summary
Governance and synchronization contracts
docs/repo/pr-governance-architecture.md, docs/repo/pr-sync*.md
The documentation defines implementation and promotion flows, live-state reads, Linked Branch relationships, Project v2 lifecycle rules, authentication boundaries, and live regression checks.
Linked Branch creation and validation
project_setup/linked_branch.py, tests/test_linked_branch.py, tests/qa/live_linked_branch.py
The new API and CLI create native Linked Branch relationships. Unit and live tests validate inputs, dry-run behavior, linkage, and cleanup.
Project v2 lookup and status synchronization
project_setup/project_lookup.py, project_setup/project.py, project_setup/pr_project_sync.py, tests/test_pr_project_sync.py, tests/test_project_field_reconciliation.py
Project lookup, single-select reconciliation, pull-request membership, and status updates now support explicit and title-based project resolution.
Promotion synchronization and routing
project_setup/promotion_sync.py, project_setup/pr_sync_router.py, tests/test_promotion_sync.py
Promotion Sync aggregates metadata, updates native fields and Project v2 status, manages backlinks, and receives project context through the router.
Live QA coverage and resource lifecycle
.github/workflows/qa-live.yml, tests/qa/live_*.py, tests/fixtures/structured-pr-sync-smoke-62.txt
The live workflow runs implementation, promotion, Linked Branch, and Project v2 checks. Tests poll for convergence and clean up temporary resources.

Estimated code review effort: 5 (Critical) | ~120 minutes

Possibly related issues

Possibly related PRs

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check nameStatusExplanationResolution
Docstring Coverage⚠️ WarningDocstring coverage is 0.00% 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 nameStatusExplanation
Title check✅ PassedThe title clearly identifies the live Q.A. metadata and cleanup verification, which is a primary part of the pull request.
Description check✅ PassedThe description includes all required sections and documents scope, testing, evidence, risks, and completion status.
Linked Issues check✅ PassedThe changes implement the linked issues for structured PR Sync, Project v2 option reconciliation, and bounded live readback polling.
Out of Scope Changes check✅ PassedThe implementation, documentation, and tests support the linked governance, Project v2, promotion sync, and live QA objectives.
✨ Finishing Touches 💡 1
📝 Generate docstrings 💡
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch Q.A

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.

❤️ Share

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

@v-Kaeferv-Kaefer mentioned this pull request Aug 11, 2026
7 tasks
fix: promote Project v2 Status reconciliation to Q.A
fix: promote Project v2 readback tolerance to Q.A

@coderabbitaicoderabbitaiBot 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: 10

🧹 Nitpick comments (12)
tests/test_pr_project_sync.py (3)

94-100: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

make_config leaks a temporary directory on every call.

tempfile.mkdtemp() creates a directory that nothing removes. Three tests call make_config, so each run leaves three directories behind. Use tempfile.TemporaryDirectory and register its cleanup with the test case.

♻️ Proposed refactor
 def make_config(self) -> str:
- directory = Path(tempfile.mkdtemp())+ temp = tempfile.TemporaryDirectory()+ self.addCleanup(temp.cleanup)+ directory = Path(temp.name)
definition = directory / "project.json"
🤖 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 `@tests/test_pr_project_sync.py` around lines 94 - 100, Update make_config to
create the temporary directory with tempfile.TemporaryDirectory and register its
cleanup with the test case, preserving the generated project.json and
project_setup.json paths while ensuring each directory is removed after the
test.

125-136: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Add a test for the ambiguous-title branch.

resolve_project_number raises RuntimeError when several boards share the configured title, at project_setup/project_lookup.py Lines 87-90. That branch refuses to guess, which is a stated objective of this PR, and no test covers it.

💚 Proposed additional test
+ def test_duplicate_project_titles_refuse_to_guess(self):+ client = Mock(spec=GitHubClient)+ with patch(+ "project_setup.project_lookup.list_owner_projects",+ return_value=[+ {"number": 6, "title": "Project Delivery Board"},+ {"number": 9, "title": "Project Delivery Board"},+ ],+ ):+ with self.assertRaises(RuntimeError) as caught:+ resolve_project_number(+ client,+ "owner/repo",+ None,+ config_path=self.make_config(),+ owner="owner",+ )+ self.assertIn("`#6`", str(caught.exception))+ self.assertIn("`#9`", str(caught.exception))+ self.assertIn("PROJECT_SETUP_PROJECT_NUMBER", str(caught.exception))
🤖 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 `@tests/test_pr_project_sync.py` around lines 125 - 136, Add a test alongside
test_missing_project_is_reported_without_guessing that configures
list_owner_projects to return multiple projects with the same configured title,
then assert resolve_project_number raises RuntimeError. Use the existing client,
config_path, owner, and repository setup, and verify the ambiguous-title path
refuses to select a project.

89-90: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Assert that the status update targets the existing item id.

The test confirms add_issue_to_project is skipped. It does not confirm that update_single_select receives existing-item. A regression that reused a stale or synthetic item id would still pass.

💚 Proposed additional assertion
 add_item.assert_not_called()
- update_status.assert_called_once()+ update_status.assert_called_once_with(+ client,+ "project-id",+ "existing-item",+ "status-field",+ "review-option",+ )
🤖 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 `@tests/test_pr_project_sync.py` around lines 89 - 90, Strengthen the test
around the existing-item path by asserting that update_single_select is called
with the existing item id, “existing-item”. Keep the add_issue_to_project
not-called assertion and existing update_status assertion unchanged.
tests/test_project_field_reconciliation.py (2)

46-48: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Assert that the new option carries no id.

The test verifies id reuse for In progress and Done. It does not verify that In review, which does not exist on the board, is sent without an id. Sending a stale or wrong id for a new option would corrupt the reconciliation, and the current assertions would not detect it.

💚 Proposed additional assertion
 self.assertEqual(variables["options"][0]["id"], "INPROGRESS-ID")
+ self.assertNotIn("id", variables["options"][1])
self.assertEqual(variables["options"][2]["id"], "DONE-ID")
🤖 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 `@tests/test_project_field_reconciliation.py` around lines 46 - 48, Extend the
assertions in the reconciliation test around variables["options"] to verify that
the newly created “In review” option has no id. Keep the existing name and
id-reuse assertions for “In progress” and “Done” unchanged.

105-108: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Assert that ensure_fields skips the second field listing.

The changed flag added at project_setup/project.py Line 214 exists to avoid a second list_project_fields call when nothing changed. This test uses return_value, so a redundant second call returns the same list and both assertions still pass. The optimization is therefore untested.

💚 Proposed additional assertion
 update_field.assert_not_called()
+ self.assertEqual(list_fields.call_count, 1)
self.assertEqual(result["Status"], current)
🤖 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 `@tests/test_project_field_reconciliation.py` around lines 105 - 108, Update
the test around ensure_fields to assert that list_project_fields is called only
once when no fields change, using a call-count assertion on its mock. Keep the
existing update_field and result assertions unchanged.
tests/qa/live_linked_branch.py (1)

125-132: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Assert the non-default base against the created pull request, and move the precondition earlier.

base_branch is built from BRANCH_PREFIX at Line 81, so base_branch == default_branch can never be true. The check at Line 125 therefore never fails, and Line 132 prints development_non_default_pr=passed without verifying the base ref that GitHub actually recorded on the pull request. The check also runs after the branch, issue, commit, and pull request already exist, so a real violation would waste the whole setup.

Assert on the base.ref value in the pr response, and keep a cheap precondition next to the branch-name construction.

♻️ Proposed refactor
 pr_number = int(pr["number"])
- if base_branch == default_branch:- raise RuntimeError("Development linkage smoke must target a non-default base branch")+ pr_base_ref = str(((pr.get("base") or {}).get("ref")) or "")+ if pr_base_ref != base_branch or pr_base_ref == default_branch:+ raise RuntimeError(+ f"Development linkage smoke must target non-default base `{base_branch}`, got `{pr_base_ref}`"+ )
if not _wait_for_development_link(client, args.repo, issue_number, pr_number):

Add the precondition where the names are built:

 base_branch = f"{BRANCH_PREFIX}base-{suffix}"
head_branch = f"{BRANCH_PREFIX}head-{suffix}"
+ if base_branch == default_branch:+ raise RuntimeError("Development linkage smoke must target a non-default base branch")
🤖 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 `@tests/qa/live_linked_branch.py` around lines 125 - 132, Move the
non-default-base precondition next to the BRANCH_PREFIX-derived base_branch
construction, validating it against default_branch before creating the branch,
issue, commit, or pull request. Replace the ineffective base_branch check near
the linkage wait with an assertion that the created pr response’s base.ref
differs from default_branch, and retain development_non_default_pr=passed only
after that validation.
project_setup/project.py (1)

217-229: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Report an existing field whose type does not match the definition.

The condition at Lines 218-222 requires field.get("type") == "single_select" and existing_field.get("__typename") == "ProjectV2SingleSelectField". When a board already has a field with the same name but a different type, both branches are skipped and continue at Line 229 hides the mismatch. Nothing is created, nothing is updated, and no message is printed.

The failure surfaces later and further away. sync_pull_request_project_status in project_setup/pr_project_sync.py finds the field at Line 66, then fails at Line 73 with "Project status option ... was not found", which does not name the real cause.

Print a warning when the types disagree.

♻️ Proposed refactor
 existing_field = existing.get(field["name"])
if existing_field:
- if (- field.get("type") == "single_select"- and existing_field.get("__typename") == "ProjectV2SingleSelectField"- and not single_select_options_match(existing_field, field)- ):- if dry_run:- print(f"[DRY-RUN] Would update field options: {field['name']}")- else:- update_single_select_field(client, existing_field, field)- changed = True- print(f"updated field options: {field['name']}")+ if field.get("type") == "single_select":+ if existing_field.get("__typename") != "ProjectV2SingleSelectField":+ print(+ f"warning: field `{field['name']}` exists as "+ f"{existing_field.get('__typename')}, not a single-select field; "+ "skipping option reconciliation"+ )+ elif not single_select_options_match(existing_field, field):+ if dry_run:+ print(f"[DRY-RUN] Would update field options: {field['name']}")+ else:+ update_single_select_field(client, existing_field, field)+ changed = True+ print(f"updated field options: {field['name']}")
continue
🤖 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 `@project_setup/project.py` around lines 217 - 229, Update the existing-field
handling in project.py to detect when the definition’s type and existing_field’s
__typename represent different field types, before the current single-select
update condition. Print a clear warning naming the field and type mismatch, then
preserve the continue behavior without creating or updating the incompatible
field.
project_setup/pr_project_sync.py (1)

10-40: 🚀 Performance & Scalability | 🔵 Trivial | 🏗️ Heavy lift

Resolve the project item from the pull request instead of listing every board item.

list_project_content_items pages through all items on the board, and sync_pull_request_project_status calls it at Line 82 on every pull-request event. The cost grows with board size, not with the work performed. A board with several thousand items produces tens of sequential GraphQL calls to locate one node id, on a webhook-driven path.

Query the pull request for its own project items, then select the entry that belongs to the target project.

♻️ Proposed refactor
+def project_item_for_content(client: GitHubClient, project_id: str, content_node_id: str) -> str | None:+ query = """+ query($content:ID!, $cursor:String) {+ node(id:$content) {+ ... on PullRequest {+ projectItems(first:100, after:$cursor, includeArchived:true) {+ pageInfo { hasNextPage endCursor }+ nodes { id project { id } }+ }+ }+ }+ }+ """+ cursor = None+ while True:+ page = client.graphql(query, {"content": content_node_id, "cursor": cursor})["node"]["projectItems"]+ for item in page["nodes"]:+ if str((item.get("project") or {}).get("id") or "") == project_id:+ return str(item["id"])+ if not page["pageInfo"]["hasNextPage"]:+ return None+ cursor = page["pageInfo"]["endCursor"]

Then replace the lookup:

- current_items = list_project_content_items(project_client, str(project["id"]))- item_id = current_items.get(pr_node)+ item_id = project_item_for_content(project_client, str(project["id"]), pr_node)

Keep list_project_content_items if other callers depend on the full map. tests/test_pr_project_sync.py patches list_project_content_items at Lines 40 and 75, so those patch targets change with this refactor.

Also applies to: 82-83

🤖 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 `@project_setup/pr_project_sync.py` around lines 10 - 40, Update
sync_pull_request_project_status to resolve the pull request’s project item
directly by querying the PR’s associated project items and selecting the entry
matching project_id, instead of calling list_project_content_items. Keep
list_project_content_items unchanged for other callers, and update the affected
tests to patch the new lookup symbol.
tests/qa/live_promotion_sync.py (1)

215-221: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Log the suppressed branch-deletion error.

The stale sweep discards every exception from delete_branch. A persistent permission or protection failure then stays invisible, and the sandbox keeps the stale branch. Print a warning instead of pass.

♻️ Proposed fix
 try:
delete_branch(client, repo, name.removeprefix("refs/heads/"))
- except Exception:- pass+ except Exception as exc: # noqa: BLE001 - stale sweep must not abort the run+ print(f"warning: stale branch `{name}` was not deleted: {exc}")
🤖 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 `@tests/qa/live_promotion_sync.py` around lines 215 - 221, Update the exception
handler around delete_branch in the stale sweep loop to print a warning
containing the branch name and exception details instead of silently passing;
preserve the existing cleanup flow and continue processing remaining refs.

Source: Linters/SAST tools

tests/qa/live_implementation_project.py (2)

129-151: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Add stale-resource cleanup before the run.

tests/qa/live_pr_sync.py and tests/qa/live_promotion_sync.py call cleanup_stale_resources before they create resources. This script does not. The suffix comes from the run identifier, so each aborted run leaves a project, an issue, branches, and an open PR in the sandbox. These resources accumulate over time.

Add an equivalent pre-run sweep that matches PREFIX and BRANCH_PREFIX.

🤖 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 `@tests/qa/live_implementation_project.py` around lines 129 - 151, The main
flow in main must perform a cleanup_stale_resources sweep immediately after
resolving the GitHub owner and before creating the project, issue, branches, or
PR. Match stale resources using the existing PREFIX and BRANCH_PREFIX values,
reusing the cleanup pattern from the other live QA scripts.

102-110: 🩺 Stability & Availability | 🔵 Trivial | 💤 Low value

Consider a longer or configurable Project v2 read-back timeout.

TIMEOUT_SECONDS is 20.0, and this poll waits for two items to converge. Project v2 read-after-write latency can exceed that bound under load, which makes the live gate flaky. Make the timeout an argument or an environment override.

🤖 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 `@tests/qa/live_implementation_project.py` around lines 102 - 110, Update
wait_for_items to use a configurable timeout rather than relying solely on the
fixed TIMEOUT_SECONDS value. Accept a timeout argument or read an environment
override, while preserving the existing polling and return behavior.
project_setup/pr_sync_router.py (1)

114-126: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Report the reason when auto-discovery is not possible.

If project_client is None and no explicit number exists, the code prints nothing. The log then gives no reason for the skipped Project v2 synchronization. Print the resolution message in that branch too.

🤖 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 `@project_setup/pr_sync_router.py` around lines 114 - 126, Update the
conditional reporting after resolve_project_number so the project_resolution
message is also printed when project_client is None and no explicit project
number is available. Preserve the existing messages for resolved projects and
skipped auto-discovery when project_client is present.
🔇 Additional comments (38)
docs/repo/pr-governance-architecture.md (8)

7-69: LGTM!


71-106: LGTM!


108-120: LGTM!


122-136: LGTM!


163-167: LGTM!


121-121: 🗄️ Data Integrity & Integration | 🔵 Trivial | ⚡ Quick win

⚠️Unverified finding
Sandbox verification was unavailable.

Use one branch-pattern replacement contract across all governance documents.

The final documents still describe the default branch-pattern list as fully replaceable, while the supplied change details say that behavior was removed. Confirm the implementation before merge.

  • docs/repo/pr-governance-architecture.md#L121-L121: state whether configuration replaces or extends the default patterns.
  • docs/repo/pr-sync.md#L109-L111: remove or correct the “fully replaceable” claim.
  • docs/repo/pr-sync.md#L127-L127: keep the configuration section consistent with the documented behavior.
  • docs/repo/pr-sync.pt-BR.md#L109-L111: mirror the corrected replacement semantics.
  • docs/repo/pr-sync.pt-BR.md#L126-L127: keep the Portuguese configuration section consistent.
  • docs/repo/pr-governance-architecture.pt-BR.md#L121-L121: mirror the corrected architecture contract.

Verify the implementation with:


137-162: 🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

⚠️Unverified finding
Sandbox verification was unavailable.

Document the Project v2 single-select reconciliation contract in both locales.

The documents define status names and project lookup, but not option matching, existing option-ID preservation, handling of missing or extra options, or idempotency. This omission leaves existing Project v2 boards underspecified.

  • docs/repo/pr-governance-architecture.md#L137-L162: add the complete option-reconciliation contract.
  • docs/repo/pr-sync.md#L95-L105: document the same rules in the PR Sync contract.
  • docs/repo/pr-sync.pt-BR.md#L95-L105: translate the same rules without changing semantics.
  • docs/repo/pr-governance-architecture.pt-BR.md#L137-L161: mirror the English architecture contract.

Verify the implementation and tests with:


169-200: 🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

⚠️Unverified finding
Sandbox verification was unavailable.

Document bounded Project v2 convergence and cleanup on every failure path.

Live validation must poll until the task and In review status are visible, fail after a bounded timeout when they do not converge, and preserve cleanup for polling, assertion, and workflow failures.

  • docs/repo/pr-governance-architecture.md#L169-L200: add the convergence and cleanup guarantees to the regression contract.
  • docs/repo/pr-sync.md#L175-L188: add the same guarantees to live validation.
  • docs/repo/pr-sync.pt-BR.md#L175-L188: translate the same guarantees.
  • docs/repo/pr-governance-architecture.pt-BR.md#L169-L200: mirror the English regression contract.

Verify the live workflow and QA helpers with:

docs/repo/pr-sync.md (4)

7-94: LGTM!


107-108: LGTM!


113-125: LGTM!


135-136: LGTM!

docs/repo/pr-sync.pt-BR.md (4)

7-94: LGTM!


107-108: LGTM!


113-125: LGTM!


135-136: LGTM!

docs/repo/pr-governance-architecture.pt-BR.md (5)

7-69: LGTM!


71-106: LGTM!


108-120: LGTM!


122-136: LGTM!


163-167: LGTM!

project_setup/linked_branch.py (2)

20-76: LGTM!

Also applies to: 79-99, 102-134


11-17: 🩺 Stability & Availability | ⚡ Quick win

Two helpers percent-encode branch names with opposite safe settings._branch_oid uses safe="", which turns a slash into %2F. _ref_path uses safe="/", which keeps the slash literal. The live test passes the slash-containing name qa/development/base-<suffix> through both paths, so one of the two encodings is wrong for its endpoint.

  • project_setup/linked_branch.py#L11-L17: confirm the /repos/{repo}/branches/{branch} endpoint resolves %2F in the path segment. If it does not, change to urllib.parse.quote(base_ref, safe="/") to match _ref_path.
  • tests/qa/live_linked_branch.py#L20-L38: keep safe="/" for /git/ref/{ref}, which requires the hierarchical form. Add a short comment naming the endpoint requirement so the difference from _branch_oid is not read as an oversight.
tests/test_linked_branch.py (1)

11-45: LGTM!

Also applies to: 47-60, 62-76

project_setup/project_lookup.py (2)

34-44: 🔒 Security & Privacy

⚠️Unverified finding
Sandbox verification was unavailable.

Confirm that resolve_owner_type returns only a closed set of GraphQL root fields.

resolved_type is interpolated directly into the query text at Line 37. The query is safe only when resolve_owner_type returns exactly organization or user. If it can return a caller-supplied value, such as the owner_type argument passed through from Line 32, an attacker-controlled string reaches the GraphQL document.

Run the following script to inspect the return contract and the caller paths:


12-25: LGTM!

Also applies to: 45-55, 58-90

project_setup/project.py (1)

206-209: LGTM!

Also applies to: 212-216, 230-236

project_setup/pr_project_sync.py (1)

43-80: LGTM!

Also applies to: 84-90

tests/test_pr_project_sync.py (1)

16-63: LGTM!

Also applies to: 109-123

tests/test_project_field_reconciliation.py (1)

28-45: LGTM!

Also applies to: 50-78, 84-104

project_setup/promotion_sync.py (2)

112-125: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

⚠️Unverified finding
Sandbox verification was unavailable.

Confirm the label comparison type. The current expression may force a label write on every run.

existing comes from issue_label_names(pr_issue). target is a sorted list. Line 117 compares set(target) != existing. If issue_label_names returns a list, the comparison between a set and a list is always unequal. The code then sends a PUT .../labels request on every invocation, even when the labels already match.

🐛 Proposed fix
- existing = issue_label_names(pr_issue)+ existing = set(issue_label_names(pr_issue))
unmanaged = sorted(name for name in existing if not name.startswith(prefixes))
target = sorted(set(unmanaged + metadata.labels))
if set(target) != existing:

42-97: LGTM!

Also applies to: 259-283, 286-366

project_setup/pr_sync_router.py (1)

9-21: LGTM!

Also applies to: 46-62, 72-90, 128-137

tests/test_promotion_sync.py (1)

20-68: LGTM!

Also applies to: 71-195, 198-244

.github/workflows/qa-live.yml (1)

26-26: LGTM!

tests/fixtures/structured-pr-sync-smoke-62.txt (1)

1-2: LGTM!

tests/qa/live_pr_sync.py (1)

10-10: LGTM!

Also applies to: 24-25, 154-176, 385-396

tests/qa/live_promotion_sync.py (1)

30-40: LGTM!

Also applies to: 101-166, 168-184, 186-214, 224-278, 281-560, 563-616

🤖 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 @.github/workflows/qa-live.yml:
- Around line 84-109: Update all three live QA steps—“Run live implementation PR
Project membership test,” “Run live Promotion Sync native metadata test,” and
“Run live Development linked-branch test”—to expose inputs.pr_number through the
step env as a dedicated variable, then reference that environment variable in
each --run-id argument instead of expanding the GitHub template directly in the
shell command.
In `@project_setup/pr_project_sync.py`:
- Around line 91-104: Update the return message in the function containing the
dry_run branch so dry-run execution reports that the PR would be synced rather
than claiming the sync completed. Preserve the existing completed-sync message
for the non-dry-run path, using the existing desired_status and project_number
context.
In `@project_setup/pr_sync_router.py`:
- Around line 63-70: Update the guard in apply_pr_sync after the apply_pr_sync
result check to also return when sync_config["enabled"] is false, before any
Project v2 operations occur. Preserve the existing syncProject, project_number,
and project_client checks.
In `@project_setup/project.py`:
- Around line 173-186: Update single_select_option_inputs to preserve color and
description for matched options, and explicitly retain or remove options omitted
from the desired definition; if retaining them, update list_project_fields to
fetch the required fields. In tests/test_project_field_reconciliation.py lines
10-26, update the expected list for the chosen contract: include Todo with
TODO-ID when retaining omitted options, or document the intentional removal
contract when deletion remains.
- Around line 189-203: Update update_single_select_field and
single_select_option_inputs so the mutation submits the complete option list:
retain existing options absent from the desired configuration, including their
IDs, and preserve each option’s existing color and description unless explicitly
configured otherwise. Ensure configured options are merged with existing
metadata rather than defaulting preserved values to GRAY or an empty
description.
In `@project_setup/promotion_sync.py`:
- Around line 163-256: Extract the shared Project v2 item-listing and
status-sync flow from list_project_content_items and
sync_promotion_project_status, reusing it from both this module and
pr_project_sync. Add a label parameter for the differing log and return-message
prefixes, while preserving each caller’s existing behavior and dry-run handling.
In `@tests/qa/live_implementation_project.py`:
- Around line 160-163: Update the project lookup in the live implementation flow
to use a named project_by_title result and explicitly raise a RuntimeError when
no project matching project_title is found. Include clear Project v2 lookup
context in the error, while preserving the existing project_id, project_number,
and ensure_fields handling for successful lookups.
In `@tests/qa/live_promotion_sync.py`:
- Around line 42-99: The shared root cause is duplicated Project v2 and Git ref
helpers across the live QA scripts. In tests/qa/live_promotion_sync.py lines
42-99, move list_projects, project_by_title, delete_project, encoded_ref,
branch_sha, create_branch, and delete_branch into a shared tests/qa helper
module and import them; in tests/qa/live_implementation_project.py lines 23-49,
remove the local list_projects, delete_project, ref_path, create_branch,
delete_branch, and branch_sha implementations and import the shared helpers
instead.
In `@tests/test_pr_project_sync.py`:
- Around line 102-107: Update
test_explicit_project_number_wins_without_remote_lookup to verify that the
mocked client recorded no method calls, rather than asserting the client object
itself was not called. Keep the existing explicit number and note assertions
unchanged.
In `@tests/test_project_field_reconciliation.py`:
- Around line 79-82: Update the test around ensure_fields by assigning the
Mock() client to a named variable before passing it to ensure_fields, then
assert update_field was called with that variable as its first argument instead
of reading update_field.call_args.args[0].
---
Nitpick comments:
In `@project_setup/pr_project_sync.py`:
- Around line 10-40: Update sync_pull_request_project_status to resolve the pull
request’s project item directly by querying the PR’s associated project items
and selecting the entry matching project_id, instead of calling
list_project_content_items. Keep list_project_content_items unchanged for other
callers, and update the affected tests to patch the new lookup symbol.
In `@project_setup/pr_sync_router.py`:
- Around line 114-126: Update the conditional reporting after
resolve_project_number so the project_resolution message is also printed when
project_client is None and no explicit project number is available. Preserve the
existing messages for resolved projects and skipped auto-discovery when
project_client is present.
In `@project_setup/project.py`:
- Around line 217-229: Update the existing-field handling in project.py to
detect when the definition’s type and existing_field’s __typename represent
different field types, before the current single-select update condition. Print
a clear warning naming the field and type mismatch, then preserve the continue
behavior without creating or updating the incompatible field.
In `@tests/qa/live_implementation_project.py`:
- Around line 129-151: The main flow in main must perform a
cleanup_stale_resources sweep immediately after resolving the GitHub owner and
before creating the project, issue, branches, or PR. Match stale resources using
the existing PREFIX and BRANCH_PREFIX values, reusing the cleanup pattern from
the other live QA scripts.
- Around line 102-110: Update wait_for_items to use a configurable timeout
rather than relying solely on the fixed TIMEOUT_SECONDS value. Accept a timeout
argument or read an environment override, while preserving the existing polling
and return behavior.
In `@tests/qa/live_linked_branch.py`:
- Around line 125-132: Move the non-default-base precondition next to the
BRANCH_PREFIX-derived base_branch construction, validating it against
default_branch before creating the branch, issue, commit, or pull request.
Replace the ineffective base_branch check near the linkage wait with an
assertion that the created pr response’s base.ref differs from default_branch,
and retain development_non_default_pr=passed only after that validation.
In `@tests/qa/live_promotion_sync.py`:
- Around line 215-221: Update the exception handler around delete_branch in the
stale sweep loop to print a warning containing the branch name and exception
details instead of silently passing; preserve the existing cleanup flow and
continue processing remaining refs.
In `@tests/test_pr_project_sync.py`:
- Around line 94-100: Update make_config to create the temporary directory with
tempfile.TemporaryDirectory and register its cleanup with the test case,
preserving the generated project.json and project_setup.json paths while
ensuring each directory is removed after the test.
- Around line 125-136: Add a test alongside
test_missing_project_is_reported_without_guessing that configures
list_owner_projects to return multiple projects with the same configured title,
then assert resolve_project_number raises RuntimeError. Use the existing client,
config_path, owner, and repository setup, and verify the ambiguous-title path
refuses to select a project.
- Around line 89-90: Strengthen the test around the existing-item path by
asserting that update_single_select is called with the existing item id,
“existing-item”. Keep the add_issue_to_project not-called assertion and existing
update_status assertion unchanged.
In `@tests/test_project_field_reconciliation.py`:
- Around line 46-48: Extend the assertions in the reconciliation test around
variables["options"] to verify that the newly created “In review” option has no
id. Keep the existing name and id-reuse assertions for “In progress” and “Done”
unchanged.
- Around line 105-108: Update the test around ensure_fields to assert that
list_project_fields is called only once when no fields change, using a
call-count assertion on its mock. Keep the existing update_field and result
assertions unchanged.
🪄 Autofix

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: 689c0467-0e66-4730-a6ac-30ae92d7f103

📥 Commits

Reviewing files that changed from the base of the PR and between d1e61e8 and 40ed705.

📒 Files selected for processing (20)
  • .github/workflows/qa-live.yml
  • docs/repo/pr-governance-architecture.md
  • docs/repo/pr-governance-architecture.pt-BR.md
  • docs/repo/pr-sync.md
  • docs/repo/pr-sync.pt-BR.md
  • project_setup/linked_branch.py
  • project_setup/pr_project_sync.py
  • project_setup/pr_sync_router.py
  • project_setup/project.py
  • project_setup/project_lookup.py
  • project_setup/promotion_sync.py
  • tests/fixtures/structured-pr-sync-smoke-62.txt
  • tests/qa/live_implementation_project.py
  • tests/qa/live_linked_branch.py
  • tests/qa/live_pr_sync.py
  • tests/qa/live_promotion_sync.py
  • tests/test_linked_branch.py
  • tests/test_pr_project_sync.py
  • tests/test_project_field_reconciliation.py
  • tests/test_promotion_sync.py

Comment on lines +84 to +109
- name: Run live implementation PR Project membership test
env:
QA_REPOSITORY: ${{ vars.QA_REPOSITORY }}
PROJECT_SETUP_PAT: ${{ secrets.QA_PROJECT_SETUP_PAT }}
run: >-
python tests/qa/live_implementation_project.py
--repo "$QA_REPOSITORY"
--run-id "pr-${{ inputs.pr_number }}-${GITHUB_RUN_ID}-${GITHUB_RUN_ATTEMPT}"

- name: Run live Promotion Sync native metadata test
env:
QA_REPOSITORY: ${{ vars.QA_REPOSITORY }}
PROJECT_SETUP_PAT: ${{ secrets.QA_PROJECT_SETUP_PAT }}
run: >-
python tests/qa/live_promotion_sync.py
--repo "$QA_REPOSITORY"
--run-id "pr-${{ inputs.pr_number }}-${GITHUB_RUN_ID}-${GITHUB_RUN_ATTEMPT}"

- name: Run live Development linked-branch test
env:
QA_REPOSITORY: ${{ vars.QA_REPOSITORY }}
PROJECT_SETUP_PAT: ${{ secrets.QA_PROJECT_SETUP_PAT }}
run: >-
python tests/qa/live_linked_branch.py
--repo "$QA_REPOSITORY"
--run-id "pr-${{ inputs.pr_number }}-${GITHUB_RUN_ID}-${GITHUB_RUN_ATTEMPT}"

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

Pass inputs.pr_number through env instead of direct template expansion.

Lines 91, 100, and 109 expand ${{ inputs.pr_number }} directly into the shell command. zizmor flags this as template injection. The same pattern exists in the earlier steps, so the new steps extend the exposure. Bind the input to an environment variable, and reference the variable in the command.

🛡️ Proposed fix for one step; apply the same change to the other two
 - name: Run live implementation PR Project membership test
env:
QA_REPOSITORY: ${{ vars.QA_REPOSITORY }}
PROJECT_SETUP_PAT: ${{ secrets.QA_PROJECT_SETUP_PAT }}
+ PR_NUMBER: ${{ inputs.pr_number }}
run: >-
python tests/qa/live_implementation_project.py
--repo "$QA_REPOSITORY"
- --run-id "pr-${{ inputs.pr_number }}-${GITHUB_RUN_ID}-${GITHUB_RUN_ATTEMPT}"+ --run-id "pr-${PR_NUMBER}-${GITHUB_RUN_ID}-${GITHUB_RUN_ATTEMPT}"
📝 Committable suggestion

‼️IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
- name: Run live implementation PR Project membership test
env:
QA_REPOSITORY: ${{ vars.QA_REPOSITORY }}
PROJECT_SETUP_PAT: ${{ secrets.QA_PROJECT_SETUP_PAT }}
run: >-
python tests/qa/live_implementation_project.py
--repo "$QA_REPOSITORY"
--run-id "pr-${{ inputs.pr_number }}-${GITHUB_RUN_ID}-${GITHUB_RUN_ATTEMPT}"
- name: Run live Promotion Sync native metadata test
env:
QA_REPOSITORY: ${{ vars.QA_REPOSITORY }}
PROJECT_SETUP_PAT: ${{ secrets.QA_PROJECT_SETUP_PAT }}
run: >-
python tests/qa/live_promotion_sync.py
--repo "$QA_REPOSITORY"
--run-id "pr-${{ inputs.pr_number }}-${GITHUB_RUN_ID}-${GITHUB_RUN_ATTEMPT}"
- name: Run live Development linked-branch test
env:
QA_REPOSITORY: ${{ vars.QA_REPOSITORY }}
PROJECT_SETUP_PAT: ${{ secrets.QA_PROJECT_SETUP_PAT }}
run: >-
python tests/qa/live_linked_branch.py
--repo "$QA_REPOSITORY"
--run-id "pr-${{ inputs.pr_number }}-${GITHUB_RUN_ID}-${GITHUB_RUN_ATTEMPT}"
- name: Run live implementation PR Project membership test
env:
QA_REPOSITORY: ${{ vars.QA_REPOSITORY }}
PROJECT_SETUP_PAT: ${{ secrets.QA_PROJECT_SETUP_PAT }}
PR_NUMBER: ${{ inputs.pr_number }}
run: >-
python tests/qa/live_implementation_project.py
--repo "$QA_REPOSITORY"
--run-id "pr-${PR_NUMBER}-${GITHUB_RUN_ID}-${GITHUB_RUN_ATTEMPT}"
- name: Run live Promotion Sync native metadata test
env:
QA_REPOSITORY: ${{ vars.QA_REPOSITORY }}
PROJECT_SETUP_PAT: ${{ secrets.QA_PROJECT_SETUP_PAT }}
PR_NUMBER: ${{ inputs.pr_number }}
run: >-
python tests/qa/live_promotion_sync.py
--repo "$QA_REPOSITORY"
--run-id "pr-${PR_NUMBER}-${GITHUB_RUN_ID}-${GITHUB_RUN_ATTEMPT}"
- name: Run live Development linked-branch test
env:
QA_REPOSITORY: ${{ vars.QA_REPOSITORY }}
PROJECT_SETUP_PAT: ${{ secrets.QA_PROJECT_SETUP_PAT }}
PR_NUMBER: ${{ inputs.pr_number }}
run: >-
python tests/qa/live_linked_branch.py
--repo "$QA_REPOSITORY"
--run-id "pr-${PR_NUMBER}-${GITHUB_RUN_ID}-${GITHUB_RUN_ATTEMPT}"
🧰 Tools
🪛 zizmor (1.29.0)

[error] 91-91: code injection via template expansion (template-injection): may expand into attacker-controllable code

(template-injection)


[error] 100-100: code injection via template expansion (template-injection): may expand into attacker-controllable code

(template-injection)


[error] 109-109: code injection via template expansion (template-injection): may expand into attacker-controllable code

(template-injection)

🤖 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 @.github/workflows/qa-live.yml around lines 84 - 109, Update all three live
QA steps—“Run live implementation PR Project membership test,” “Run live
Promotion Sync native metadata test,” and “Run live Development linked-branch
test”—to expose inputs.pr_number through the step env as a dedicated variable,
then reference that environment variable in each --run-id argument instead of
expanding the GitHub template directly in the shell command.

Source: Linters/SAST tools

Comment on lines +91 to +104
if dry_run:
print(
f"[DRY-RUN] Would set PR #{ctx.number} `{field_name}` "
f"to `{desired_status}` in Project v2 #{project_number}"
)
else:
update_single_select(
project_client,
str(project["id"]),
str(item_id),
str(status_field["id"]),
selected,
)
return f"PR #{ctx.number} synced to `{desired_status}` in Project v2 #{project_number}."

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

The returned note claims a completed sync during a dry run.

Line 104 returns "PR #N synced to ..." for both modes. In dry-run mode no mutation runs. The router at project_setup/pr_sync_router.py Line 74 captures this note, and the governance flow renders such notes into pull-request summaries, so a dry run can report state that does not exist.

🐛 Proposed fix
 if dry_run:
print(
f"[DRY-RUN] Would set PR #{ctx.number} `{field_name}` "
f"to `{desired_status}` in Project v2 #{project_number}"
)
+ return f"[DRY-RUN] PR #{ctx.number} would sync to `{desired_status}` in Project v2 #{project_number}."
else:
📝 Committable suggestion

‼️IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
ifdry_run:
print(
f"[DRY-RUN] Would set PR #{ctx.number} `{field_name}` "
f"to `{desired_status}` in Project v2 #{project_number}"
)
else:
update_single_select(
project_client,
str(project["id"]),
str(item_id),
str(status_field["id"]),
selected,
)
returnf"PR #{ctx.number} synced to `{desired_status}` in Project v2 #{project_number}."
ifdry_run:
print(
f"[DRY-RUN] Would set PR #{ctx.number} `{field_name}` "
f"to `{desired_status}` in Project v2 #{project_number}"
)
returnf"[DRY-RUN] PR #{ctx.number} would sync to `{desired_status}` in Project v2 #{project_number}."
else:
update_single_select(
project_client,
str(project["id"]),
str(item_id),
str(status_field["id"]),
selected,
)
returnf"PR #{ctx.number} synced to `{desired_status}` in Project v2 #{project_number}."
🤖 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 `@project_setup/pr_project_sync.py` around lines 91 - 104, Update the return
message in the function containing the dry_run branch so dry-run execution
reports that the PR would be synced rather than claiming the sync completed.
Preserve the existing completed-sync message for the non-dry-run path, using the
existing desired_status and project_number context.

Comment on lines +63 to +70
if result != 0 or not is_same_repository(ctx, repo):
return result

# Implementation Sync keeps the linked task as a Project item, and the
# router additionally makes the PR itself a Project item so GitHub's
# native Projects sidebar reflects the active review lifecycle.
if not sync_config.get("syncProject", True) or project_number is None or project_client is None:
return result

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Respect enabled: false before the PR joins the Project board.

apply_pr_sync returns 0 when sync.enabled is false, and it performs no work in that case. The router then continues, because it only checks syncProject, project_number, and project_client. The PR is added to Project v2 and its Status is set, even though PR Sync is disabled.

Add the enabled check to the guard.

🐛 Proposed fix
- if not sync_config.get("syncProject", True) or project_number is None or project_client is None:+ if not sync_config.get("enabled", True):+ return result+ if not sync_config.get("syncProject", True) or project_number is None or project_client is None:
return result
📝 Committable suggestion

‼️IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
ifresult!=0ornotis_same_repository(ctx, repo):
returnresult
# Implementation Sync keeps the linked task as a Project item, and the
# router additionally makes the PR itself a Project item so GitHub's
# native Projects sidebar reflects the active review lifecycle.
ifnotsync_config.get("syncProject", True) orproject_numberisNoneorproject_clientisNone:
returnresult
ifresult!=0ornotis_same_repository(ctx, repo):
returnresult
# Implementation Sync keeps the linked task as a Project item, and the
# router additionally makes the PR itself a Project item so GitHub's
# native Projects sidebar reflects the active review lifecycle.
ifnotsync_config.get("enabled", True):
returnresult
ifnotsync_config.get("syncProject", True) orproject_numberisNoneorproject_clientisNone:
returnresult
🤖 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 `@project_setup/pr_sync_router.py` around lines 63 - 70, Update the guard in
apply_pr_sync after the apply_pr_sync result check to also return when
sync_config["enabled"] is false, before any Project v2 operations occur.
Preserve the existing syncProject, project_number, and project_client checks.

Comment on lines +173 to +186
def single_select_option_inputs(existing_field: dict, desired_field: dict) -> list[dict]:
existing_by_name = {
str(option.get("name") or "").casefold(): option
for option in existing_field.get("options", [])
if option.get("name")
}
result: list[dict] = []
for desired_name in desired_field.get("options", []):
option = {"name": str(desired_name), "color": "GRAY", "description": ""}
current = existing_by_name.get(str(desired_name).casefold())
if current and current.get("id"):
option["id"] = str(current["id"])
result.append(option)
return result

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🗄️ Data Integrity & Integration | 🟠 Major | 🏗️ Heavy lift

Option replacement semantics are destructive, and the unit test locks that behavior in.single_select_option_inputs returns only the desired options, and updateProjectV2Field replaces the whole option set, so any option the definition omits is deleted along with the Status values of items assigned to it. The test asserts exactly that output, so the behavior is now a fixed contract rather than an open question.

  • project_setup/project.py#L173-L186: preserve color and description for matched options, and decide explicitly whether unlisted options are removed or retained. If retention is chosen, extend the options selection in list_project_fields at Line 127 to fetch color and description.
  • tests/test_project_field_reconciliation.py#L10-L26: update the expected list to match the chosen contract. If unlisted options are retained, add Todo with its TODO-ID to the expected output. If removal stays intentional, add a comment naming the removal contract so the omission of Todo reads as deliberate.
📍 Affects 2 files
  • project_setup/project.py#L173-L186 (this comment)
  • tests/test_project_field_reconciliation.py#L10-L26
🤖 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 `@project_setup/project.py` around lines 173 - 186, Update
single_select_option_inputs to preserve color and description for matched
options, and explicitly retain or remove options omitted from the desired
definition; if retaining them, update list_project_fields to fetch the required
fields. In tests/test_project_field_reconciliation.py lines 10-26, update the
expected list for the chosen contract: include Todo with TODO-ID when retaining
omitted options, or document the intentional removal contract when deletion
remains.

Comment on lines +189 to +203
def update_single_select_field(client: GitHubClient, existing_field: dict, desired_field: dict) -> None:
mutation = """
mutation($field:ID!, $options:[ProjectV2SingleSelectFieldOptionInput!]!) {
updateProjectV2Field(input:{fieldId:$field,singleSelectOptions:$options}) {
projectV2Field { ... on ProjectV2SingleSelectField { id } }
}
}
"""
client.graphql(
mutation,
{
"field": existing_field["id"],
"options": single_select_option_inputs(existing_field, desired_field),
},
)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🌐 Web query:

GitHub GraphQL updateProjectV2Field singleSelectOptions input schema

💡 Result:

The updateProjectV2Field mutation does not exist in the official GitHub GraphQL API [1]. While the schema definition for creating project fields (createProjectV2Field) includes a singleSelectOptions input that uses the ProjectV2SingleSelectFieldOptionInput object [2][3], there is no corresponding mutation to update an existing field's options or configuration [1]. If you are attempting to update the value of a single-select field for a specific item (e.g., changing the status of an issue in a project), you should use the updateProjectV2ItemFieldValue mutation instead [4][5][6]. To update an item's field value to a specific single-select option: mutation { updateProjectV2ItemFieldValue( input: { projectId: "PROJECT_ID" itemId: "ITEM_ID" fieldId: "FIELD_ID" value: { singleSelectOptionId: "OPTION_ID" } }) { projectV2Item { id } } } The ProjectV2SingleSelectFieldOptionInput object (referenced in some community discussions regarding non-existent or experimental endpoints) typically contains the following fields [2][3]: - name: String! (The name of the option) - description: String! (The description text of the option) - color: ProjectV2SingleSelectFieldOptionColor! (The display color, e.g., GRAY, BLUE, etc.) [7][8] - id: String (The ID of an existing option; including this is necessary to preserve the option's identity if such an update were possible) [2][3] Because updateProjectV2Field is not a supported mutation, any attempts to use it to modify project field options via the GraphQL API will fail [1]. Management of project field options must be performed through the GitHub Projects user interface [1].

Citations:


🌐 Web query:

site:docs.github.com/en/graphql/reference/mutations updateProjectV2Field

💡 Result:

updateProjectV2Field updates a GitHub Projects (v2) field.

Input:UpdateProjectV2FieldInput!

Key fields:

  • fieldId: ID! — field to update
  • name: String — new field name
  • singleSelectOptions — single-select options
  • multiSelectOptions — multi-select options
  • iterationConfiguration — iteration-field configuration
  • clientMutationId: String

Returns:

  • projectV2Field — updated field configuration
  • clientMutationId

(docs.github.com)

Citations:


🌐 Web query:

GitHub GraphQL schema ProjectV2SingleSelectFieldOptionInput updateProjectV2Field

💡 Result:

The mutation updateProjectV2Field is used in the GitHub GraphQL API to update settings for a Project V2 field, specifically including its single-select options [1][2]. The input object ProjectV2SingleSelectFieldOptionInput is used to define individual options within the singleSelectOptions argument of this mutation [3][1]. Its fields include: - name (String!): The name of the option [3]. - color (ProjectV2SingleSelectFieldOptionColor!): The display color of the option [3][4]. - description (String!): The description text for the option [3]. - id (String): The ID of an existing option. Including this is critical when updating; if omitted, existing options will be removed, and their associated field values on items will be cleared [3][1][2]. Key considerations for using updateProjectV2Field: - Destructive Updates: The mutation replaces the entire list of options for the field [1][2]. To safely modify options (e.g., adding a new one), you must first query the existing options, include them in the mutation alongside the new options, and ensure their IDs are provided [1][2]. - Required Arguments: The mutation requires the fieldId and the full list of desired singleSelectOptions [1]. Note that it does not take a projectId argument; the project context is implicit from the field ID [1]. - Color Requirement: The color field is mandatory for every option provided in the input; omitting it may cause the mutation to fail [4]. Note: While this mutation is used by various automation tools [4][2], some documentation or community discussions have historically labeled it as unsupported or non-existent [5], likely due to its role as an internal-facing or evolved API endpoint. Developers typically query the project's field definitions first to obtain the necessary field IDs and existing option structures [1][2].

Citations:


🏁 Script executed:

#!/bin/bashset -eu
printf'%s\n''--- relevant repository references ---'
rg -n -C 4 'updateProjectV2Field|ProjectV2SingleSelectFieldOptionInput|singleSelectOptions|single_select_option_inputs'. --glob '!node_modules' --glob '!dist' --glob '!build'printf'%s\n''--- project.py section ---'
sed -n '150,215p' project_setup/project.py
printf'%s\n''--- github.py graphql error handling ---'
sed -n '185,220p' project_setup/github.py

Repository: v-Kaefer/Github-Project-Automation

Length of output: 1954


🌐 Web query:

site:docs.github.com/en/graphql/reference/projects "updateProjectV2Field" "ProjectV2SingleSelectFieldOptionInput"

💡 Result:

The ProjectV2SingleSelectFieldOptionInput input object is used in the GitHub GraphQL API to define or update options for a single-select custom field in GitHub Projects [1]. When performing operations related to single-select fields—such as creating a field or updating one—this input object allows you to specify the properties of the select options [1]. The ProjectV2SingleSelectFieldOptionInput object includes the following fields: - color (ProjectV2SingleSelectFieldOptionColor!): The display color associated with the option [1]. - description (String!): The description text for the option [1]. - id (String): The ID of an existing single-select option [1]. Providing this ID is critical when updating existing options to preserve their identity, which prevents existing item field values from being cleared [1]. - name (String!): The name of the option [1]. If you are looking to update a specific item's value within a Project V2, you typically use the updateProjectV2ItemFieldValue mutation rather than updating the field definition itself [1]. When using this mutation for a single-select field, you reference the specific option ID, which is often derived from the field configuration using the ProjectV2SingleSelectField type [1].

Citations:


🌐 Web query:

site:docs.github.com/en/graphql/reference/objects ProjectV2SingleSelectFieldOptionInput id

💡 Result:

ProjectV2SingleSelectFieldOptionInput.id is an optional String:

The ID of an existing single-select option. Include it when updating options to preserve their identity and prevent existing item values from being cleared. [1] (docs.github.com)

Citations:


🌐 Web query:

GitHub public GraphQL schema updateProjectV2Field fieldId singleSelectOptions

💡 Result:

The updateProjectV2Field mutation exists in the GitHub GraphQL API and is used to update the configuration of an existing project field, including the singleSelectOptions for single-select fields [1][2]. When using this mutation, there are critical considerations to prevent data loss and ensure successful execution: 1. Full Replacement Behavior: The updateProjectV2Field mutation replaces all existing options for the specified field [2]. To add new options while preserving existing ones, you must fetch the current options first and include both the existing options (with their current IDs) and the new options in the singleSelectOptions array [2][3]. Failing to include existing option IDs will result in those options being removed from the field [2]. 2. Mutation Parameters: - fieldId: The ID of the field to update (e.g., PVTF_...) [2]. - name: The name of the field [1][2]. - singleSelectOptions: An array of ProjectV2SingleSelectFieldOptionInput objects [1][2]. 3. Required Option Fields: Each option object in the singleSelectOptions array must include: - name: The name of the option [2][3]. - color: The color of the option (e.g., "GRAY", "BLUE", etc.). This field is mandatory; omitting it or providing a null value will cause the mutation to fail [3]. - id: Optional for new options, but required for existing options to preserve their current state and associated item assignments [2]. It is important to distinguish this mutation from updateProjectV2ItemFieldValue, which is used to update the specific value of an item within the project (e.g., setting an item's status) rather than modifying the field's schema or available options [4].

Citations:


🌐 Web query:

GitHub GraphQL API reference UpdateProjectV2FieldInput singleSelectOptions required id ProjectV2SingleSelectFieldOptionInput

💡 Result:

In the GitHub GraphQL API, the updateProjectV2Field mutation is used to modify single-select fields [1][2]. When updating these fields, the singleSelectOptions input is an array of ProjectV2SingleSelectFieldOptionInput objects [3][1]. Key requirements and behaviors for this input include: 1. Preservation of Identity: To update existing options and prevent their assignments (field values) from being cleared, you must include the id field for those existing options [3][1][2]. If you omit the id for an existing option, the mutation will treat it as a deletion and create a new option, which will break existing item associations [1][2]. 2. Replacement Behavior: The singleSelectOptions array replaces the entire existing list of options for the field [1][2]. You must include both the options you wish to keep (with their original ids) and any new options you wish to add [1][4][2]. 3. Required Fields for Options: Each ProjectV2SingleSelectFieldOptionInput object generally requires a name [3]. While documentation or implementation may vary on strictly required fields, it is common to provide name, description, and color [3][5][4]. Notably, the color field is frequently required by the API; failing to provide it for all options in the array often results in a validation error [5]. 4. Mutation Constraints: The UpdateProjectV2FieldInput object does not accept a projectId [1][2]. The mutation only requires the fieldId because the field is already uniquely associated with a project [1][2]. Including a projectId in this specific mutation input will result in an error [1][2].

Citations:


🌐 Web query:

raw GitHub GraphQL schema UpdateProjectV2FieldInput singleSelectOptions ProjectV2SingleSelectFieldOptionInput

💡 Result:

In the GitHub GraphQL API, the UpdateProjectV2FieldInput input object is used to modify existing project fields [1][2]. For SINGLE_SELECT fields, this input accepts an array of ProjectV2SingleSelectFieldOptionInput objects via the singleSelectOptions field [3][4]. Crucially, the updateProjectV2Field mutation replaces the entire list of options [1][2]. If you do not include the IDs of existing options in your mutation, those options will be deleted, and all corresponding item field assignments will be lost [1][2]. The ProjectV2SingleSelectFieldOptionInput input object consists of the following fields: - name (String!): The name of the option [3][4]. - description (String!): The description text of the option [3][4]. - color (ProjectV2SingleSelectFieldOptionColor!): The display color of the option [3][4]. - id (String): The ID of an existing single-select option [3][4]. Including this ID is required to preserve the option's identity and existing assignments during an update [3][4][2]. Note that the updateProjectV2Field mutation takes fieldId as its primary identifier [1][2]. It does not accept a projectId, as the project context is implicit from the field ID [1][2]. If you attempt to pass a projectId in this mutation, you will receive an error stating that the input object does not accept that argument [1][2].

Citations:


Preserve existing options and metadata

updateProjectV2Field replaces the complete singleSelectOptions list. Include existing options that are not in the configured list, with their IDs, or the mutation deletes them and clears assigned item values. Preserve existing color and description values instead of resetting them to "GRAY" and "".

🤖 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 `@project_setup/project.py` around lines 189 - 203, Update
update_single_select_field and single_select_option_inputs so the mutation
submits the complete option list: retain existing options absent from the
desired configuration, including their IDs, and preserve each option’s existing
color and description unless explicitly configured otherwise. Ensure configured
options are merged with existing metadata rather than defaulting preserved
values to GRAY or an empty description.

Comment on lines +163 to +256
def list_project_content_items(client: GitHubClient, project_id: str) -> dict[str, str]:
query = """
query($project:ID!, $cursor:String) {
node(id:$project) {
... on ProjectV2 {
items(first:100,after:$cursor) {
pageInfo { hasNextPage endCursor }
nodes {
id
content {
__typename
... on Issue { id }
... on PullRequest { id }
}
}
}
}
}
}
"""
result: dict[str, str] = {}
cursor = None
while True:
page = client.graphql(query, {"project": project_id, "cursor": cursor})["node"]["items"]
for item in page["nodes"]:
content = item.get("content") or {}
if content.get("__typename") in {"Issue", "PullRequest"} and content.get("id"):
result[str(content["id"])] = str(item["id"])
if not page["pageInfo"]["hasNextPage"]:
return result
cursor = page["pageInfo"]["endCursor"]


def sync_promotion_project_status(
project_client: GitHubClient,
repo: str,
ctx: PullRequestContext,
pr_issue: dict[str, Any],
project_number: int,
desired_status: str,
config: dict[str, Any],
*,
owner: str | None = None,
dry_run: bool = False,
) -> str:
if not config.get("syncProject", True):
return "disabled by configuration."

project_owner = owner or split_repo(repo)[0]
project = find_project(project_client, project_owner, project_number)
fields = {
str(field["name"]): field
for field in list_project_fields(project_client, str(project["id"]))
if field and field.get("name")
}
field_name = str(config.get("projectStatusField") or "Status")
status_field = fields.get(field_name)
if not status_field:
raise RuntimeError(f"Project field `{field_name}` was not found")

selected = status_option_id(status_field, desired_status)
if not selected:
available = ", ".join(str(item.get("name")) for item in status_field.get("options", []))
raise RuntimeError(
f"Project status option `{desired_status}` was not found in `{field_name}`. "
f"Available options: {available or '(none)'}"
)

pr_node = str(pr_issue.get("node_id") or "")
if not pr_node:
raise RuntimeError(f"Promotion PR #{ctx.number} has no GraphQL node id")
current_items = list_project_content_items(project_client, str(project["id"]))
item_id = current_items.get(pr_node)
if not item_id:
if dry_run:
print(f"[DRY-RUN] Would add promotion PR #{ctx.number} to Project v2 #{project_number}")
item_id = f"dry-run-pr-{ctx.number}"
else:
item_id = add_issue_to_project(project_client, str(project["id"]), pr_node)

if dry_run:
print(
f"[DRY-RUN] Would set promotion PR #{ctx.number} `{field_name}` "
f"to `{desired_status}` in Project v2 #{project_number}"
)
else:
update_single_select(
project_client,
str(project["id"]),
str(item_id),
str(status_field["id"]),
selected,
)
return f"promotion PR synced to `{desired_status}` in Project v2 #{project_number}."

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

📐 Maintainability & Code Quality | 🟠 Major | 🏗️ Heavy lift

Reduce the duplication with pr_project_sync.

list_project_content_items and sync_promotion_project_status repeat the logic of list_project_content_items and sync_pull_request_project_status in project_setup/pr_project_sync.py:43-104. The only differences are the log and return strings. A change to the Project v2 contract must then be applied in two places.

Extract one shared implementation, and pass a label for the message prefix.

🤖 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 `@project_setup/promotion_sync.py` around lines 163 - 256, Extract the shared
Project v2 item-listing and status-sync flow from list_project_content_items and
sync_promotion_project_status, reusing it from both this module and
pr_project_sync. Add a label parameter for the differing log and return-message
prefixes, while preserving each caller’s existing behavior and dry-run handling.

Comment on lines +160 to +163
project = next(item for item in list_projects(client, owner, owner_type) if item.get("title") == project_title)
project_id = str(project["id"])
project_number = int(project["number"])
ensure_fields(client, project_id, {"fields": [{"name": "Status", "type": "single_select", "options": ["In progress", "In review", "Done"]}]})

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

Raise a clear error when the created project is not found.

Line 160 uses next(...) with no default. If the project title does not appear, Python raises StopIteration. The failure message then gives no context about the Project v2 lookup. The sibling scripts use project_by_title plus an explicit RuntimeError.

🐛 Proposed fix
- project = next(item for item in list_projects(client, owner, owner_type) if item.get("title") == project_title)+ project = next(+ (item for item in list_projects(client, owner, owner_type) if item.get("title") == project_title),+ None,+ )+ if not project:+ raise RuntimeError(f"Implementation Project v2 `{project_title}` creation verification failed")
project_id = str(project["id"])
📝 Committable suggestion

‼️IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
project=next(itemforiteminlist_projects(client, owner, owner_type) ifitem.get("title") ==project_title)
project_id=str(project["id"])
project_number=int(project["number"])
ensure_fields(client, project_id, {"fields": [{"name": "Status", "type": "single_select", "options": ["In progress", "In review", "Done"]}]})
project=next(
(itemforiteminlist_projects(client, owner, owner_type) ifitem.get("title") ==project_title),
None,
)
ifnotproject:
raiseRuntimeError(f"Implementation Project v2 `{project_title}` creation verification failed")
project_id=str(project["id"])
project_number=int(project["number"])
ensure_fields(client, project_id, {"fields": [{"name": "Status", "type": "single_select", "options": ["In progress", "In review", "Done"]}]})
🤖 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 `@tests/qa/live_implementation_project.py` around lines 160 - 163, Update the
project lookup in the live implementation flow to use a named project_by_title
result and explicitly raise a RuntimeError when no project matching
project_title is found. Include clear Project v2 lookup context in the error,
while preserving the existing project_id, project_number, and ensure_fields
handling for successful lookups.

Comment on lines +42 to +99
def list_projects(client: GitHubClient, owner: str, owner_type: str) -> list[dict]:
query = f"""
query($login:String!, $cursor:String) {{
{owner_type}(login:$login) {{
projectsV2(first:100, after:$cursor) {{
pageInfo {{ hasNextPage endCursor }}
nodes {{ id number title url }}
}}
}}
}}
"""
projects: list[dict] = []
cursor = None
while True:
data = client.graphql(query, {"login": owner, "cursor": cursor})
node = data.get(owner_type)
if not node:
return projects
page = node["projectsV2"]
projects.extend(project for project in page["nodes"] if project)
if not page["pageInfo"]["hasNextPage"]:
return projects
cursor = page["pageInfo"]["endCursor"]


def project_by_title(client: GitHubClient, owner: str, owner_type: str, title: str) -> dict | None:
return next((project for project in list_projects(client, owner, owner_type) if project.get("title") == title), None)


def delete_project(client: GitHubClient, project_id: str) -> None:
mutation = """
mutation($project:ID!) {
deleteProjectV2(input:{projectId:$project}) { projectV2 { id } }
}
"""
client.graphql(mutation, {"project": project_id})


def encoded_ref(branch: str) -> str:
return urllib.parse.quote(f"heads/{branch}", safe="/")


def branch_sha(client: GitHubClient, repo: str, branch: str) -> str:
ref = client.request_json("GET", f"{API_BASE}/repos/{repo}/git/ref/{encoded_ref(branch)}")
return str((ref.get("object") or {})["sha"])


def create_branch(client: GitHubClient, repo: str, branch: str, sha: str) -> None:
client.request_json(
"POST",
f"{API_BASE}/repos/{repo}/git/refs",
{"ref": f"refs/heads/{branch}", "sha": sha},
)


def delete_branch(client: GitHubClient, repo: str, branch: str) -> None:
client.request_json("DELETE", f"{API_BASE}/repos/{repo}/git/refs/{encoded_ref(branch)}")

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

📐 Maintainability & Code Quality | 🟠 Major | 🏗️ Heavy lift

The live Q.A scripts copy the same Project v2 and Git ref helpers. The shared root cause is the absence of a common tests/qa helper module, so each script carries its own copy of the projectsV2 GraphQL query, the deleteProjectV2 mutation, and the ref encoding and branch helpers.

  • tests/qa/live_promotion_sync.py#L42-L99: move list_projects, project_by_title, delete_project, encoded_ref, branch_sha, create_branch, and delete_branch into a shared helper module, and import them here.
  • tests/qa/live_implementation_project.py#L23-L49: delete the local list_projects and delete_project copies, plus the ref_path, create_branch, delete_branch, and branch_sha copies, and import the shared helpers instead.
📍 Affects 2 files
  • tests/qa/live_promotion_sync.py#L42-L99 (this comment)
  • tests/qa/live_implementation_project.py#L23-L49
🤖 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 `@tests/qa/live_promotion_sync.py` around lines 42 - 99, The shared root cause
is duplicated Project v2 and Git ref helpers across the live QA scripts. In
tests/qa/live_promotion_sync.py lines 42-99, move list_projects,
project_by_title, delete_project, encoded_ref, branch_sha, create_branch, and
delete_branch into a shared tests/qa helper module and import them; in
tests/qa/live_implementation_project.py lines 23-49, remove the local
list_projects, delete_project, ref_path, create_branch, delete_branch, and
branch_sha implementations and import the shared helpers instead.

Comment on lines +102 to +107
def test_explicit_project_number_wins_without_remote_lookup(self):
client = Mock(spec=GitHubClient)
number, note = resolve_project_number(client, "owner/repo", 42, config_path=self.make_config())
self.assertEqual(number, 42)
self.assertEqual(note, "configured explicitly")
client.assert_not_called()

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

client.assert_not_called() does not verify that no remote lookup happened.

assert_not_called checks that the mock itself was never invoked as a callable. resolve_project_number never calls client(...), so this assertion passes even if client.graphql ran. The test name states "without remote lookup", which the assertion does not verify.

Assert that no method recorded a call.

💚 Proposed fix
 self.assertEqual(note, "configured explicitly")
- client.assert_not_called()+ self.assertEqual(client.mock_calls, [])
📝 Committable suggestion

‼️IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
deftest_explicit_project_number_wins_without_remote_lookup(self):
client=Mock(spec=GitHubClient)
number, note=resolve_project_number(client, "owner/repo", 42, config_path=self.make_config())
self.assertEqual(number, 42)
self.assertEqual(note, "configured explicitly")
client.assert_not_called()
deftest_explicit_project_number_wins_without_remote_lookup(self):
client=Mock(spec=GitHubClient)
number, note=resolve_project_number(client, "owner/repo", 42, config_path=self.make_config())
self.assertEqual(number, 42)
self.assertEqual(note, "configured explicitly")
self.assertEqual(client.mock_calls, [])
🤖 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 `@tests/test_pr_project_sync.py` around lines 102 - 107, Update
test_explicit_project_number_wins_without_remote_lookup to verify that the
mocked client recorded no method calls, rather than asserting the client object
itself was not called. Keep the existing explicit number and note assertions
unchanged.

Comment on lines +79 to +82
result = ensure_fields(Mock(), "PROJECT", definition, dry_run=False)

update_field.assert_called_once_with(update_field.call_args.args[0], initial, definition["fields"][0])
self.assertEqual(result["Status"]["options"][1]["name"], "In review")

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

The client argument assertion at Line 81 is self-referential and cannot fail.

update_field.call_args.args[0] reads the client from the recorded call, then the assertion compares it to the same recorded call. That element always matches, whatever ensure_fields passed. The client is created inline at Line 79, so no named reference exists to compare against.

Bind the client to a variable and assert against it.

💚 Proposed fix
- result = ensure_fields(Mock(), "PROJECT", definition, dry_run=False)+ client = Mock()+ result = ensure_fields(client, "PROJECT", definition, dry_run=False)- update_field.assert_called_once_with(update_field.call_args.args[0], initial, definition["fields"][0])+ update_field.assert_called_once_with(client, initial, definition["fields"][0])
self.assertEqual(result["Status"]["options"][1]["name"], "In review")
📝 Committable suggestion

‼️IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
result=ensure_fields(Mock(), "PROJECT", definition, dry_run=False)
update_field.assert_called_once_with(update_field.call_args.args[0], initial, definition["fields"][0])
self.assertEqual(result["Status"]["options"][1]["name"], "In review")
client=Mock()
result=ensure_fields(client, "PROJECT", definition, dry_run=False)
update_field.assert_called_once_with(client, initial, definition["fields"][0])
self.assertEqual(result["Status"]["options"][1]["name"], "In review")
🤖 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 `@tests/test_project_field_reconciliation.py` around lines 79 - 82, Update the
test around ensure_fields by assigning the Mock() client to a named variable
before passing it to ensure_fields, then assert update_field was called with
that variable as its first argument instead of reading
update_field.call_args.args[0].

Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

1 participant

@v-Kaefer