ci: auto-unblock issues on blocker-close — flip Blocked → Todo when all blockers resolve - #576
Conversation
|
Warning Rate limit exceeded
You’ve run out of usage credits. Purchase more in the billing tab. ⌛ How to resolve this issue?After the wait time has elapsed, a review can be triggered using the We recommend that you space out your commits to avoid hitting the rate limit. 🚦 How do rate limits work?CodeRabbit enforces hourly rate limits for each developer per organization. Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout. Please see our FAQ for further information. ℹ️ Review info⚙️ Run configurationConfiguration used: Path: .coderabbit.yaml Review profile: CHILL Plan: Pro Run ID: 📒 Files selected for processing (2)
✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
Reviewer's GuideAdds a GitHub Actions workflow that, when an issue is closed, scans open issues for formal blocker references to it and automatically flips their project status from Blocked to Todo (or posts status comments) plus a Python replay script that dry-runs the same logic against live GitHub data for regression/debugging. Sequence diagram for auto-unblock workflow on issue closesequenceDiagram
actor Developer
participant GitHub
participant AutoUnblockWorkflow as auto_unblock_on_blocker_close
participant GH_CLI as gh_cli_on_runner
participant GitHub_REST as github_rest_api
participant GitHub_GQL as github_graphql_api
Developer->>GitHub: Close blocker issue
GitHub-->>AutoUnblockWorkflow: issues.closed event
AutoUnblockWorkflow->>GH_CLI: issue list (open issues, number, body)
GH_CLI-->>GitHub_REST: GET issues list
GitHub_REST-->>GH_CLI: Open issues JSON
GH_CLI-->>AutoUnblockWorkflow: Encoded open issues
loop For each open issue
AutoUnblockWorkflow->>AutoUnblockWorkflow: Check body for blocker reference to closed issue
alt Body references closed issue as blocker
AutoUnblockWorkflow->>AutoUnblockWorkflow: Parse all blocker numbers via regex
loop For each blocker
AutoUnblockWorkflow->>GH_CLI: issue view blocker
GH_CLI-->>GitHub_REST: GET issue state
GitHub_REST-->>GH_CLI: state, closedAt
GH_CLI-->>AutoUnblockWorkflow: blocker state
end
alt Any blocker not CLOSED
AutoUnblockWorkflow->>GH_CLI: issue view comments (idempotency marker check)
GH_CLI-->>GitHub_REST: GET comments
GitHub_REST-->>GH_CLI: comments
GH_CLI-->>AutoUnblockWorkflow: comments JSON
alt Marker not found
AutoUnblockWorkflow->>GH_CLI: issue comment partial unblock status
GH_CLI-->>GitHub_REST: POST comment
GitHub_REST-->>GH_CLI: Comment created
else Marker found
AutoUnblockWorkflow->>AutoUnblockWorkflow: Skip duplicate comment
end
else All blockers CLOSED
AutoUnblockWorkflow->>GitHub_GQL: query issue projectItems and Status value
GitHub_GQL-->>AutoUnblockWorkflow: project item id and status optionId
alt Status optionId is Blocked
alt PROJECT_TOKEN set and project item present
AutoUnblockWorkflow->>GitHub_GQL: mutation updateProjectV2ItemFieldValue to Todo (GH_TOKEN=PROJECT_TOKEN)
GitHub_GQL-->>AutoUnblockWorkflow: mutation result
alt Mutation succeeded
AutoUnblockWorkflow->>GH_CLI: issue view comments (idempotency marker check)
GH_CLI-->>GitHub_REST: GET comments
GitHub_REST-->>GH_CLI: comments
GH_CLI-->>AutoUnblockWorkflow: comments JSON
alt Marker not found
AutoUnblockWorkflow->>GH_CLI: issue comment auto unblock with flip confirmation
GH_CLI-->>GitHub_REST: POST comment
GitHub_REST-->>GH_CLI: Comment created
else Marker found
AutoUnblockWorkflow->>AutoUnblockWorkflow: Skip duplicate comment
end
else Mutation failed
AutoUnblockWorkflow->>AutoUnblockWorkflow: Flip failed, switch to comment only
AutoUnblockWorkflow->>GH_CLI: issue view comments (marker check)
GH_CLI-->>GitHub_REST: GET comments
GitHub_REST-->>GH_CLI: comments
GH_CLI-->>AutoUnblockWorkflow: comments JSON
alt Marker not found
AutoUnblockWorkflow->>GH_CLI: issue comment auto unblock with manual flip request
GH_CLI-->>GitHub_REST: POST comment
GitHub_REST-->>GH_CLI: Comment created
else Marker found
AutoUnblockWorkflow->>AutoUnblockWorkflow: Skip duplicate comment
end
end
else PROJECT_TOKEN missing or no project item
AutoUnblockWorkflow->>AutoUnblockWorkflow: Cannot flip, comment only mode
AutoUnblockWorkflow->>GH_CLI: issue view comments (marker check)
GH_CLI-->>GitHub_REST: GET comments
GitHub_REST-->>GH_CLI: comments
GH_CLI-->>AutoUnblockWorkflow: comments JSON
alt Marker not found
AutoUnblockWorkflow->>GH_CLI: issue comment auto unblock with manual flip request
GH_CLI-->>GitHub_REST: POST comment
GitHub_REST-->>GH_CLI: Comment created
else Marker found
AutoUnblockWorkflow->>AutoUnblockWorkflow: Skip duplicate comment
end
end
else Status not Blocked
AutoUnblockWorkflow->>AutoUnblockWorkflow: Skip flip and comment
end
end
else No blocker reference
AutoUnblockWorkflow->>AutoUnblockWorkflow: Skip issue
end
end
AutoUnblockWorkflow-->>GitHub: Log auto unblock scan complete
Flow diagram for per-issue auto-unblock decision logicflowchart TD
Start["Start scan when blocker issue closes"] --> ListOpen["List up to 1000 open issues via gh issue list"]
ListOpen --> AnyOpen{Any open issues?}
AnyOpen -- No --> End["Exit: nothing to do"]
AnyOpen -- Yes --> ForEachIssue["For each open issue"]
ForEachIssue --> CheckBody["Does body contain formal blocker reference to closed issue?"]
CheckBody -- No --> NextIssue["Skip to next issue"]
NextIssue --> ForEachIssue
CheckBody -- Yes --> ParseBlockers["Parse all blocker issue numbers with regex"]
ParseBlockers --> CheckStates["For each blocker: fetch state via gh issue view"]
CheckStates --> AnyOpenBlocker{Any blocker not CLOSED?}
AnyOpenBlocker -- Yes --> MarkerCheckPartial["Check for marker comment <!-- auto-unblock:#n -->"]
MarkerCheckPartial --> MarkerExistsPartial{Marker already present?}
MarkerExistsPartial -- Yes --> NextIssue
MarkerExistsPartial -- No --> CommentPartial["Post partial unblock comment listing open blockers"]
CommentPartial --> NextIssue
AnyOpenBlocker -- No --> QueryProject["Query project item and Status via GraphQL"]
QueryProject --> StatusBlocked{Status optionId == Blocked?}
StatusBlocked -- No --> NextIssue
StatusBlocked -- Yes --> CanFlip{PROJECT_TOKEN set and project item id found?}
CanFlip -- No --> MarkerCheckFallback["Check for marker comment"]
MarkerCheckFallback --> MarkerExistsFallback{Marker already present?}
MarkerExistsFallback -- Yes --> NextIssue
MarkerExistsFallback -- No --> CommentFallback["Post auto unblock comment requesting manual Blocked to Todo flip"]
CommentFallback --> NextIssue
CanFlip -- Yes --> Mutation["Call updateProjectV2ItemFieldValue to set Status Todo"]
Mutation --> MutationOk{Mutation succeeded?}
MutationOk -- No --> MarkerCheckFallback
MutationOk -- Yes --> MarkerCheckFlip["Check for marker comment"]
MarkerCheckFlip --> MarkerExistsFlip{Marker already present?}
MarkerExistsFlip -- Yes --> NextIssue
MarkerExistsFlip -- No --> CommentFlip["Post auto unblock comment confirming Blocked to Todo flip"]
CommentFlip --> NextIssue
ForEachIssue -->|All issues processed| Done["auto-unblock scan complete"]
Done --> End
File-Level Changes
Assessment against linked issues
Possibly linked issues
Tips and commandsInteracting with Sourcery
Customizing Your ExperienceAccess your dashboard to:
Getting Help
|
| - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1 | ||
| with: | ||
| fetch-depth: 1 |
There was a problem hiding this comment.
Hey - I've found 1 security issue, 2 other issues, and left some high level feedback:
Security issues:
- Detected subprocess function 'run' without a static string. If this data can be controlled by a malicious actor, it may be an instance of command injection. Audit the use of this call to ensure it is not controllable by an external resource. You may consider using 'shlex.escape()'. (link)
General comments:
- In the replay script, the
--status-option-blockedargument is parsed but never used; either wire it into the logic or remove the option to avoid confusion. - The workflow’s GraphQL query hard-codes
repository(owner:"robotrocketscience", name:"aelfrice")while other calls use theREPOenv; consider deriving the owner/name fromREPOto keep behavior consistent if the workflow is ever reused or the repo is renamed.
Prompt for AI Agents
Please address the comments from this code review:
## Overall Comments
- In the replay script, the `--status-option-blocked` argument is parsed but never used; either wire it into the logic or remove the option to avoid confusion.
- The workflow’s GraphQL query hard-codes `repository(owner:"robotrocketscience", name:"aelfrice")` while other calls use the `REPO` env; consider deriving the owner/name from `REPO` to keep behavior consistent if the workflow is ever reused or the repo is renamed.
## Individual Comments
### Comment 1
<location path="scripts/replay-auto-unblock.py" line_range="93-102" />
<code_context>
+ epilog=__doc__,
+ )
+ parser.add_argument("issue", type=int, help="Issue number to check.")
+ parser.add_argument(
+ "--simulate-closed",
+ dest="simulate_closed",
+ type=int,
+ required=True,
+ help="Issue number to treat as CLOSED for this replay.",
+ )
+ parser.add_argument(
+ "--body",
+ default=None,
+ help=(
+ "Override body text (supports \\n escapes). "
+ "If omitted, the live GitHub body is fetched."
+ ),
+ )
+ parser.add_argument(
+ "--repo",
+ default=REPO_DEFAULT,
+ help=f"GitHub repository (default: {REPO_DEFAULT}).",
+ )
+ parser.add_argument(
+ "--status-option-blocked",
+ default="3bc23bae",
+ help="optionId representing Blocked on the project board (default: 3bc23bae).",
</code_context>
<issue_to_address>
**issue:** `--status-option-blocked` argument is parsed but never used in the replay logic.
Since this flag is never used in `main()`, it acts as dead configuration and may mislead users into expecting status-ID validation that doesn’t exist. Please either hook it into the replay behavior (e.g., printing or simulating the expected status check) or remove it to avoid unused options.
</issue_to_address>
### Comment 2
<location path="scripts/replay-auto-unblock.py" line_range="145-50" />
<code_context>
+ print()
+
+ # ── 2. Check if body references the simulated-closed issue as a blocker
+ trigger_pattern = re.compile(
+ rf"(?:blocked[-\s]by:?\s*|blocked\s+by:?\s*|depends\s+on:?\s*|gate:?\s*)"
+ rf"#{re.escape(str(closed_num))}\b",
+ re.IGNORECASE,
+ )
+ triggers = trigger_pattern.findall(body)
</code_context>
<issue_to_address>
**suggestion:** Trigger regex is redefined instead of reusing the global blocker pattern, increasing divergence risk.
Since this regex is manually re-stated and only `closed_num` varies, future syntax changes could cause it to fall out of sync with `BLOCKER_PATTERN`. Consider constructing this pattern from the same building blocks as `BLOCKER_PATTERN` (e.g., shared prefix plus parameterized issue number) so the replay script behavior automatically tracks workflow changes.
Suggested implementation:
```python
# ── 2. Check if body references the simulated-closed issue as a blocker
# Reuse the shared BLOCKER_PATTERN so this logic stays in sync with the
# main workflow's blocker syntax. Filter matches to only those that
# reference the simulated-closed issue number.
all_blocker_matches = [m.group(0) for m in BLOCKER_PATTERN.finditer(body)]
triggers = [match for match in all_blocker_matches if f"#{closed_num}" in match]
```
This change assumes:
1. `BLOCKER_PATTERN` is already defined in this module (or imported) and matches the same blocker syntax used by the workflow.
2. `BLOCKER_PATTERN` is compiled with appropriate flags (e.g., `re.IGNORECASE`) as needed.
If `BLOCKER_PATTERN` is not currently available in this file, you should:
- Import it from the module where it is defined, or
- Move its definition to a shared location and import it here.
If `BLOCKER_PATTERN`'s matches do not always contain the full `#<number>` text, you may instead want to filter using a capturing group (e.g., `m.group("issue") == str(closed_num)`), adjusting the filter expression to match the actual group name or index in the existing pattern.
</issue_to_address>
### Comment 3
<location path="scripts/replay-auto-unblock.py" line_range="55-59" />
<code_context>
result = subprocess.run(
["gh"] + args,
capture_output=True,
text=True,
)
</code_context>
<issue_to_address>
**security (python.lang.security.audit.dangerous-subprocess-use-audit):** Detected subprocess function 'run' without a static string. If this data can be controlled by a malicious actor, it may be an instance of command injection. Audit the use of this call to ensure it is not controllable by an external resource. You may consider using 'shlex.escape()'.
*Source: opengrep*
</issue_to_address>Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.
| parser.add_argument( | ||
| "--simulate-closed", | ||
| dest="simulate_closed", | ||
| type=int, | ||
| required=True, | ||
| help="Issue number to treat as CLOSED for this replay.", | ||
| ) | ||
| parser.add_argument( | ||
| "--body", | ||
| default=None, |
There was a problem hiding this comment.
issue: --status-option-blocked argument is parsed but never used in the replay logic.
Since this flag is never used in main(), it acts as dead configuration and may mislead users into expecting status-ID validation that doesn’t exist. Please either hook it into the replay behavior (e.g., printing or simulating the expected status check) or remove it to avoid unused options.
| # Same pattern set as the workflow (case-insensitive, word-boundary on issue number). | ||
| BLOCKER_PATTERN = re.compile( | ||
| r"(?:blocked[-\s]by:?\s*|blocked\s+by:?\s*|depends\s+on:?\s*|gate:?\s*)#(\d+)\b", | ||
| re.IGNORECASE, |
There was a problem hiding this comment.
suggestion: Trigger regex is redefined instead of reusing the global blocker pattern, increasing divergence risk.
Since this regex is manually re-stated and only closed_num varies, future syntax changes could cause it to fall out of sync with BLOCKER_PATTERN. Consider constructing this pattern from the same building blocks as BLOCKER_PATTERN (e.g., shared prefix plus parameterized issue number) so the replay script behavior automatically tracks workflow changes.
Suggested implementation:
# ── 2. Check if body references the simulated-closed issue as a blocker
# Reuse the shared BLOCKER_PATTERN so this logic stays in sync with the
# main workflow's blocker syntax. Filter matches to only those that
# reference the simulated-closed issue number.
all_blocker_matches = [m.group(0) for m in BLOCKER_PATTERN.finditer(body)]
triggers = [match for match in all_blocker_matches if f"#{closed_num}" in match]This change assumes:
BLOCKER_PATTERNis already defined in this module (or imported) and matches the same blocker syntax used by the workflow.BLOCKER_PATTERNis compiled with appropriate flags (e.g.,re.IGNORECASE) as needed.
If BLOCKER_PATTERN is not currently available in this file, you should:
- Import it from the module where it is defined, or
- Move its definition to a shared location and import it here.
If BLOCKER_PATTERN's matches do not always contain the full #<number> text, you may instead want to filter using a capturing group (e.g., m.group("issue") == str(closed_num)), adjusting the filter expression to match the actual group name or index in the existing pattern.
| result = subprocess.run( | ||
| ["gh"] + args, | ||
| capture_output=True, | ||
| text=True, | ||
| ) |
There was a problem hiding this comment.
security (python.lang.security.audit.dangerous-subprocess-use-audit): Detected subprocess function 'run' without a static string. If this data can be controlled by a malicious actor, it may be an instance of command injection. Audit the use of this call to ensure it is not controllable by an external resource. You may consider using 'shlex.escape()'.
Source: opengrep
|
This PR is now behind Auto-rebase was removed because the bot has no signing key; rebasing as the bot strips author signatures and the |
|
[claim:review:einstein:2026-05-10T06:31:22Z] |
|
[claim:review:leibniz:2026-05-10T06:31:40Z] |
|
[release:review:leibniz:2026-05-10T06:31:45Z] |
df294eb to
1cb805a
Compare
…lose (#570) Triggers on issues.closed. Searches all open issues for formal blocker references (Blocked-by, Blocked by, Depends on, gate — case-insensitive, word-boundary on issue number). For each matched issue: if all referenced blockers are now CLOSED and board status is Blocked, flips status to Todo and posts a confirmation comment. If any blocker remains open, posts a one-line status comment. Falls back to comment-only when PROJECT_TOKEN is absent or the GraphQL mutation fails, pinging @robotrocketscience. Idempotent via hidden comment marker <!-- auto-unblock:#N -->.
…ecision Runnable as: uv run python scripts/replay-auto-unblock.py <issue> --simulate-closed <N> Fetches the issue body from GitHub (or accepts --body override), parses all formal blocker references via the same regex as the workflow, checks each blocker's live state (overriding the simulated-closed issue to CLOSED), and prints the parsed blocker list plus the would-be flip decision without mutating anything. Also demonstrates the \b word-boundary guard that prevents #1542 from matching when #154 closes.
…ation header gh api always sets its own Authorization header from GH_TOKEN, so passing --header "Authorization: bearer ..." produces two Authorization headers (behavior undefined; gh's wins in practice). Swap PROJECT_TOKEN by overriding GH_TOKEN for the mutation subprocess, and drop the redundant header on the read query.
1cb805a to
40bb5c6
Compare
|
[release:review:einstein:2026-05-10T06:36:59Z] |
Summary
Closes #570.
Adds
.github/workflows/auto-unblock-on-blocker-close.yml: triggered onissues.closed, finds every open issue whose body references the closing issue as a blocker (Blocked-by / Blocked by / Depends on / gate), and for each:Blocked→ flip statusBlocked → Todoand post a confirmation comment.A hidden marker
<!-- auto-unblock:#<closed-issue> -->makes the comment idempotent across re-runs.Auth — operator action required
Project-board mutations require a PAT with
Projects: writescope, stored as repo secretPROJECT_TOKEN. Issue comments use the defaultGITHUB_TOKEN.If
PROJECT_TOKENis unset OR the GraphQL mutation fails for any reason, the workflow falls back to comment-only mode: it still posts the confirmation comment (mentioning@robotrocketscience) and exits 0. No silent failures.The auth swap uses
GH_TOKEN="$PROJECT_TOKEN" gh api graphql ...rather than--header "Authorization: bearer ..."—gh apialways sets its own Authorization header fromGH_TOKEN, so passing a custom header produces two Authorization headers and the gh-internal one wins. Env override is the canonical fix.Project-board constants
Hard-coded in the workflow (per the data already gathered for the repo's user-owned project,
aelfrice v2.1):PROJECT_ID = PVT_kwHOEHqEMc4BWDLCSTATUS_FIELD_ID = PVTSSF_lAHOEHqEMc4BWDLCzhRaei8STATUS_OPTION_TODO = f9451636STATUS_OPTION_BLOCKED = 3bc23baeReplay script
scripts/replay-auto-unblock.pyexercises the same regex + decision logic without mutating anything. Output for the spec's regression case (#154 + #437):Note: the replay uses a
--bodyoverride because issue #154's real body uses informal prose ("remains pending the calibrated #437 reproducibility harness…") rather than the formalBlocked-by:/Depends on:syntax the workflow parses. The script is honest about this — under #154's real body it reports "no formal blocker reference found" and exits without claiming a flip would have happened. The flip behavior is correct when bodies use the formal syntax (which the spec mandates and #569's audit workflow will encourage going forward).Acceptance map
.github/workflows/auto-unblock-on-blocker-close.yml, triggers onissues.closed.updateProjectV2ItemFieldValuemutation.benchmarks/results/v2.0.0.jsonis canonical,uv sync && aelf bench all#437 →WOULD FLIP.<!-- auto-unblock:#<n> -->skips repeat comments; status check skips flip when not currentlyBlocked.Todo/In Progress/Done.\bboundary prevents#1542matching#154.Test plan
python3 -c "import yaml; yaml.safe_load(open('.github/workflows/auto-unblock-on-blocker-close.yml'))"— parses cleanuv run python scripts/replay-auto-unblock.py 154 --simulate-closed 437 --body 'Blocked-by: #307\nDepends on: #437'— would-flipuv run pytest -x -q— 3144 passed, 49 skippedgithub/main— emptyPROJECT_TOKENrepo secret (fine-grained PAT,Projects: write) before relying on automatic flips. Until then the workflow comment-pings instead of flipping.Summary by Sourcery
Add an automation workflow that updates project-board status and comments on dependent issues when a blocking issue is closed, plus a dry-run script to replay and validate this logic against live GitHub data.
New Features:
Enhancements:
Tests: