Skip to content

feat: automated review queue label sync stateless cron-based v1 #2229

Description

@darshit2308

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:

  1. 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.
  2. 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
  3. With limited junior committers, committers, and maintainers, PRs can sit unreviewed indefinitely with no escalation path
  4. 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)
  • Bot cron = scoreboard (keeps visual queue labels accurate)
  • 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

Step 6: Label determination

advanced:

  • write_approvals >= 2 -> ready-to-merge
  • else -> queue:committers (junior-committer stage skipped)

beginner / intermediate :

  • write_approvals >= 2 -> ready-to-merge
  • junior_committer_approval >= 1 -> queue:committers
  • else -> queue:junior-committer

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

  1. PR opens -> links to GFI / Beginer / Intermediate issue
  2. Cron: 0 approvals -> queue:junior-committer ==> least-busy junior committer assigned -> comment: "@x assigned for soft quality check"
  3. Junior committer reviews -> clicks Approve (read-only)
  4. Cron: junior_approval = 1, write_approval = 0 -> queue:committers ==> junior committer unassigned, least-busy committer assigned -> comment: "@y assigned for committer review"
  5. Committer reviews -> clicks Approve (write_approval = 1)
  6. Cron: write_approval = 1, not yet 2 ==> label stays queue:committers, no change
  7. Maintainer reviews -> clicks Approve (write_approval = 2)
  8. Cron: write_approval = 2 -> ready-to-merge ==> committer unassigned, maintainer assigned -> comment: "@z 2 approvals secured, please verify and merge"
  9. Maintainer checks linked issue updated ==> merges GitHub branch protection enforces code owner requirement

Advanced PR

  1. PR opens ==> links to skill:advanced issue
  2. Cron: difficulty = advanced, 0 approvals ==> queue:committers directly (junior-committer skipped) -> least-busy committer assigned
  3. Committer approves (write_approval = 1)
  4. Cron: still 1, stays queue:committers
  5. Maintainer approves (write_approval = 2)
  6. Cron: ready-to-merge ==> maintainer assigned -> merge

Escalation (no junior committer response)

  1. queue:junior-committer applied, and junior is assigned
  2. No activity for 5 days (pr.updated_at unchanged)
  3. Cron: escalation triggered -> queue:committers applied -> comment: "No junior review after 5 days, escalating" -> least-busy committer assigned

New commits after approval

  1. PR in queue:committers with 1 write approval
  2. Contributor pushes new commits
  3. GitHub dismisses stale review (branch protection)
  4. pr.updated_at updates
  5. Cron: write_approval = 0, junior_approval = 0 -> queue:junior-committer restored -> junior committer reassigned Self-healing , no manual intervention needed

Trigger

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.

Explicitly Out of v1 Scope

  • DCO / PR template checks (separate workflow)
  • CODEOWNERS file parsing (branch protection handles this)
  • Stale PR cleanup (v2)
  • Probot app integration (v2)
  • First-time contributor welcome message (v2)
  • Cross-repo assignment tracking (v2)

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.

Metadata

Metadata

Assignees

Labels

Good First IssueIssues which are ideal for a first time or new project contributor.

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions