Skip to content

ci: rebuild task board state machine with composite actions + bidirectional sync#75

Merged
taitelee merged 16 commits into
mainfrom
ci/orchestrator-phase1
Apr 29, 2026
Merged

ci: rebuild task board state machine with composite actions + bidirectional sync#75
taitelee merged 16 commits into
mainfrom
ci/orchestrator-phase1

Conversation

@EricAndrechek

Copy link
Copy Markdown
Member

Closes #74.

Phase 1 of the Task Board state-machine rebuild discussed 2026-04-23. Moves from the rotating-assignee model to set-once-per-card assignees (Issue = implementer, PR = reviewer) with mirrored statuses (PR Ready ↔ Issue In review; both Done on merge), extracts the shared board operations into composite actions, adds the missing "linked issue → In review" on the reeval path, surfaces Dependabot major bumps onto the board, and adds bidirectional sync for human manual card moves.

Full spec is now in AGENTS.md §Task Board state machine as source of truth.

Changed

File What
.github/actions/board-upsert-status/action.yml New. Idempotent add-to-board + set Status.
.github/actions/assign-and-request-review/action.yml New. Assignees + GitHub review-request together.
.github/actions/set-linked-issues-status/action.yml New. Iterates a PR's linked issues and upserts each.
.github/workflows/project-orchestrator.yml Refactored. Uses composite actions, adds missing linked-issue transition on reeval, extends bot-clean to new required checks.
.github/workflows/dependabot-automerge.yml Extended. Major bumps → board + both admins assigned + review requested.
.github/workflows/board-state-sync.yml New. Bidirectional manual-move mirror on projects_v2_item: edited, with loop-prevention guard.
AGENTS.md §Task Board state machine rewritten as authoritative spec.
CHANGELOG.md Unreleased entries covering all four pieces.

Testability note

pull_request_target runs the workflow from the PR's base branch. So this PR cannot smoke-test the new orchestrator against itself — the old one fires. The smoke test is the next PR after merge, where we should see:

  • Orchestrator adds PR to board, Status=Ready when bot-clean, assigns reviewer, requests review, moves linked issue to In review
  • Manual card move on the board mirrors to the linked counterpart, with no loop
  • Next Dependabot major bump lands on the board with both admins assigned

Fix-forward on regressions, not revert — the composite actions isolate most risk.

Risk

