You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
hiero-sdk-python currently has no automated review workflow. Maintainers manually track which PRs need attention and on which stage of review it is present. This creates several problems:
Maintainers have to manually edit the lablels of the PR, to tell the stage of the PR it currently is. They need to open the PR, and check its state manually.
The existing workflow copies all labels from linked issues onto PRs, cluttering the PR label view with difficulty/scope labels that must belong on issues only
With limited junior committers, committers, and maintainers, PRs can sit unreviewed indefinitely with no escalation path
The review process needs to reflect the Python SDK's role hierarchy (junior committer -> committer -> maintainer) and OpenSSF requirements (2 write-permission approvals before merge)
Solution
Overview
A stateless cron-based workflow that runs every 30 minutes, scans all open PRs, and keeps exactly one review queue label accurate on each PR based on live review data. The bot reflects state, GitHub
branch protection enforces it.
Mental model:
GitHub branch protection = judge (decides if merge is allowed)
addAssignees = sends real persistent notifications to reviewers
Required Branch Protection Settings
These must be configured before the bot is useful:
Required approving reviews: 2
Restrict to write/admin collaborators only
Require review from Code Owners: enabled
Dismiss stale reviews when new commits pushed: enabled
The bot never reads or parses the CODEOWNERS file. Code owner enforcement is fully delegated to branch protection.
Role Hierarchy
Role
GitHub permission
Job
Junior committer
read
Soft quality check, does PR meet requirements? Reviews beginner/GFI PRs
Committer
write
First formal technical review
Maintainer
write + code owner
Final approval and merge decision
Junior committers are the triage function. No separate triage team.
The bot uses addAssignees instead, which only requires write access from the calling token (the cron's GITHUB_TOKEN). This sends a real persistent GitHub notification and shows up in the assignee's dashboard.
Label Set: 3 Labels Only On PRs
Label
Meaning
queue:junior-committer
Waiting for junior committer soft check
queue:committers
Junior check done, waiting for committer formal review
ready-to-merge
2 write approvals secured, waiting for maintainer merge
Only one label active per PR at a time. Never zero.
Issue labels (skill: beginner, skill: intermediate, skill: advanced, Good First Issue) stay on issues only.
The existing label-copy-from-issue workflow must be disabled
as part of this implementation.
Full Flow Diagram
flowchart TD
classDef trigger fill:#e1bee7,stroke:#8e24aa,color:#000
classDef bot fill:#c8e6c9,stroke:#388e3c,color:#000
classDef human fill:#bbdefb,stroke:#1976d2,color:#000
classDef gate fill:#ffecb3,stroke:#ffa000,color:#000
classDef done fill:#b2dfdb,stroke:#00796b,color:#000
classDef skip fill:#f5f5f5,stroke:#9e9e9e,color:#555
CRON([🕐 Cron fires every 30 min]):::trigger
RATE{Rate limit\n>= 200 remaining?}:::gate
SKIP_RUN([Skip run, log warning]):::skip
FETCH[Fetch all open non-draft PRs]:::bot
CRON --> RATE
RATE -- No --> SKIP_RUN
RATE -- Yes --> FETCH
FETCH --> WORKLOAD[Compute team workload\nfrom already-fetched PR list]:::bot
WORKLOAD --> FOR_EACH[For each open PR]:::bot
FOR_EACH --> PARSE[Parse PR body\nfor closing keywords\ncloses fix resolves + issue number]:::bot
PARSE --> DIFF{Difficulty from\nlinked issue?}:::gate
DIFF -- skill:advanced --> ADV[difficulty = advanced]:::bot
DIFF -- skill:beginner\nor Good First Issue --> BEG[difficulty = beginner]:::bot
DIFF -- skill:intermediate --> INT[difficulty = intermediate]:::bot
DIFF -- No linked issue\nor no label --> DEFAULT[difficulty = intermediate\npost once: please link issue]:::bot
ADV & BEG & INT & DEFAULT --> REVIEWS[Fetch all reviews\npaginated\nkeep latest per user\nignore COMMENTED + DISMISSED]:::bot
REVIEWS --> PERMS[Check permission\nfor each APPROVED reviewer\ncatch 404 silently]:::bot
PERMS --> COUNT[write_approvals = approvers\nwith write or admin\n\njunior_approvals = approvers\nin junior-committer team\nvia getMembershipForUserInOrg]:::bot
COUNT --> LABEL_LOGIC{Determine\ncorrect label}:::gate
LABEL_LOGIC -- advanced AND\nwrite_approvals >= 2 --> RTM[ready-to-merge]:::done
LABEL_LOGIC -- advanced AND\nwrite_approvals < 2 --> QC[queue:committers]:::bot
LABEL_LOGIC -- non-advanced AND\nwrite_approvals >= 2 --> RTM
LABEL_LOGIC -- non-advanced AND\njunior_approvals >= 1 --> QC
LABEL_LOGIC -- non-advanced AND\n0 approvals --> QJC[queue:junior-committer]:::bot
QJC --> ESCALATE{Days since\npr.updated_at >= 5?}:::gate
ESCALATE -- Yes --> QC
ESCALATE -- No --> APPLY
QC --> APPLY
RTM --> APPLY
APPLY{Label\nchanged?}:::gate
APPLY -- No --> NEXT[Next PR]:::skip
APPLY -- Yes --> ADD_LABEL[Add new label FIRST\nthen remove old queue labels]:::bot
ADD_LABEL --> ASSIGN[Unassign previous bot assignee\nAssign least-busy team member\nfor new label stage]:::bot
ASSIGN --> COMMENT{Bot already\ncommented for\nthis label state?}:::gate
COMMENT -- Yes --> NEXT
COMMENT -- No --> POST[Post one comment\ntagging assigned person\nexplaining what is needed]:::bot
POST --> NEXT
NEXT --> FOR_EACH
Loading
Detailed Logic
Step 1: Rate limit guard
Check remaining API calls at run start.
If < 200 -> skip entire run. Prevents partial updates.
Step 2: Fetch all open PRs + compute workload
Fetch all open non-draft PRs in one paginated call. Compute team member workload (count of assigned open PRs) from this same list, zero extra API calls needed.
Step 3: For each PR , parse difficulty
Parse PR body using comprehensive regex covering all GitHub closing keywords (closes,, closed, fixes, fix, etc).
Step 4: Fetch and filter reviews
Paginate all reviews. Keep only latest state per user. Only track: APPROVED, CHANGES_REQUESTED. Completely ignore: COMMENTED, DISMISSED.
Step 5: Check permissions
For each user with latest state = APPROVED:
Try getCollaboratorPermissionLevel
write or admin -> write_approval++
404 (not a collaborator) -> caught silently, ignored
For beginner/intermediate routing: check team membership via getMembershipForUserInOrg against junior-committer team
If correct_label = queue:junior-committer AND days since pr.updated_at >= 5 -> override to queue:committers.
Step 8: Apply label
We will add new label FIRST, then remove old queue labels. This prevents any PR ever having zero queue labels mid-run.
Step 9: Assignment (only on label change)
We Unassign only the previous bot-assigned person (tracked via comment history, not all assignees, to preserve manual assignments).
Assign least-busy member of the appropriate team using addAssignees.
Least-busy = lowest count of currently assigned open PRs,
computed from the already-fetched PR list in Step 2.
Step 10: Comment (only on label change)
Check last bot comment on PR. If it already references the current label -> skip. Otherwise post exactly one comment.
on:
schedule:
- cron: '*/30 * * * *'workflow_dispatch: # manual trigger from Actions tab
Cron completely solves the fork PR read-only token problem.
Scheduled workflows run on base branch with full write permissions.
Fork origin is irrelevant. No pull_request_target. No workflow_run.
Accepted tradeoff: labels update within ~30 minutes of a review
event. For a review queue tracker this is acceptable.
workflow_dispatch available for urgent manual sync.
Probot GitHub App (sdk-automations)
Cleanest long-term permission model. Planned for v2 once v1 validates the review logic. The cron workflow is designed to migrate cleanly to Probot since it is fully stateless , the same recompute logic moves to a webhook handler with no architectural changes needed.
Problem
hiero-sdk-python currently has no automated review workflow. Maintainers manually track which PRs need attention and on which stage of review it is present. This creates several problems:
Solution
Overview
A stateless cron-based workflow that runs every 30 minutes, scans all open PRs, and keeps exactly one review queue label accurate on each PR based on live review data. The bot reflects state, GitHub
branch protection enforces it.
Mental model:
Required Branch Protection Settings
These must be configured before the bot is useful:
The bot never reads or parses the CODEOWNERS file. Code owner enforcement is fully delegated to branch protection.
Role Hierarchy
Junior committers are the triage function. No separate triage team.
The bot uses
addAssigneesinstead, which only requires write access from the calling token (the cron's GITHUB_TOKEN). This sends a real persistent GitHub notification and shows up in the assignee's dashboard.Label Set: 3 Labels Only On PRs
queue:junior-committerqueue:committersready-to-mergeOnly one label active per PR at a time. Never zero.
Issue labels (
skill: beginner,skill: intermediate,skill: advanced,Good First Issue) stay on issues only.The existing label-copy-from-issue workflow must be disabled
as part of this implementation.
Full Flow Diagram
flowchart TD classDef trigger fill:#e1bee7,stroke:#8e24aa,color:#000 classDef bot fill:#c8e6c9,stroke:#388e3c,color:#000 classDef human fill:#bbdefb,stroke:#1976d2,color:#000 classDef gate fill:#ffecb3,stroke:#ffa000,color:#000 classDef done fill:#b2dfdb,stroke:#00796b,color:#000 classDef skip fill:#f5f5f5,stroke:#9e9e9e,color:#555 CRON([🕐 Cron fires every 30 min]):::trigger RATE{Rate limit\n>= 200 remaining?}:::gate SKIP_RUN([Skip run, log warning]):::skip FETCH[Fetch all open non-draft PRs]:::bot CRON --> RATE RATE -- No --> SKIP_RUN RATE -- Yes --> FETCH FETCH --> WORKLOAD[Compute team workload\nfrom already-fetched PR list]:::bot WORKLOAD --> FOR_EACH[For each open PR]:::bot FOR_EACH --> PARSE[Parse PR body\nfor closing keywords\ncloses fix resolves + issue number]:::bot PARSE --> DIFF{Difficulty from\nlinked issue?}:::gate DIFF -- skill:advanced --> ADV[difficulty = advanced]:::bot DIFF -- skill:beginner\nor Good First Issue --> BEG[difficulty = beginner]:::bot DIFF -- skill:intermediate --> INT[difficulty = intermediate]:::bot DIFF -- No linked issue\nor no label --> DEFAULT[difficulty = intermediate\npost once: please link issue]:::bot ADV & BEG & INT & DEFAULT --> REVIEWS[Fetch all reviews\npaginated\nkeep latest per user\nignore COMMENTED + DISMISSED]:::bot REVIEWS --> PERMS[Check permission\nfor each APPROVED reviewer\ncatch 404 silently]:::bot PERMS --> COUNT[write_approvals = approvers\nwith write or admin\n\njunior_approvals = approvers\nin junior-committer team\nvia getMembershipForUserInOrg]:::bot COUNT --> LABEL_LOGIC{Determine\ncorrect label}:::gate LABEL_LOGIC -- advanced AND\nwrite_approvals >= 2 --> RTM[ready-to-merge]:::done LABEL_LOGIC -- advanced AND\nwrite_approvals < 2 --> QC[queue:committers]:::bot LABEL_LOGIC -- non-advanced AND\nwrite_approvals >= 2 --> RTM LABEL_LOGIC -- non-advanced AND\njunior_approvals >= 1 --> QC LABEL_LOGIC -- non-advanced AND\n0 approvals --> QJC[queue:junior-committer]:::bot QJC --> ESCALATE{Days since\npr.updated_at >= 5?}:::gate ESCALATE -- Yes --> QC ESCALATE -- No --> APPLY QC --> APPLY RTM --> APPLY APPLY{Label\nchanged?}:::gate APPLY -- No --> NEXT[Next PR]:::skip APPLY -- Yes --> ADD_LABEL[Add new label FIRST\nthen remove old queue labels]:::bot ADD_LABEL --> ASSIGN[Unassign previous bot assignee\nAssign least-busy team member\nfor new label stage]:::bot ASSIGN --> COMMENT{Bot already\ncommented for\nthis label state?}:::gate COMMENT -- Yes --> NEXT COMMENT -- No --> POST[Post one comment\ntagging assigned person\nexplaining what is needed]:::bot POST --> NEXT NEXT --> FOR_EACHDetailed Logic
Step 1: Rate limit guard
Check remaining API calls at run start.
If < 200 -> skip entire run. Prevents partial updates.
Step 2: Fetch all open PRs + compute workload
Fetch all open non-draft PRs in one paginated call. Compute team member workload (count of assigned open PRs) from this same list, zero extra API calls needed.
Step 3: For each PR , parse difficulty
Parse PR body using comprehensive regex covering all GitHub closing keywords (closes,, closed, fixes, fix, etc).
Step 4: Fetch and filter reviews
Paginate all reviews. Keep only latest state per user. Only track: APPROVED, CHANGES_REQUESTED. Completely ignore: COMMENTED, DISMISSED.
Step 5: Check permissions
For each user with latest state = APPROVED:
Step 6: Label determination
advanced:
beginner / intermediate :
Step 7: Escalation
If correct_label = queue:junior-committer AND days since pr.updated_at >= 5 -> override to queue:committers.
Step 8: Apply label
We will add new label FIRST, then remove old queue labels. This prevents any PR ever having zero queue labels mid-run.
Step 9: Assignment (only on label change)
We Unassign only the previous bot-assigned person (tracked via comment history, not all assignees, to preserve manual assignments).
Assign least-busy member of the appropriate team using addAssignees.
Least-busy = lowest count of currently assigned open PRs,
computed from the already-fetched PR list in Step 2.
Step 10: Comment (only on label change)
Check last bot comment on PR. If it already references the current label -> skip. Otherwise post exactly one comment.
Complete End-to-End Flows
GFI / Beginer / Intermediate PR
Advanced PR
Escalation (no junior committer response)
New commits after approval
Trigger
Cron completely solves the fork PR read-only token problem.
Scheduled workflows run on base branch with full write permissions.
Fork origin is irrelevant. No pull_request_target. No workflow_run.
Accepted tradeoff: labels update within ~30 minutes of a review
event. For a review queue tracker this is acceptable.
workflow_dispatch available for urgent manual sync.
Explicitly Out of v1 Scope
Alternatives
Probot GitHub App (sdk-automations)
Cleanest long-term permission model. Planned for v2 once v1 validates the review logic. The cron workflow is designed to migrate cleanly to Probot since it is fully stateless , the same recompute logic moves to a webhook handler with no architectural changes needed.