Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
358 changes: 358 additions & 0 deletions .github/workflows/assign-reviewers.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,358 @@
name: Assign Reviewers (reusable)

# Auto-requests expertise-aware, load-balanced PR reviewers, with optional
# "new-folk" randomization so people outside the matched experts get review
# reps. Models its structure on cursor-review-auto-label.yml: a workflow_call
# contract, caller-side `vars` for config, a GitHub App token, and defensive
# draft/skip handling that never fails the PR.
#
# WHY AN APP TOKEN: reviewer requests are made with the CLOUD_CODE_BOT app token
# (vars.APP_ID + secrets.CLOUD_CODE_BOT_PRIVATE_KEY), not the default
# GITHUB_TOKEN. On fork PRs the GITHUB_TOKEN is read-only, so a
# GITHUB_TOKEN-driven requestReviewers would 403 (the failure seen in
# github-workflows#12). The app token sidesteps that, so the calling job only
# needs `contents: read` — no `pull-requests: write` on GITHUB_TOKEN.
Comment on lines +9 to +14

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🩺 Stability & Availability | 🟠 Major | 🏗️ Heavy lift

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo '--- file list ---'
git ls-files .github/workflows/assign-reviewers.yml

echo '--- outline ---'
ast-grep outline .github/workflows/assign-reviewers.yml --view expanded || true

echo '--- relevant lines ---'
cat -n .github/workflows/assign-reviewers.yml | sed -n '1,220p'

Repository: Comfy-Org/github-workflows

Length of output: 11481


🌐 Web query:

GitHub Actions fork pull_request repository secrets GITHUB_TOKEN only reusable workflow workflow_call secrets unavailable private key

💡 Result:

When a GitHub Actions workflow is triggered by a pull request from a forked repository, GitHub intentionally restricts access to secrets to prevent malicious code in the fork from exfiltrating sensitive data [1][2][3]. Key points regarding this restriction: 1. Secrets Availability: With the exception of the GITHUB_TOKEN, secrets are not passed to the runner when a workflow is triggered from a forked repository [2][4]. This applies to both the repository's own secrets and any secrets you might attempt to pass into a reusable workflow [2][5]. 2. GITHUB_TOKEN Restrictions: For workflows triggered by a pull_request event from a fork, the GITHUB_TOKEN is restricted to read-only access by default [1][6]. Even if you use a reusable workflow, this restriction persists [6]. 3. Reusable Workflows (workflow_call): Reusable workflows do not bypass the security restrictions imposed by the triggering event [2]. If the caller workflow is triggered by a pull request from a fork, the secrets will remain unavailable to the called (reusable) workflow, even if you explicitly map them in the jobs.<job_id>.secrets section [2][5]. 4. Safely Accessing Secrets: - pull_request_target: This event trigger runs in the context of the base repository and is granted access to secrets and a read/write GITHUB_TOKEN [1][7]. However, this is dangerous if you check out code from the pull request commit, as that code is untrusted [1][3]. To use this safely, you must ensure the workflow only runs on code from your own repository's default branch or implements strict manual approval/permission checks before processing the untrusted code [1][3]. - Manual Approval: For pull_request events, you cannot "enable" secrets. Many maintainers instead use automation that requires a user with write access to manually approve or trigger a second workflow run (e.g., via a comment or label) to perform tasks requiring secrets [8][9][3]. In summary, you cannot force secrets to be available in a workflow triggered by a pull request from a fork using standard configuration [2][10]. You must either redesign your workflow to use the pull_request_target event (with extreme caution) or require a trusted collaborator to re-run the workflow [1][8][3].

Citations:


Fork PRs still can’t use this example as written

The pull_request caller in the example won’t receive CLOUD_CODE_BOT_PRIVATE_KEY on forked PRs, so the app-token step fails before reviewer assignment can run. Either call out that this workflow is same-repo-only, or switch the guidance to a carefully constrained pull_request_target setup that never checks out untrusted head code. Tiny fork, big flop.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.github/workflows/assign-reviewers.yml around lines 9 - 14, The
reviewer-assignment guidance currently assumes the app token is available on all
pull_request runs, but forked PRs won’t receive CLOUD_CODE_BOT_PRIVATE_KEY so
the workflow fails before assign-reviewers can run. Update the assign-reviewers
workflow comments and example around the reviewer request step to explicitly
state that the current setup is same-repo-only, or revise it to use a tightly
scoped pull_request_target pattern that avoids checking out untrusted head code;
keep the guidance centered on the CLOUD_CODE_BOT token flow and requestReviewers
usage.