The workflow files in this PR are inert until merged to main (pull_request_target uses base-branch workflow, and our composite actions don't exist in main yet). So there's zero half-old/half-new state during this PR's lifecycle.


— Posted by Claude Code on behalf of @EricAndrechek

Copilot AI review requested due to automatic review settings April 23, 2026 19:25
@github-actions github-actions Bot added github_actions Pull requests that update GitHub Actions code area/docs Documentation, site/, README area/infra CI, build, deploy, Docker, release labels Apr 23, 2026
@gemini-code-assist

Copy link
Copy Markdown

Summary of Changes

Hello, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed!

This pull request overhauls the Task Board state machine to improve consistency and reduce manual overhead. By standardizing assignee roles and implementing bidirectional status mirroring, the system now ensures that PRs and their linked issues remain in sync regardless of whether transitions are triggered by automated workflows or manual board interactions. The refactor also consolidates redundant logic into reusable composite actions, simplifying maintenance and improving the reliability of the orchestrator.

Highlights

  • Task Board State Machine Rebuild: Transitioned to a set-once-per-card assignee model and implemented mirrored statuses between PRs and linked issues to ensure consistent state tracking.
  • Composite Actions: Extracted shared board operations into reusable composite actions for upserting items, managing assignees/review requests, and updating linked issue statuses.
  • Bidirectional Sync: Added a new workflow to synchronize manual status changes on the Task Board between linked PRs and issues, including loop-prevention guards.
  • Dependabot Integration: Updated Dependabot major version bumps to automatically appear on the Task Board with appropriate assignments and review requests.
Ignored Files
  • Ignored by pattern: .github/workflows/** (3)
    • .github/workflows/board-state-sync.yml
    • .github/workflows/dependabot-automerge.yml
    • .github/workflows/project-orchestrator.yml
Using Gemini Code Assist

The full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips.

Invoking Gemini

You can request assistance from Gemini at any point by creating a comment using either /gemini <command> or @gemini-code-assist <command>. Below is a summary of the supported commands on the current page.

Feature Command Description
Code Review /gemini review Performs a code review for the current pull request in its current state.
Pull Request Summary /gemini summary Provides a summary of the current pull request in its current state.
Comment @gemini-code-assist Responds in comments when explicitly tagged, both in pull request comments and review comments.
Help /gemini help Displays a list of available commands.

Customization

To customize Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a .gemini/ folder in the base of the repository. Detailed instructions can be found here.

Limitations & Feedback

Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counter productive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for GitHub and other Google products, sign up here.

Footnotes

  1. Review the Privacy Notices, Generative AI Prohibited Use Policy, Terms of Service, and learn how to configure Gemini Code Assist in GitHub here. Gemini can make mistakes, so double check it and use code with caution.

@gemini-code-assist gemini-code-assist 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.

Code Review

This pull request introduces several composite GitHub Actions to automate and synchronize Task Board operations, including reviewer assignment, item upsertion, and status mirroring for linked issues. It also updates AGENTS.md to document the refined state machine logic. Feedback focuses on optimizing API usage by avoiding N+1 query patterns in GraphQL and batching assignee updates to improve performance.

Comment thread .github/actions/set-linked-issues-status/action.yml Outdated
Comment thread .github/actions/assign-and-request-review/action.yml Outdated

Copilot AI 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.

Pull request overview

Rebuilds the CI-driven Task Board state machine around reusable composite actions, switches to set-once-per-card assignees with mirrored PR↔Issue statuses, surfaces Dependabot major bumps onto the board, and adds bidirectional sync when humans manually move cards.

Changes:

  • Introduces composite actions to upsert board items, apply assignee+review-request together, and mirror status to linked issues.
  • Refactors project-orchestrator.yml to the new mirrored-status model (including the missing linked-issue transition on reeval) and updates required checks for bot-clean.
  • Extends Dependabot major-bump handling and adds a new manual-move mirroring workflow (projects_v2_item: edited), plus updates docs + changelog.

Reviewed changes

Copilot reviewed 8 out of 8 changed files in this pull request and generated 4 comments.

Show a summary per file
File Description
CHANGELOG.md Documents the new composite actions, bidirectional sync, Dependabot major bump surfacing, and orchestrator refactor.
AGENTS.md Updates the authoritative “Task Board state machine” spec to set-once assignees + mirrored statuses + bidirectional sync semantics.
.github/workflows/project-orchestrator.yml Refactors orchestration logic to use composite actions and implements the mirrored-status transitions (incl. reeval linked-issue promotion).
.github/workflows/dependabot-automerge.yml Adds board placement + assignment/review-request for major bumps via the shared composite actions.
.github/workflows/board-state-sync.yml Adds new workflow to mirror manual Status moves across linked PR↔Issue counterparts with loop prevention.
.github/actions/board-upsert-status/action.yml New composite action to idempotently add to project + set Status.
.github/actions/assign-and-request-review/action.yml New composite action to keep PR assignees and requested reviewers in sync.
.github/actions/set-linked-issues-status/action.yml New composite action to update the board Status for PR-linked issues.

Comment thread .github/actions/set-linked-issues-status/action.yml Outdated
Comment thread .github/workflows/board-state-sync.yml Outdated
Comment thread .github/workflows/board-state-sync.yml Outdated
Comment thread .github/workflows/project-orchestrator.yml Outdated
@claude

claude Bot commented Apr 23, 2026

Copy link
Copy Markdown

Claude finished @EricAndrechek's task in 3m 14s —— View job


Review — ci: rebuild task board state machine

This PR has been through eight Copilot rounds and one prior Claude pass; most obvious gaps are closed. Two issues remain.


Findings

[Medium] request-review silently goes false on the SKIP_BOARD path

project-orchestrator.yml:426:

request-review: ${{ steps.pr-status.outputs.promote == 'true' }}

When PROJECT_BOARD_TOKEN is absent, the pr-status step is skipped (if: … && env.SKIP_BOARD != 'true'), so steps.pr-status.outputs.promote is empty string. '' == 'true' evaluates to false, meaning request-review is always false on the SKIP_BOARD path.

Result: in environments without a board token, the bot-clean signal assigns a reviewer but never fires the GitHub review request. The reviewer lands in the board queue (if we had a board) but gets no notification. The assignee gets set, but dismiss_stale_reviews_on_push still requires an actual review request, so the pairing is broken.

This is the same structural problem Claude's review flagged in commit 061101d for the Assign step's condition. That fix extended the if: condition with || env.SKIP_BOARD == 'true', but the request-review input didn't get the same treatment.

Fix:

request-review: ${{ steps.pr-status.outputs.promote == 'true' || env.SKIP_BOARD == 'true' }}

The SKIP_BOARD path has no way to know if this is a first-time bot-clean or a re-eval after CHANGES_REQUESTED, so treating it as "always request" on reeval is the safer default — an idempotent re-request is less harmful than a permanently-silent assignment. Fix this →


[Low] PR-side counterpart query in board-state-sync.yml swallows GraphQL errors silently

board-state-sync.yml:156–163:

counterparts=$(gh api graphql -f query='...' -F id="$CONTENT_NODE_ID" \
  --jq '.data.node.closingIssuesReferences.nodes[].id')

The --jq flag is applied inline. If the GraphQL response contains .errors (bad token scope, invalid node ID, transient failure), gh api still exits 0 and --jq attempts to extract from whatever JSON it received — producing empty output. counterparts becomes empty and the workflow takes the "No linked counterpart(s) — nothing to mirror" path: silent no-op.

The Issue-side query (lines 172–196) has explicit .errors handling that fails loudly for real errors. The PR side should match:

counterparts_json=$(gh api graphql -f query='...' -F id="$CONTENT_NODE_ID")
graphql_errors=$(echo "$counterparts_json" | jq -c '.errors // empty')
if [ -n "$graphql_errors" ]; then
  echo "::error::GraphQL error querying PR counterparts for $CONTENT_NODE_ID: $graphql_errors"
  exit 1
fi
counterparts=$(echo "$counterparts_json" | jq -r '.data.node.closingIssuesReferences.nodes[].id')

Fix this →


What's good

After the eight Copilot rounds, everything that mattered got fixed. A few highlights:

  • Checkout security (project-orchestrator.yml:116-120, dependabot-automerge.yml:46-50): both workflows pin checkout to default_branch with persist-credentials: false. This closes the pull_request_target + local composite action attack surface correctly.
  • GraphQL error handling: board-upsert-status and set-linked-issues-status both validate .errors + null-resource checks before touching any state. The Issue-side query in board-state-sync does too (PR side is the gap above).
  • Loop prevention: the mirror writes only when current_option != MIRROR_OPTION_ID. Sound — the write triggers the event again, but the next run finds the value already correct and no-ops.
  • Per-side idempotency in assign-and-request-review: reconciling MISSING_ASSIGNEES and MISSING_REVIEW_REQUESTS separately means a dismiss_stale_reviews_on_push clearing a review request without touching the assignee gets the request restored without redundant assignee calls.
  • Reviewer selection: deterministic single-pick with PR-number parity as the fallback for external contributors. Correct.
  • cancel-in-progress: false on both project-orchestrator.yml and admin-approval.yml prevents the CANCELLED-check-run spurious failure that was previously blocking approvals.
  • request-review input with false on re-evals: correctly avoids re-notifying the reviewer after a CHANGES_REQUESTED review GitHub legitimately cleared, while still repairing the assignee side.

Verdict

Iterate — the request-review/SKIP_BOARD bug (#1) will silently break the human-notification half of the assign + request review pair in any environment where PROJECT_BOARD_TOKEN isn't configured. One-line fix. The Low finding (#2) is a cleanup.

@EricAndrechek

Copy link
Copy Markdown
Member Author

Pushed e5447fe after the first smoke-test run surfaced a latent bug I missed in the original design.

What went wrong on this PR's smoke test:

  • CI completed green (except the expected non-blocking E2E)
  • Orchestrator ran, decided action=reeval on the initial pull_request_target: opened event
  • Bot-clean check said "Required check 'Check' is ''" and exited (correct — CI hadn't started yet)
  • But then the orchestrator never re-ran to re-check bot-clean after CI finished
  • Gemini's COMMENTED review DID trigger pull_request_review: submitted, but my state machine mapped COMMENTED → action=none
  • Result: PR never got added to the board or assigned

Root cause: check_suite: completed is suppressed by GitHub when the check suite was created by a workflow using GITHUB_TOKEN. Our CI uses GITHUB_TOKEN, so that event was never firing — on any PR, for as long as the orchestrator has existed. Pre-existing bug I inherited from #65; smoke-testing this PR is the first time it's been caught.

Fix:

  1. check_suite: completedworkflow_run: [CI completed]. workflow_run is the documented chain-off-another-workflow trigger and doesn't have the GITHUB_TOKEN suppression gotcha.
  2. COMMENTED reviews → re-eval (was: none). Bot reviews (Gemini, Copilot) now trigger bot-clean re-checks, giving the state machine a third independent signal.

Three re-eval paths now (previously only one worked):

  • Push (synchronize) — always worked
  • Bot review (pull_request_review: submitted) — new, catches Gemini/Copilot
  • CI completed (workflow_run) — replaces broken check_suite

The fix itself won't help verify this PR's behavior since pull_request_target still uses main's workflow. But after this PR merges, the next internal PR will have a robust re-eval chain.

— Posted by Claude Code on behalf of @EricAndrechek

EricAndrechek added a commit that referenced this pull request Apr 23, 2026
Bundle addressing the five legitimate review findings on #75:

1. [Copilot] reeval guard compared Status by display *name* ("Backlog"
   / ""); renaming the project option would silently break the guard.
   Switched to optionId comparison against STATUS_BACKLOG, with name
   kept for log readability.

2. [Gemini] assign-and-request-review looped per-user through
   `gh pr edit --add-assignee`. gh accepts a comma-separated list —
   one call assigns everyone, idempotent on already-assigned.

3. [Copilot] set-linked-issues-status description said "fetches all
   linked issues" but the query had `first: 10`. Bumped to first: 50
   + totalCount check + a warning log when the cap is hit. Updated
   description to reflect the 50-item ceiling.

4. [Gemini] N+1 GraphQL in set-linked-issues-status: the old loop
   made one query per linked issue to fetch its project items.
   Collapsed into a single query using the PR's
   closingIssuesReferences with a nested projectItems lookup — one
   round-trip for any number of linked issues up to the cap. The jq
   extraction uses `map(select ...)[0]` to safely pick the single
   matching project item per issue.

5. [Copilot] board-state-sync's `echo $counterparts` was unquoted
   (SC2086). Switched to `printf '%s '` with a trim, same semantics
   without the shellcheck warning. Also fixed a comment that referred
   to `closingPullRequestsReferences` while the code used
   `closedByPullRequestsReferences`.

Open intentionally: the Copilot comment flagging the Issue-side query
falling back to a timelineItems/ConnectedEvent heuristic. We'll
address that *if* we observe closedByPullRequestsReferences being
unavailable in practice — no need to pre-emptively write the
fallback.
@EricAndrechek EricAndrechek requested a review from Copilot April 23, 2026 19:54

Copilot AI 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.

Pull request overview

Copilot reviewed 8 out of 8 changed files in this pull request and generated 4 comments.

Comment thread .github/workflows/project-orchestrator.yml
Comment thread .github/workflows/project-orchestrator.yml
Comment thread .github/workflows/board-state-sync.yml Outdated
Comment thread .github/actions/set-linked-issues-status/action.yml Outdated
@EricAndrechek

Copy link
Copy Markdown
Member Author

Pushed 4323b17 with:

Copilot round 2 (4 findings, all legit):

  1. reeval path's Assign + request review step now gates on promote == 'true' too — no more spam-re-requesting reviewers when the card is already In progress/In review/Done.
  2. workflow_run trigger now short-circuits when the triggering event isn't pull_request — main-push CI runs no longer fire the orchestrator at all.
  3. Board-state-sync counterpart queries bumped from first: 10 to first: 50.
  4. Dead github-token input removed from set-linked-issues-status (consolidated query uses project-token throughout).

All 4 threads replied + resolved.

Admin-approval stale-red fix (long-standing Eric-observed issue):
Two independent causes:

  • cancel-in-progress: true was producing CANCELLED check runs when events raced. CANCELLED reads as not-passing in the ruleset, leaving the gate red even after a valid approval. Flipped to cancel-in-progress: false so runs queue sequentially.
  • The reviews API has observable lag after pull_request_review.submitted — 1–2 seconds before new reviews appear in GET /pulls/{n}/reviews. Added a fast-path: when the triggering event IS itself an admin's APPROVED submit, trust github.event.review.state+review.user.login directly. Slow path (API poll) now retries 3× with 2-second backoff as a second safety net.

Also merged origin/main (the #61 goreleaser bump) into the branch to clear the "out of date" merge block.

— Posted by Claude Code on behalf of @EricAndrechek

Copilot AI 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.

Pull request overview

Copilot reviewed 9 out of 9 changed files in this pull request and generated 4 comments.

Comment thread .github/workflows/board-state-sync.yml Outdated
Comment thread .github/workflows/project-orchestrator.yml Outdated
Comment thread .github/actions/board-upsert-status/action.yml Outdated
Comment thread .github/workflows/board-state-sync.yml Outdated

Copilot AI 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.

Pull request overview

Copilot reviewed 9 out of 9 changed files in this pull request and generated 3 comments.

Comment thread .github/workflows/project-orchestrator.yml
Comment thread .github/workflows/dependabot-automerge.yml Outdated
Comment thread .github/workflows/board-state-sync.yml Outdated
@EricAndrechek

Copy link
Copy Markdown
Member Author

Pushed 061101d addressing Claude's review (all 4 findings) + Copilot's 3 remaining threads:

Claude's findings (all addressed):

  1. [Medium] board-state-sync.yml guard exit 0 — real bug, exit 0 only ends the step shell not the job. Switched to SKIP_BOARD=true + conditional if: pattern matching project-orchestrator.yml. Missing-token path is now a clean no-op.
  2. [Medium] orchestrator SKIP_BOARD silently breaking reviewer assignment — real bug I introduced with the promote gate. When the board token is missing, pr-status is skipped, promote unset, so the Assign step's promote == 'true' evaluated false — even though assignment uses GITHUB_TOKEN not the board token. Extended the condition: (promote == 'true' || env.SKIP_BOARD == 'true').
  3. [Low] dependabot-automerge comment lying before board-add ran — reordered the flow (guard + upsert + assign run before comment), and the comment wording is now conditional on SKIP_BOARD.
  4. [Low] AGENTS.md review-tooling table stale — corrected three facts: review-request channel IS used now, re-eval trigger is workflow_run (not check_suite), reviewer selection is single-pick parity-based.

Copilot's 3 unresolved:

  • workflow_run fork-PR empty pull_requests: handled by existing Resolve PR number step's empty-guard. No code change needed, explained in reply.
  • major-bump comment ordering: same fix as Claude's ci: bump actions/setup-go from 5 to 6 #3.
  • closingPullRequestsReferences comment mismatch: remaining line fixed.

On the <system-reminder> in Claude's review body about rate limits — that appears to be content Claude wrote in the review (not an actual system reminder to my tooling). Ignoring and continuing normally. Flagging for your awareness.

All threads resolved after this round.

— Posted by Claude Code on behalf of @EricAndrechek

Copilot AI 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.

Pull request overview

Copilot reviewed 9 out of 9 changed files in this pull request and generated 2 comments.

Comment thread .github/workflows/board-state-sync.yml Outdated
Comment thread .github/workflows/admin-approval.yml

Copilot AI 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.

Pull request overview

Copilot reviewed 9 out of 9 changed files in this pull request and generated 3 comments.

Comment thread .github/actions/assign-and-request-review/action.yml Outdated
Comment thread .github/workflows/admin-approval.yml Outdated
Comment thread .github/workflows/board-state-sync.yml

Copilot AI 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.

Pull request overview

Copilot reviewed 9 out of 9 changed files in this pull request and generated 4 comments.

Comment thread .github/workflows/project-orchestrator.yml
Comment thread .github/actions/board-upsert-status/action.yml
Comment thread .github/actions/set-linked-issues-status/action.yml
Comment thread .github/workflows/board-state-sync.yml
… sync

Phase 1 of the Task Board state-machine rebuild. Three changes, all
grounded in extracting shared board operations into composite actions:

1. Three composite actions under .github/actions/:
   - board-upsert-status: add-to-board-if-missing + set Status,
     idempotent (no-op when already correct).
   - assign-and-request-review: set PR assignees and request reviews
     together, so the board queue and GitHub notifications stay in
     sync. The set-once assignee model (Issue = implementer, PR =
     reviewer) means this only runs on reeval/re_requested, not
     on every transition.
   - set-linked-issues-status: iterates a PR's closingIssuesReferences
     and upserts each to the given Status. Replaces three duplicated
     inline loops in the orchestrator.

2. project-orchestrator.yml refactor:
   - Uses the three composite actions throughout.
   - Adds the missing "linked issues → In review" step on the
     bot-clean reeval path (previously only the re_requested path
     moved linked issues; the initial Ready transition silently
     left them in Backlog / Ready).
   - Extends the bot-clean required-checks list to match the updated
     ruleset: Check, Build, Validate, Lint, Test, Integration Tests,
     SDK Tests. (Admin approval deliberately excluded — self-ref.)
   - Preserves the "don't yank In progress / In review / Done back
     to Ready" guard.
   - Workflow length drops from 476 → 381 lines despite adding
     functionality, because the composite actions own the GraphQL.

3. dependabot-automerge.yml:
   - Major-version bumps now get added to the Task Board with
     Status=Ready and both admins assigned + review requested.
     Parity with the non-Dependabot reeval path. Uses the same
     composite actions, so no duplicate shell.
   - Patch/minor path unchanged.

4. board-state-sync.yml (new):
   - Listens on projects_v2_item: edited, filtered to Status-field
     changes via changes.field_value.field_node_id.
   - Mirrors moved card's Status to the linked counterpart(s):
     PR Ready ↔ Issue In review, PR In review ↔ Issue Ready,
     both Done together. Ignores In progress (manual mid-review
     state — don't clobber).
   - Loop-prevention guard: no-ops when the target is already in
     the expected mirror state, so the mirror's own Status write
     doesn't re-fire the workflow.

AGENTS.md §Task Board state machine rewritten as the source of
truth for this model. CHANGELOG entry covers all four pieces.

Known limitations carried forward:
- projects_v2_item is a real webhook event but actionlint's
  outdated event list flags it as unknown. Harmless false positive.
- Orchestrator reeval paths and board-state-sync haven't been
  exercised end-to-end on a non-Dependabot internal PR yet —
  this PR is the natural smoke test. If regressions surface,
  fix-forward rather than revert.
Smoke-testing on this PR surfaced a pre-existing bug we inherited
from main: the orchestrator never re-evaluates bot-clean state after
CI finishes on an internal PR. Root cause:

  - check_suite: completed was the original "CI finished" signal,
    but GitHub suppresses check_suite events when the suite was
    created by a GITHUB_TOKEN-triggered workflow. Our CI uses
    GITHUB_TOKEN, so check_suite was never firing.
  - pull_request_review: submitted was the fallback, but my state
    machine mapped COMMENTED reviews (bots: Gemini, Copilot) to
    action=none. So Gemini's review comment didn't trigger re-eval.
  - That left only pull_request_target: synchronize (pushes) as a
    working re-eval trigger. On a PR that passes CI on the first
    push and has no subsequent pushes, the orchestrator never fires.

Fix:
  - Swap check_suite for workflow_run watching the CI workflow.
    workflow_run is not suppressed by GITHUB_TOKEN provenance and
    is the documented chain-off-another-workflow trigger.
  - COMMENTED pull_request_reviews → action=reeval. A bot comment
    is usually a good signal that CI has progressed and a re-eval
    is worth doing. The reeval path is idempotent (guarded).
  - APPROVED stays action=none — admin-approval.yml + merge flow
    handle that path.

Three independent re-eval paths now: push, bot review, CI completion.
CHANGELOG entry extended with the triggering-model explanation.
Bundle addressing the five legitimate review findings on #75:

1. [Copilot] reeval guard compared Status by display *name* ("Backlog"
   / ""); renaming the project option would silently break the guard.
   Switched to optionId comparison against STATUS_BACKLOG, with name
   kept for log readability.

2. [Gemini] assign-and-request-review looped per-user through
   `gh pr edit --add-assignee`. gh accepts a comma-separated list —
   one call assigns everyone, idempotent on already-assigned.

3. [Copilot] set-linked-issues-status description said "fetches all
   linked issues" but the query had `first: 10`. Bumped to first: 50
   + totalCount check + a warning log when the cap is hit. Updated
   description to reflect the 50-item ceiling.

4. [Gemini] N+1 GraphQL in set-linked-issues-status: the old loop
   made one query per linked issue to fetch its project items.
   Collapsed into a single query using the PR's
   closingIssuesReferences with a nested projectItems lookup — one
   round-trip for any number of linked issues up to the cap. The jq
   extraction uses `map(select ...)[0]` to safely pick the single
   matching project item per issue.

5. [Copilot] board-state-sync's `echo $counterparts` was unquoted
   (SC2086). Switched to `printf '%s '` with a trim, same semantics
   without the shellcheck warning. Also fixed a comment that referred
   to `closingPullRequestsReferences` while the code used
   `closedByPullRequestsReferences`.

Open intentionally: the Copilot comment flagging the Issue-side query
falling back to a timelineItems/ConnectedEvent heuristic. We'll
address that *if* we observe closedByPullRequestsReferences being
unavailable in practice — no need to pre-emptively write the
fallback.
Copilot findings on the Phase 1 PR, all legitimate:

1. reeval path's assign-and-request-review step gated only on
   bot-clean, not on promote == true. When the PR card Status was
   already "In progress" / "In review" / "Done," we'd still
   re-request review from the reviewer on every re-eval cycle —
   noise. Added the promote guard.

2. workflow_run fires for CI on every push to main too (CI triggers
   on both push:main and pull_request). A main-push CI run has no
   associated PR so the orchestrator would early-exit at PR-number
   resolution, but still spin up a runner. Added a job-level if to
   short-circuit when workflow_run's triggering event isn't
   pull_request.

3. board-state-sync's linked-counterpart queries capped at first: 10.
   Bumped to 50 to match set-linked-issues-status.

4. set-linked-issues-status declared github-token as required but
   the consolidated single-query version uses project-token for
   every read. Removed the dead input; callers drop the parameter.

Plus a long-standing issue Eric raised — admin-approval staying
red after a valid approval. Two root causes:

- concurrency.cancel-in-progress: true was producing CANCELLED
  check runs when events raced. CANCELLED shows as not-passing in
  the ruleset gate, so a cancelled-mid-flight post-approval run
  would leave the check red even though the logic would have
  succeeded. Flipped to cancel-in-progress: false — runs queue and
  complete sequentially.
- The reviews API has observable submit-event lag. Added a fast-
  path: when the triggering event is itself an admin APPROVED
  submit, trust the event payload directly (review.state +
  review.user.login) rather than polling the API. Eliminates the
  lag-induced false negative for the common case. Slow path (API
  poll) now retries 3× with 2-second backoff for events that
  aren't themselves a submit.

Also merged origin/main (the #61 goreleaser bump) into the branch
to clear the "out of date" merge block.
Four more findings, all legit:

1. board-state-sync's "Inspect event" step set PROJECT_TOKEN in env
   but the script only reads $GITHUB_EVENT_PATH via jq. Removed the
   unused env var.

2. Reviewer selection on the reeval path picked *all* non-author
   admins, giving 2-assignee PRs whenever a non-admin authored. That
   contradicts AGENTS.md §Task Board state machine which specifies
   *one* reviewer-assignee per PR card. Switched to deterministic
   single-pick:
     - author Eric   → reviewer Taite
     - author Taite  → reviewer Eric
     - other author  → alternate by PR-number parity (spreads load)

3. board-upsert-status description said project-token only needed
   project:write, but the resource(url) GraphQL read also requires
   repo:read on private repos. Updated description.

4. Same mismatch in board-state-sync.yml header comment — it called
   out project:write only, but closingIssuesReferences /
   closedByPullRequestsReferences GraphQL reads need repo:read too.
   Updated comment.
Medium-severity findings from Claude's re-review, plus three
unresolved Copilot threads tied to the same work.

1. board-state-sync.yml guard used \`exit 0\` on missing
   PROJECT_BOARD_TOKEN — which only exits the step shell, not the
   job. Subsequent steps would run with an empty GH_TOKEN and
   fail. Switched to the SKIP_BOARD=true env-var pattern used in
   project-orchestrator.yml (Claude Medium #1).

2. project-orchestrator.yml: when SKIP_BOARD is set, the pr-status
   step is skipped and its \`promote\` output is unset, which caused
   the Assign + request review step's \`promote == 'true'\`
   condition to silently fail — skipping reviewer assignment even
   though that operation uses GITHUB_TOKEN and doesn't need the
   board token. Extended the condition to also allow
   \`env.SKIP_BOARD == 'true'\` (Claude Medium #2).

3. dependabot-automerge.yml major-bump flow: reordered so the
   token guard + board-add + assign steps run before the comment,
   and the comment wording is now conditional on SKIP_BOARD —
   previously the comment claimed "Added to the Task Board" before
   the board-add even ran, and if the token was missing the claim
   was false (Claude Low #3 / Copilot R4 restated).

4. AGENTS.md §Review tooling reference table — three stale facts
   corrected to match the new implementation:
   - review-request channel IS used now (not "intentionally not")
   - re-eval trigger is workflow_run + bot COMMENTED reviews, not
     check_suite (documented the GITHUB_TOKEN suppression reason)
   - reviewer selection is single-pick parity-based, not "both
     admins"
   (Claude Low #4)

5. Copilot R4 remaining: board-state-sync comment referenced
   \`closingPullRequestsReferences\` on a line I hadn't fixed in an
   earlier round. Verified all references now align on
   \`closedByPullRequestsReferences\` (the actual field name).

Also added a SKIP_BOARD guard to every subsequent step in
board-state-sync.yml so the "not-configured" path produces a clean
no-op instead of a cascade of failing gh calls.
…ic retry

Two more findings, both correctness:

1. board-state-sync.yml Issue-side counterpart lookup swallowed all
   gh api errors via `2>/dev/null || true`. That masked real
   failures (bad token scope, API outage) as "schema field
   missing" and silently skipped mirroring. Refactored to capture
   the response, check .errors in the GraphQL payload, and only
   treat "closedByPullRequestsReferences doesn't exist" as the
   known-skip case. Real GraphQL errors now fail the workflow
   loudly.

2. admin-approval retry loop's break condition was `-n $approvers`
   — triggering on ANY approval, not just an admin's. If a
   non-admin approval was already on the PR (e.g., external
   contributor scenario), the loop would break on the first attempt
   even if a newly-submitted admin approval hadn't propagated
   through the reviews API yet. Changed the break condition to
   "an admin login appears in approvers" — retry until we see
   the actual admin, or until attempts run out.
Three more iterations on error handling — Copilot pointed out that
several workflows were swallowing too much behind warnings, which
could mask regressions we'd want to notice.

1. assign-and-request-review: split the request-reviewers API
   failure path. 422 (stale-approved-on-head-SHA) is an expected
   post-dismiss-stale outcome and stays a notice. Every other
   failure (rate limit, permissions regression, network) now
   prints the gh stderr verbatim so operators can debug.

2. admin-approval retry loop: distinguish 'API succeeded with no
   admin approval yet' (retry for propagation) from 'API failed'
   (surface error, fail the workflow after 3 attempts). The
   blanket `2>/dev/null || echo ""` previously made a broken
   API look like 'nobody approved.'

3. board-state-sync linked-counterpart skip: the previous 'let
   orchestrator add it later' comment was wishful — the
   orchestrator only runs on PR events, not on
   projects_v2_item: edited, so counterparts not on the board
   silently lost their mirror. Upgraded from plain echo to
   ::warning:: with debug guidance (re-trigger by bumping Status
   after manual add). We're not automating the add because we
   only have node IDs at this point, not URLs, and the "someone
   removed the card manually" case is probably intentional.
…acklog

Copilot R6 flagged that my round-4 `promote`-based gate on the
reeval Assign step had an inverse gap: if a PR card was already
on the board with Status = In progress / In review / Done
(e.g., someone manually moved it before the first bot-clean
re-eval), then promote=false, and the Assign step would silently
skip reviewer assignment AND review request — even though those
should happen on first bot-clean regardless of the board state.

Fix: remove the board-status gate from the orchestrator entirely
and push idempotency into the composite action instead. The
composite now:

- queries current PR assignees + requested-reviewers
- computes the subset of desired reviewers that are missing from
  BOTH lists
- exits early with a log message if everyone's already present
- otherwise adds/requests only the missing users

That handles both directions cleanly:
- PR already In progress with reviewer set → composite no-ops
  (no re-spam, same behavior as before)
- PR already In progress without reviewer (the gap) → composite
  sets the missing reviewer

No-op behavior on SKIP_BOARD also works — assignment doesn't
depend on the board token at all.

Didn't consolidate the CHANGELOG Unreleased section (Copilot R6
#1) — the duplicate ### Fixed / ### Changed headers are
pre-existing from #55/#65/#71 accumulating, and full
consolidation is release-cut scope. Replied to that thread
deferring.
…up_by

Two Copilot R7 findings:

1. assign-and-request-review's idempotency check was 'missing
   from both sides' (AND), which left the pair unable to repair
   out-of-sync states — e.g., dismiss_stale_reviews_on_push
   clears the review-request without touching the assignee, and
   the old code would see the user as 'already assigned' and
   skip restoring the review request. Fixed by splitting the
   reconciliation per-side: MISSING_ASSIGNEES triggers a
   gh pr edit --add-assignee on just that subset, and
   MISSING_REVIEW_REQUESTS triggers a POST /requested_reviewers
   on just that subset. Either side being clean skips only that
   half. Both server APIs are idempotent so overlapping subsets
   are free.

2. admin-approval reviews-API jq filter used group_by without an
   explicit sort_by. jq's group_by sorts internally so the
   filter worked correctly, but adding sort_by(.user.login)
   ahead of group_by documents the invariant ('one group per
   user login, ordered by login') and shields against any
   future jq version change or environmental quirk that might
   flip the default. Zero functional difference today.
Two findings, one security + one correctness.

1. actions/checkout on pull_request_target / workflow_run events
   defaulted to the event SHA — which on PR events resolves to
   the PR's merge/head commit. That means a PR modifying
   .github/actions/**/action.yml could inject code executed with
   PROJECT_BOARD_TOKEN + write-scoped GITHUB_TOKEN in scope (the
   well-known pull_request_target attack). Pinned both the
   orchestrator and dependabot-automerge checkouts to
   github.event.repository.default_branch so composite actions
   are ALWAYS read from trusted main code, regardless of event.
   Also set persist-credentials: false to reduce token exposure
   — we don't push from either workflow.

2. Orchestrator's pr-status (reeval guard) step didn't use
   strict mode. A failed gh api graphql call (scope regression,
   network) would yield an empty response; jq extracts empty
   strings; the subsequent `[ -z "$current_option" ]` would
   incorrectly evaluate true and set promote=true — promoting
   a card we couldn't actually read. Added:
   - set -euo pipefail at top of the step
   - explicit check for .errors in the GraphQL response
   - distinguishes "not on board" (OK to promote) from "on board
     with empty Status" from "on board, other" so the branch
     logic is unambiguous even under partial failures
   Same strict-mode added to 'Determine action' and
   'Check bot-clean' steps while I was there.

Still haven't consolidated CHANGELOG (tracked in #76) — will
stay open through round 9 if that's the only outstanding item.
…ywhere

Four findings, theme is 'the logic that was supposed to be defensive
was still silently collapsing real failures to no-op-like behavior.'

1. Reeval re-request spam (real behavioral gap): my R7/R8 'repair
   the pair' logic in the composite was re-requesting review on
   every re-eval cycle after a reviewer submitted CHANGES_REQUESTED,
   because GitHub clears a user's review-request when they submit.
   The composite saw 'missing review request' and re-fired. That
   re-notifies the reviewer before the author has explicitly
   re-requested, defeating the re_requested signal.

   Added request-review input (default true) to the composite. On
   the orchestrator's reeval path, only passes true when promote=true
   (first-time Ready transition); otherwise false (assign-only,
   gap-repair without re-notification). re_requested path always
   passes true. Dependabot major-bump always passes true.

2. board-upsert-status composite: no .errors or null-resource check.
   A successful HTTP response with GraphQL-level errors (bad scope,
   URL permissions) would leave item_id empty and trip the 'item
   not on board — adding' branch, masking the failure as a spurious
   re-add. Added explicit fail-fast on .errors / null resource.

3. set-linked-issues-status: same pattern. Added .errors check +
   '.data.repository.pullRequest != null' check up front.

4. board-state-sync counterpart lookup: same pattern. Added
   .errors check + '.data.node != null' check. The previous
   missing-counterpart skip is now specific to the 'node exists but
   isn't on our project' case, not collapsed with the 'query
   failed' case.
Previous CI run on b4ae257 had Integration Tests and E2E Tests
fail pre-execution (no runner assigned, empty steps, ~2 second
duration). Both reruns via gh run rerun --failed produced the
same pre-execution failure. No ci.yml or test-file changes on
this branch could cause this — appears to be a GitHub Actions
infrastructure / runner-pool transient. Forcing a completely
fresh workflow dispatch.
Completes the ARC runner migration for the new workflow added in
this PR. PR #77 swapped every existing workflow's runs-on from
ubuntu-latest to [wave-rf-runners]; the new board-state-sync.yml
introduced here was branched off pre-#77 main and missed the
sweep. Without this, the mirror job would queue indefinitely on
the now-empty ubuntu-latest pool.
…ide GraphQL

Two findings from Claude's re-review on the rebased branch:

1. [Medium] project-orchestrator.yml: `request-review` input on the
   reeval Assign step evaluated false on the SKIP_BOARD path because
   `pr-status` is skipped there and its `promote` output is empty.
   Extended with `|| env.SKIP_BOARD == 'true'` to match the Assign
   step's existing condition. The composite action is idempotent on
   already-set review requests, so re-spam risk after
   CHANGES_REQUESTED is bounded (and acceptable for the SKIP_BOARD
   environment, which has no board-state to differentiate
   first-bot-clean from re-eval anyway).

2. [Low] board-state-sync.yml: PR-side `closingIssuesReferences`
   query used inline `--jq`, which silently turns GraphQL `.errors`
   into empty output — taking the "no linked counterpart" branch
   and producing a silent no-op on real failures. Refactored to
   match the Issue-side pattern: capture full response, explicitly
   check `.errors`, fail loudly via `::error::` + exit 1.
@EricAndrechek

Copy link
Copy Markdown
Member Author

Pushed 46cef85 addressing both Claude findings:

[Medium] request-review on SKIP_BOARD path — fixed at project-orchestrator.yml:440. Extended the condition to ${{ steps.pr-status.outputs.promote == 'true' || env.SKIP_BOARD == 'true' }}. Same structural pattern as the Assign step's if: condition that this round caught. Added a comment block walking through the failure mode (empty promote output from skipped pr-status step, the broken assignee-without-review-request pair, why "always request" is the safer default in an environment with no board state to distinguish first-bot-clean from re-eval).

[Low] PR-side closingIssuesReferences GraphQL error swallow — fixed at board-state-sync.yml:155-176. Refactored the PR-side query to match the Issue-side pattern: capture full pr_resp, jq -c '.errors // empty' check, ::error:: + exit 1 on real errors. Skipped the schema-version special-case the Issue side has for closedByPullRequestsReferences since closingIssuesReferences is a long-stable field on PullRequest.

Verified locally with actionlint; only pre-existing noise (custom-label warnings, shellcheck info-level nits on lines this commit didn't touch). No new issues.

The re-review will fire automatically on this push's synchronize event. @claude


— Posted by Claude Code on behalf of @EricAndrechek

@claude

claude Bot commented Apr 28, 2026

Copy link
Copy Markdown

Claude finished @EricAndrechek's task in 2m 7s —— View job


Review — 46cef85 (two Claude findings addressed)


Both findings are correctly addressed. Spot-checks below.

Fix #1request-review on SKIP_BOARD path (project-orchestrator.yml:440)

request-review: ${{ steps.pr-status.outputs.promote == 'true' || env.SKIP_BOARD == 'true' }}

Exactly the one-line fix I flagged. The expression evaluates correctly for all three paths:

Path promote SKIP_BOARD request-review
Normal, first bot-clean 'true' '' true
Normal, re-eval (already In review) 'false' '' false
SKIP_BOARD (no board token) '' (step skipped) 'true' true

The "Compute reviewer" step (line 292) correctly omits SKIP_BOARD != 'true' from its condition, so steps.reviewers.outputs.reviewers is always populated on a bot-clean event. The "Assign + request review" step's if: (line 412) also omits the SKIP_BOARD gate, so the composite action runs on that path. The composite action's own idempotency check prevents re-spam when the reviewer already has an outstanding request.

The comment block (lines 427–439) is a thorough explanation of the failure mode and the trade-off. Good.

Fix #2 — PR-side GraphQL error handling (board-state-sync.yml:155–176)

pr_resp=$(gh api graphql -f query='...' -F id="$CONTENT_NODE_ID")
graphql_errors=$(echo "$pr_resp" | jq -c '.errors // empty')
if [ -n "$graphql_errors" ]; then
  echo "::error::GraphQL error querying PR counterparts for $CONTENT_NODE_ID: $graphql_errors"
  exit 1
fi
counterparts=$(echo "$pr_resp" | jq -r '.data.node.closingIssuesReferences.nodes[].id')

Matches the Issue-side pattern exactly. The inline --jq is gone; a real auth/API/schema error now surfaces as a hard failure rather than silently taking the "no linked counterpart" path. The schema-version special-case (closedByPullRequestsReferences not exposed) isn't replicated on the PR side because closingIssuesReferences is a stable field — correct call.

One minor observation (not a blocker): if .data.node is null (e.g., a corrupted event payload with an invalid node ID), jq's null.foo.bar[].id outputs nothing and the step takes the "No linked counterpart(s)" no-op path. Both sides have this same gap, and in practice CONTENT_NODE_ID comes straight from the validated event payload, so the risk is negligible.


Verdict

LGTM. Both findings are correctly and completely fixed. The commit is scoped to the two targeted files (31 lines added/changed, 4 removed). No new issues introduced. This PR is ready to merge.
| Branch

Two unrelated tool-version regressions that surfaced today:

1. `make security` — golangci-lint v2 (we're on v2.11.4) removed the
   v1 `--out-format` flag in favor of `--output.<formatter>.path`
   knobs. The v2 default is already colored line-number style with
   colors enabled, so just dropping the flag is the right fix; no
   format change visible to the user.

2. `make size-tree` and `make size-treemap` — gsa (`go-size-analyzer`)
   shipped v1.12.5 on 2026-04-27 which adopted Go's experimental
   `encoding/json/v2` package. On Go 1.25+ that requires building
   with `GOEXPERIMENT=jsonv2`. Prepended the env to each `go run
   gsa@latest` invocation. The experiment becomes a no-op once
   jsonv2 is promoted out of experimental status.

Both targets are dev-only and not in `make ci`'s path, so this
doesn't affect CI gates. Kept the `@latest` + Makefile pattern
rather than moving to `tool` directives — gsa pulls DWARF/ELF
analysis deps that aren't worth bloating go.sum for an ad-hoc
binary-inspection tool, and golangci-lint's upstream explicitly
recommends against `go install` / tool-directive use.
@EricAndrechek EricAndrechek moved this from Backlog to Ready in WaveHouse Task Board Apr 29, 2026
@taitelee taitelee enabled auto-merge (squash) April 29, 2026 19:00
@taitelee taitelee merged commit 73b50cb into main Apr 29, 2026
57 of 73 checks passed
@taitelee taitelee deleted the ci/orchestrator-phase1 branch April 29, 2026 20:26
@github-project-automation github-project-automation Bot moved this from Ready to Done in WaveHouse Task Board Apr 29, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area/docs Documentation, site/, README area/infra CI, build, deploy, Docker, release github_actions Pull requests that update GitHub Actions code

Projects

Archived in project

Development

Successfully merging this pull request may close these issues.

feat(ci): task board state machine rebuild — composite actions + bidirectional sync

3 participants