#
# REVIEWER CONFIG (in the CALLER repo, default `.github/reviewers.yml`): a
# top-level `default_pool` plus a `rules` list mapping path globs to reviewers.
#
# default_pool: [octocat, hubot] # fallback when no rule matches
# rules:
# - paths: ["frontend/**", "*.css"] # globs (*, **, ? supported)
# reviewers: [alice, bob]
# - paths:
# - backend/**
# reviewers:
# - carol
#
# Both flow (`[a, b]`) and block (`- a`) sequences are accepted. The config is
# read from the PR head SHA via the contents API, so a PR's own edits to the
# config take effect on that PR.
#
# SELECTION: changed paths -> union of matching experts (else default_pool) ->
# drop the author + vars.REVIEWER_EXCLUDE -> rank ascending by each candidate's
# open review load (steering off anyone at/over vars.REVIEWER_LOAD_CAP) ->
# deterministically (per PR number) maybe swap the last slot for a
# vars.REVIEWER_GROWTH_POOL member -> request the top `num_reviewers`.
#
# Example caller (consumer repo):
#
# name: CI - Assign Reviewers
# on:
# pull_request:
# types: [opened, ready_for_review]
# jobs:
# assign:
# permissions:
# contents: read
# uses: Comfy-Org/github-workflows/.github/workflows/assign-reviewers.yml@<sha> # v1
# with:
# num_reviewers: 2
# secrets:
# CLOUD_CODE_BOT_PRIVATE_KEY: ${{ secrets.CLOUD_CODE_BOT_PRIVATE_KEY }}
#
# Caller-side configuration:
# vars.APP_ID - CLOUD_CODE_BOT app id (same as cursor-review-auto-label)
# vars.REVIEWER_GROWTH_POOL - whitespace-separated logins for new-folk randomization
# vars.REVIEWER_LOAD_CAP - max open reviews before a candidate is steered off (optional)
# vars.REVIEWER_EXCLUDE - whitespace-separated logins to hard-exclude (optional)

on:
workflow_call:
inputs:
reviewer_config_path:
description: >-
Path in the caller repo to the expertise/path-glob reviewer config.
type: string
required: false
default: .github/reviewers.yml
num_reviewers:
description: How many reviewers to request.
type: number
required: false
default: 2
skip_label:
description: >-
If this label is present on the PR, reviewer assignment is suppressed.
type: string
required: false
default: skip-auto-assign
secrets:
CLOUD_CODE_BOT_PRIVATE_KEY:
description: >-
Private key for the CLOUD_CODE_BOT GitHub App (app id = vars.APP_ID).
Required: reviewer requests are made by the app so they succeed even on
fork PRs where the default GITHUB_TOKEN is read-only.
required: true

permissions:
contents: read

jobs:
assign-reviewers:
name: Request expertise-aware reviewers
runs-on: ubuntu-latest
permissions:
contents: read
steps:
- name: Generate GitHub App token
id: app-token
uses: actions/create-github-app-token@bcd2ba49218906704ab6c1aa796996da409d3eb1 # v3.2.0
with:
app-id: ${{ vars.APP_ID }}
private-key: ${{ secrets.CLOUD_CODE_BOT_PRIVATE_KEY }}
Comment on lines +98 to +103

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🔒 Security & Privacy | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

sed -n '1,180p' .github/workflows/assign-reviewers.yml

Repository: Comfy-Org/github-workflows

Length of output: 7846


🌐 Web query:

actions/create-github-app-token permission-contents permission-pull-requests default installation permissions

💡 Result:

The actions/create-github-app-token action allows you to define granular permissions for the generated installation access token using inputs prefixed with permission- [1][2][3]. By default, if you do not specify any permission-* inputs, the generated token inherits all permissions that were granted to the app's installation [1][3][4]. To restrict the token's scope, you can explicitly set specific permissions by prefixing the permission key with permission- (e.g., using permission-contents or permission-pull-requests) and setting the value to read or write [1][5][6]. It is a recommended practice to explicitly list only the permissions required for your specific use case to follow the principle of least privilege [1][3]. Note the following critical details regarding these permissions: 1. Installation Constraint: The permissions you request via the action must be a subset of the permissions already granted to the app's installation [3][4]. If you attempt to request a permission that the installation does not possess, the action will result in an error [3][4]. 2. Scope Inheritance: If you omit a specific permission-* input, the resulting token will have the full set of permissions defined at the app installation level for that category, provided the app has them [1][3]. 3. Implementation: The action maps these inputs to the permissions parameter of the GitHub API "Create an installation access token for an app" endpoint, which enforces the requested restrictions [7]. If you encounter unexpected errors (such as 403 or 422) after specifying permissions, ensure that the app installation has been granted the necessary permissions in your GitHub App settings and that your workflow inputs correctly match the required scopes for your tasks [8][3][7].

Citations:


🏁 Script executed:

sed -n '180,420p' .github/workflows/assign-reviewers.yml

Repository: Comfy-Org/github-workflows

Length of output: 8618


Restrict the GitHub App token permissions. actions/create-github-app-token inherits the installation’s full scopes by default; add permission-contents: read and permission-pull-requests: write so this job mints a least-privilege token, not a broad one.

🧰 Tools
🪛 zizmor (1.26.1)

[error] 100-100: dangerous use of GitHub App tokens (github-app): app token inherits blanket installation permissions

(github-app)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.github/workflows/assign-reviewers.yml around lines 98 - 103, The GitHub App
token in the `Generate GitHub App token` step is being minted with broader
default installation scopes than needed. Update the
`actions/create-github-app-token` usage in this workflow to explicitly request
least-privilege permissions by adding `permission-contents: read` and
`permission-pull-requests: write` alongside the existing `app-id` and
`private-key` inputs so the `app-token` step only grants the access required by
this job.

Source: Linters/SAST tools


- name: Select and request reviewers
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
env:
REVIEWER_CONFIG_PATH: ${{ inputs.reviewer_config_path }}
NUM_REVIEWERS: ${{ inputs.num_reviewers }}
SKIP_LABEL: ${{ inputs.skip_label }}
GROWTH_POOL: ${{ vars.REVIEWER_GROWTH_POOL }}
LOAD_CAP: ${{ vars.REVIEWER_LOAD_CAP }}
EXCLUDE: ${{ vars.REVIEWER_EXCLUDE }}
with:
# Authenticate the octokit with the APP token so reviewer requests
# work on fork PRs (read-only GITHUB_TOKEN would 403).
github-token: ${{ steps.app-token.outputs.token }}
Comment on lines +98 to +117

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

Run no-op guards before minting the App token.

Drafts, bots, skip-labeled PRs, and already-requested PRs still mint a privileged token before returning. Move the cheap payload-based guards to a pre-token job/step condition so skipped PRs remain true no-ops.

Also applies to: 124-136

🧰 Tools
🪛 zizmor (1.26.1)

[error] 100-100: dangerous use of GitHub App tokens (github-app): app token inherits blanket installation permissions

(github-app)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.github/workflows/assign-reviewers.yml around lines 98 - 117, The
reviewer-selection workflow still mints the GitHub App token before checking
no-op cases like drafts, bots, skip-labeled PRs, and already-requested PRs. Move
those cheap payload-based guards ahead of the token minting in the
assign-reviewers workflow, or gate the “Generate GitHub App token” / “Select and
request reviewers” steps with the same preconditions, so the job exits without
creating a privileged token when no action is needed.

script: |
const pr = context.payload.pull_request;
if (!pr) { core.info('Not a pull_request event — nothing to do.'); return; }
const { owner, repo } = context.repo;
const pull_number = pr.number;

// --- 1. Early exits (never fail the PR) ---
if (pr.draft) { core.info('PR is a draft — skipping.'); return; }
if (pr.user && pr.user.type === 'Bot') { core.info('Author is a bot — skipping.'); return; }
const skipLabel = process.env.SKIP_LABEL;
const labels = (pr.labels || []).map((l) => l.name);
if (skipLabel && labels.includes(skipLabel)) {
core.info(`${skipLabel} present — skipping.`);
return;
}
if ((pr.requested_reviewers || []).length > 0) {
core.info('PR already has requested reviewers — skipping.');
return;
}
Comment on lines +133 to +136

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

sed -n '110,170p' .github/workflows/assign-reviewers.yml

Repository: Comfy-Org/github-workflows

Length of output: 3134


🏁 Script executed:

rg -n "requested_teams|requested_reviewers|reviewers" .github/workflows -S

Repository: Comfy-Org/github-workflows

Length of output: 3543


🌐 Web query:

GitHub pull request webhook payload requested_reviewers requested_teams review request teams API docs

💡 Result:

When a pull request review is requested on GitHub, the pull_request webhook sends events with the action review_requested or review_request_removed [1][2]. In these webhook payloads, the specific entities involved in the review request are provided in separate fields rather than within the pull_request object itself [1]: - requested_reviewer: This field contains the user object when an individual person is requested to review the pull request [1]. - requested_team: This field contains the team object when a team is requested to review the pull request [1]. These fields are present at the root level of the webhook payload, alongside the action, number, pull_request, and repository objects [1]. Note that this differs from the standard Pull Request object retrieved via the REST API (e.g., GET /repos/{owner}/{repo}/pulls/{pull_number}), where the current state of all requested reviewers and teams is represented by the requested_reviewers (an array of user objects) and requested_teams (an array of team objects) fields within the Pull Request resource itself [3]. In summary, use requested_reviewer or requested_team when parsing the pull_request webhook payload for specific review events, and use requested_reviewers or requested_teams when inspecting the current state of a pull request via the GitHub REST API [1][3].

Citations:


Treat team review requests as existing reviewers too. requested_reviewers ignores requested_teams, so a PR with only a team request can still pick up extra individual reviewers. Add the team check here—otherwise the guard is a bit team-dreamy.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.github/workflows/assign-reviewers.yml around lines 133 - 136, The reviewer
guard in the workflow currently only checks requested_reviewers, so PRs with
only requested_teams can still get extra reviewers added. Update the existing
check in the assign-reviewers workflow to treat team review requests as
already-reviewed too by including requested_teams alongside requested_reviewers,
keeping the skip path in the same guard block.


// --- helpers ---
// Dependency-free glob -> RegExp (`*` within a segment, `**` across
// segments, `?` single non-slash char). minimatch/picomatch are not
// reliably available inside github-script, so we roll our own.
const globToRegExp = (glob) => {
let re = '';
for (let i = 0; i < glob.length; i++) {
const c = glob[i];
if (c === '*') {
if (glob[i + 1] === '*') {
if (glob[i + 2] === '/') { re += '(?:.*/)?'; i += 2; }
else { re += '.*'; i += 1; }
} else { re += '[^/]*'; }
} else if (c === '?') {
re += '[^/]';
} else if ('.+^${}()|[]\\/'.includes(c)) {
re += '\\' + c;
} else {
re += c;
}
}
return new RegExp('^' + re + '$');
};
const matchesAny = (p, globs) => globs.some((g) => globToRegExp(g).test(p));

// Minimal YAML parser scoped to the documented reviewers.yml schema
// (default_pool + rules[{paths, reviewers}], flow or block seqs).
// js-yaml is not bundled with github-script; the schema is small and
// fixed, so a focused parser is safer than an unavailable dependency.
const parseReviewerConfig = (text) => {
const out = { default_pool: [], rules: [] };
const stripComment = (s) => {
let inS = false, inD = false;
for (let i = 0; i < s.length; i++) {
const ch = s[i];
if (ch === "'" && !inD) inS = !inS;
else if (ch === '"' && !inS) inD = !inD;
else if (ch === '#' && !inS && !inD && (i === 0 || /\s/.test(s[i - 1]))) return s.slice(0, i);
}
return s;
};
const unquote = (s) => {
s = s.trim();
if ((s.startsWith('"') && s.endsWith('"')) || (s.startsWith("'") && s.endsWith("'"))) return s.slice(1, -1);
return s;
};
const parseFlow = (s) => {
s = s.trim();
if (!s.startsWith('[')) return null;
const end = s.indexOf(']');
const inner = s.slice(1, end === -1 ? s.length : end);
return inner.split(',').map((x) => unquote(x)).filter((x) => x.length > 0);
};
const indentOf = (l) => l.match(/^ */)[0].length;
const lines = text.split(/\r?\n/).map(stripComment);
let i = 0;
while (i < lines.length) {
const raw = lines[i];
if (!raw.trim()) { i++; continue; }
const line = raw.trim();
if (indentOf(raw) === 0 && line.startsWith('default_pool:')) {
const rest = line.slice('default_pool:'.length).trim();
const flow = parseFlow(rest);
if (flow) { out.default_pool = flow; i++; continue; }
i++;
while (i < lines.length) {
const r = lines[i];
if (!r.trim()) { i++; continue; }
if (indentOf(r) === 0) break;
const t = r.trim();
if (t.startsWith('- ')) out.default_pool.push(unquote(t.slice(2)));
i++;
}
continue;
}
if (indentOf(raw) === 0 && line.startsWith('rules:')) {
i++;
let current = null, listKey = null, ruleIndent = -1;
const setKey = (seg) => {
const m = seg.match(/^(paths|reviewers):(.*)$/);
if (!m) return;
const key = m[1];
const val = m[2].trim();
const flow = parseFlow(val);
if (flow) { if (current) current[key] = flow; listKey = null; }
else if (val === '') { listKey = key; }
else { if (current) current[key] = [unquote(val)]; listKey = null; }
};
while (i < lines.length) {
const r = lines[i];
if (!r.trim()) { i++; continue; }
if (indentOf(r) === 0) break;
const ind = indentOf(r);
const t = r.trim();
const isDash = t === '-' || t.startsWith('- ');
if (isDash && (ruleIndent === -1 || ind === ruleIndent)) {
if (ruleIndent === -1) ruleIndent = ind;
current = { paths: [], reviewers: [] };
out.rules.push(current);
listKey = null;
const afterDash = t.slice(1).trim();
if (afterDash) setKey(afterDash);
} else if (isDash && listKey && current) {
current[listKey].push(unquote(t.slice(1).trim()));
} else {
setKey(t);
}
i++;
}
continue;
}
i++;
}
return out;
};

// --- 2. Changed paths ---
const files = await github.paginate(github.rest.pulls.listFiles, {
owner, repo, pull_number, per_page: 100,
});
const changed = files.map((f) => f.filename);
core.info(`PR #${pull_number}: ${changed.length} changed file(s).`);

// --- 3. Read + parse the caller's reviewer config from the head SHA ---
const configPath = process.env.REVIEWER_CONFIG_PATH;
let config;
try {
const res = await github.rest.repos.getContent({
owner, repo, path: configPath, ref: pr.head.sha,
});
if (Array.isArray(res.data) || !res.data.content) {
core.info(`${configPath} is not a file — nothing to do.`);
return;
}
const text = Buffer.from(res.data.content, 'base64').toString('utf8');
config = parseReviewerConfig(text);
} catch (e) {
core.info(`Could not read ${configPath}@${pr.head.sha} (${e.message}) — nothing to do.`);
return;
}

// Match changed paths -> union of expert reviewers; else default_pool.
const candidates = new Set();
for (const rule of config.rules) {
const globs = rule.paths || [];
if (globs.length && changed.some((p) => matchesAny(p, globs))) {
for (const r of (rule.reviewers || [])) candidates.add(r);
}
}
if (candidates.size === 0) {
for (const r of (config.default_pool || [])) candidates.add(r);
}

// --- 4. Remove the author and hard-excluded logins ---
const exclude = new Set(
(process.env.EXCLUDE || '').split(/\s+/).filter(Boolean)
);
if (pr.user && pr.user.login) exclude.add(pr.user.login);
let pool = [...candidates].filter((c) => !exclude.has(c));
if (pool.length === 0) {
core.info('No eligible candidates after exclusions — nothing to request.');
return;
}

// --- 5. Load-balance: rank ascending by open review load ---
const cap = parseInt(process.env.LOAD_CAP || '', 10); // NaN when unset
const withLoad = [];
for (const login of pool) {
let load = 0;
try {
const r = await github.rest.search.issuesAndPullRequests({
q: `is:open is:pr review-requested:${login} org:${owner}`,
});
load = r.data.total_count;
} catch (e) {
core.info(`Load query failed for ${login} (${e.message}) — treating load as 0.`);
}
withLoad.push({ login, load });
}
let ranked = withLoad.slice().sort((a, b) => a.load - b.load);
if (!Number.isNaN(cap)) {
const under = ranked.filter((x) => x.load < cap);
// Only steer off over-cap candidates if some remain; otherwise a
// busy reviewer beats no reviewer.
if (under.length > 0) ranked = under;
}
let selected = ranked.map((x) => x.login);

// --- 6. New-folk randomization (deterministic per PR number) ---
const num = Math.max(1, parseInt(process.env.NUM_REVIEWERS || '2', 10) || 2);
const growth = (process.env.GROWTH_POOL || '').split(/\s+/).filter(Boolean);
selected = selected.slice(0, num);
// Deterministic pseudo-random in [0,1) from the PR number — stable
// across re-runs of the same PR, so behavior is not flaky.
const rand = ((pull_number * 2654435761) >>> 0) / 4294967296;
if (growth.length && selected.length >= 1 && rand < 0.5) {
const eligible = growth.filter((g) => !selected.includes(g) && !exclude.has(g));
if (eligible.length) {
const pick = eligible[pull_number % eligible.length];
if (selected.length >= 2) {
selected[selected.length - 1] = pick; // swap last expert slot
} else {
selected.push(pick); // single expert: add a newcomer instead of dropping coverage
}
}
}
selected = selected.slice(0, num);

// --- 7. Request reviewers (never fail the PR) ---
if (selected.length === 0) {
core.info('No reviewers selected — nothing to request.');
return;
}
try {
await github.rest.pulls.requestReviewers({
owner, repo, pull_number, reviewers: selected,
});
core.info(`Requested reviewers: ${selected.join(', ')}`);
} catch (e) {
core.warning(`requestReviewers failed (${e.message}) — leaving PR unassigned.`);
}
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ This repo is **public** so any repo — public or private, inside or outside the
| [`detect-unreviewed-merge.yml`](.github/workflows/detect-unreviewed-merge.yml) | SOC 2 compliance — detects PRs merged without prior approval and opens a tracking issue in [`Comfy-Org/unreviewed-merges`](https://github.com/Comfy-Org/unreviewed-merges). |
| [`cursor-review.yml`](.github/workflows/cursor-review.yml) | Label-triggered multi-model code review. A 4-lab × 2-review-type cursor-agent panel runs adversarial + edge-case passes, a judge model consolidates them into one PR review with per-finding severity badges, and the triggerer gets Slack start/complete DMs. Prompts and scripts live in [`.github/cursor-review/`](.github/cursor-review) — the single source of truth, so consumer repos carry only a thin caller. Requires `CURSOR_API_KEY` (+ optional `SLACK_BOT_TOKEN`). |
| [`cursor-review-auto-label.yml`](.github/workflows/cursor-review-auto-label.yml) | Companion to `cursor-review.yml`. On PR assignment, applies the review label for an opted-in reviewer (via the CLOUD_CODE_BOT app token, so the label actually triggers the review). The opt-in roster lives in the caller's `vars.CURSOR_REVIEW_OPTED_IN_LOGINS` — no roster is baked into the workflow. Requires `vars.APP_ID` + `CLOUD_CODE_BOT_PRIVATE_KEY`. |
| [`assign-reviewers.yml`](.github/workflows/assign-reviewers.yml) | Auto-requests expertise-aware, load-balanced PR reviewers with new-folk randomization. Matches changed paths against a caller-repo `.github/reviewers.yml` (path-glob → reviewers, plus a `default_pool`), drops the author + `vars.REVIEWER_EXCLUDE`, ranks candidates by open review load (steering off anyone at/over `vars.REVIEWER_LOAD_CAP`), and may swap a slot for a `vars.REVIEWER_GROWTH_POOL` member. Requests go through the CLOUD_CODE_BOT app token so they work on fork PRs. Requires `vars.APP_ID` + `CLOUD_CODE_BOT_PRIVATE_KEY`. |

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

📐 Maintainability & Code Quality | 🔵 Trivial

Clarify the config-path contract.

This reads like the workflow is hard-wired to .github/reviewers.yml, but reviewer_config_path is a public input. Calling out the override here will keep the docs from going off-piste.

💡 Suggested wording tweak
-| [`assign-reviewers.yml`](.github/workflows/assign-reviewers.yml) | Auto-requests expertise-aware, load-balanced PR reviewers with new-folk randomization. Matches changed paths against a caller-repo `.github/reviewers.yml` (path-glob → reviewers, plus a `default_pool`), drops the author + `vars.REVIEWER_EXCLUDE`, ranks candidates by open review load (steering off anyone at/over `vars.REVIEWER_LOAD_CAP`), and may swap a slot for a `vars.REVIEWER_GROWTH_POOL` member. Requests go through the CLOUD_CODE_BOT app token so they work on fork PRs. Requires `vars.APP_ID` + `CLOUD_CODE_BOT_PRIVATE_KEY`. |
+| [`assign-reviewers.yml`](.github/workflows/assign-reviewers.yml) | Auto-requests expertise-aware, load-balanced PR reviewers with new-folk randomization. Matches changed paths against the caller’s reviewer config (default `.github/reviewers.yml`, override via `reviewer_config_path`) with path-glob → reviewers plus a `default_pool`, drops the author + `vars.REVIEWER_EXCLUDE`, ranks candidates by open review load (steering off anyone at/over `vars.REVIEWER_LOAD_CAP`), and may swap a slot for a `vars.REVIEWER_GROWTH_POOL` member. Requests go through the CLOUD_CODE_BOT app token so they work on fork PRs. Requires `vars.APP_ID` + `CLOUD_CODE_BOT_PRIVATE_KEY`. |
📝 Committable suggestion

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

Suggested change
| [`assign-reviewers.yml`](.github/workflows/assign-reviewers.yml) | Auto-requests expertise-aware, load-balanced PR reviewers with new-folk randomization. Matches changed paths against a caller-repo `.github/reviewers.yml` (path-glob → reviewers, plus a `default_pool`), drops the author + `vars.REVIEWER_EXCLUDE`, ranks candidates by open review load (steering off anyone at/over `vars.REVIEWER_LOAD_CAP`), and may swap a slot for a `vars.REVIEWER_GROWTH_POOL` member. Requests go through the CLOUD_CODE_BOT app token so they work on fork PRs. Requires `vars.APP_ID` + `CLOUD_CODE_BOT_PRIVATE_KEY`. |
| [`assign-reviewers.yml`](.github/workflows/assign-reviewers.yml) | Auto-requests expertise-aware, load-balanced PR reviewers with new-folk randomization. Matches changed paths against the caller’s reviewer config (default `.github/reviewers.yml`, override via `reviewer_config_path`) with path-glob → reviewers plus a `default_pool`, drops the author + `vars.REVIEWER_EXCLUDE`, ranks candidates by open review load (steering off anyone at/over `vars.REVIEWER_LOAD_CAP`), and may swap a slot for a `vars.REVIEWER_GROWTH_POOL` member. Requests go through the CLOUD_CODE_BOT app token so they work on fork PRs. Requires `vars.APP_ID` + `CLOUD_CODE_BOT_PRIVATE_KEY`. |
🧰 Tools
🪛 LanguageTool

[uncategorized] ~14-~14: The official name of this software platform is spelled with a capital “H”.
Context: ...ID+CLOUD_CODE_BOT_PRIVATE_KEY. | | [assign-reviewers.yml`](.github/workflows/assign-reviewers.yml) | Auto-...

(GITHUB)


[uncategorized] ~14-~14: The official name of this software platform is spelled with a capital “H”.
Context: ...hes changed paths against a caller-repo .github/reviewers.yml (path-glob → reviewers, ...

(GITHUB)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@README.md` at line 14, The README entry for assign-reviewers.yml currently
implies the reviewer config is fixed at .github/reviewers.yml, but the workflow
also supports the public reviewer_config_path input. Update the description to
state that the workflow reads the reviewer mapping from reviewer_config_path
when provided, with .github/reviewers.yml as the default/fallback, and keep the
rest of the behavior summary unchanged.


## Usage

Expand Down