build: fixed pr-merge filtering to avoid non-mergable PRs
CI / benchmark-regression (push) Failing after 0s
CI / benchmark-publish (push) Failing after 0s
CI / push-validation (push) Successful in 28s
CI / helm (push) Successful in 45s
CI / build (push) Successful in 4m2s
CI / lint (push) Successful in 4m14s
CI / quality (push) Successful in 4m31s
CI / typecheck (push) Successful in 4m55s
CI / security (push) Successful in 5m14s
CI / e2e_tests (push) Successful in 7m17s
CI / integration_tests (push) Successful in 8m0s
CI / unit_tests (push) Successful in 9m18s
CI / docker (push) Failing after 46s
CI / coverage (push) Successful in 14m52s
CI / status-check (push) Failing after 4s

This commit is contained in:
2026-04-18 01:41:40 -04:00
parent bcf58f0703
commit 59a2a83328
12 changed files with 191 additions and 52 deletions
+7 -7
View File
@@ -287,13 +287,13 @@ See [`scripts/`](./references/scripts/) for the full index with invocation instr
| Script | Reference Docs | Script File | Description |
|--------|---------------|-------------|-------------|
| `list_prs` | [docs](./references/scripts/list_prs/) | [list_prs.ts](./scripts/list_prs.ts) | General-purpose PR lister with all filter options; core module imported by the six wrappers below |
| `list_prs_ready_to_merge` | [docs](./references/scripts/list_prs_ready_to_merge/) | [list_prs_ready_to_merge.ts](./scripts/list_prs_ready_to_merge.ts) | Open PRs with ≥ 1 approval, not stale — ready to merge immediately |
| `list_prs_stale_clean` | [docs](./references/scripts/list_prs_stale_clean/) | [list_prs_stale_clean.ts](./scripts/list_prs_stale_clean.ts) | Open PRs with ≥ 1 approval, stale but no conflicts — server-side rebase then merge |
| `list_prs_stale_conflicts` | [docs](./references/scripts/list_prs_stale_conflicts/) | [list_prs_stale_conflicts.ts](./scripts/list_prs_stale_conflicts.ts) | Open PRs with ≥ 1 approval, stale with conflicts — local clone, resolve, force-push, then merge |
| `list_prs_needs_review_not_stale` | [docs](./references/scripts/list_prs_needs_review_not_stale/) | [list_prs_needs_review_not_stale.ts](./scripts/list_prs_needs_review_not_stale.ts) | Open PRs with zero approvals, not stale — review only needed; merges immediately on approval |
| `list_prs_needs_review_stale_clean` | [docs](./references/scripts/list_prs_needs_review_stale_clean/) | [list_prs_needs_review_stale_clean.ts](./scripts/list_prs_needs_review_stale_clean.ts) | Open PRs with zero approvals, stale but no conflicts — review + server-side rebase needed |
| `list_prs_needs_review_stale_conflicts` | [docs](./references/scripts/list_prs_needs_review_stale_conflicts/) | [list_prs_needs_review_stale_conflicts.ts](./scripts/list_prs_needs_review_stale_conflicts.ts) | Open PRs with zero approvals, stale with conflicts — review + local conflict resolution needed |
| `list_prs` | [docs](./references/scripts/list_prs/) | [list_prs.ts](./scripts/list_prs.ts) | General-purpose PR lister with all filter options (`--stale`, `--ci-status`, `--min-approvals`, …); core module imported by the six wrappers below |
| `list_prs_ready_to_merge` | [docs](./references/scripts/list_prs_ready_to_merge/) | [list_prs_ready_to_merge.ts](./scripts/list_prs_ready_to_merge.ts) | Open PRs with ≥ 1 approval, not stale, **and CI passing** — ready to merge immediately |
| `list_prs_stale_clean` | [docs](./references/scripts/list_prs_stale_clean/) | [list_prs_stale_clean.ts](./scripts/list_prs_stale_clean.ts) | Open PRs with ≥ 1 approval, stale but no conflicts, any CI status — server-side rebase then merge |
| `list_prs_stale_conflicts` | [docs](./references/scripts/list_prs_stale_conflicts/) | [list_prs_stale_conflicts.ts](./scripts/list_prs_stale_conflicts.ts) | Open PRs with ≥ 1 approval, stale with conflicts, any CI status — local clone, resolve, force-push, then merge |
| `list_prs_needs_review_not_stale` | [docs](./references/scripts/list_prs_needs_review_not_stale/) | [list_prs_needs_review_not_stale.ts](./scripts/list_prs_needs_review_not_stale.ts) | Open PRs with zero approvals, not stale, any CI status — review only needed; merges immediately on approval |
| `list_prs_needs_review_stale_clean` | [docs](./references/scripts/list_prs_needs_review_stale_clean/) | [list_prs_needs_review_stale_clean.ts](./scripts/list_prs_needs_review_stale_clean.ts) | Open PRs with zero approvals, stale but no conflicts, any CI status — review + server-side rebase needed |
| `list_prs_needs_review_stale_conflicts` | [docs](./references/scripts/list_prs_needs_review_stale_conflicts/) | [list_prs_needs_review_stale_conflicts.ts](./scripts/list_prs_needs_review_stale_conflicts.ts) | Open PRs with zero approvals, stale with conflicts, any CI status — review + local conflict resolution needed |
| `merge_pr` | [docs](./references/scripts/merge_pr/) | [merge_pr.ts](./scripts/merge_pr.ts) | Initiates a rebase-style merge with automerge scheduling; handles open issue dependencies |
| `rebase_pr` | [docs](./references/scripts/rebase_pr/) | [rebase_pr.ts](./scripts/rebase_pr.ts) | Triggers a Forgejo server-side rebase on a stale, conflict-free PR — no local clone required |
@@ -87,7 +87,7 @@ product-builder (AUTO-PROD-BLDR) ← process supervisor (like systemd)
**Workers**: `pr-merge-worker` via **blocking Task tool call** (not async)
**Worker tag pattern**: NONE — no `[AUTO-PRMRG-N]` sessions ever exist
**Role**: Merges approved PRs. Verifies seven merge criteria. Handles pre-merge rebasing.
**Role**: Merges approved PRs. Verifies merge criteria. Handles pre-merge rebasing.
**⚠️ Special**: Does NOT use async-agent-manager. Calls `pr-merge-worker` directly as a Task subagent and blocks until it completes. When the supervisor is busy executing its worker, it appears as a normal busy session — this is correct behavior. Product-builder and system-watchdog must NOT flag the absence of `[AUTO-PRMRG-N]` sessions as a problem.
@@ -108,7 +108,7 @@ product-builder (AUTO-PROD-BLDR) ← process supervisor (like systemd)
**Definition**: `bug-hunt-pool-supervisor`
**Worker count**: N_QUARTER
**Sleep**: 900 seconds (15 minutes)
**Workers**: `bug-hunt-worker` (async sessions, use Gemini 2.5 Pro for large context)
**Workers**: `bug-hunt-worker` (async sessions)
**Worker tag pattern**: `[AUTO-BUG-N]`
**Role**: Proactive bug detection. Maps source modules, dispatches workers for deep code analysis combined with specification comparison.
@@ -119,7 +119,7 @@ product-builder (AUTO-PROD-BLDR) ← process supervisor (like systemd)
**Definition**: `test-infra-pool-supervisor`
**Worker count**: N_QUARTER
**Sleep**: 900 seconds (15 minutes)
**Workers**: `test-infra-worker` (async sessions, use Gemini 2.5 Pro)
**Workers**: `test-infra-worker` (async sessions)
**Worker tag pattern**: `[AUTO-INF-N]`
**Role**: Testing infrastructure improvement. Analyzes CI timing, coverage gaps, test architecture. Never disables or weakens existing checks — only proposes additions and optimizations.
@@ -17,13 +17,13 @@ buckets. Together they cover the complete PR pipeline used by
| Script | One-liner |
|--------|-----------|
| [`list_prs`](./list_prs/) | General-purpose PR lister — all filter options, importable as a module by the six wrappers below |
| [`list_prs_ready_to_merge`](./list_prs_ready_to_merge/) | Open PRs with ≥ 1 approval that are already current with their base branch — merge immediately |
| [`list_prs_stale_clean`](./list_prs_stale_clean/) | Open PRs with ≥ 1 approval that are behind their base branch but have no conflicts — server-side rebase then merge |
| [`list_prs_stale_conflicts`](./list_prs_stale_conflicts/) | Open PRs with ≥ 1 approval that are behind their base branch and have merge conflicts — local clone, resolve, force-push, then merge |
| [`list_prs_needs_review_not_stale`](./list_prs_needs_review_not_stale/) | Open PRs with zero approvals that are current with their base branch — review only needed |
| [`list_prs_needs_review_stale_clean`](./list_prs_needs_review_stale_clean/) | Open PRs with zero approvals that are stale but conflict-free — review + server-side rebase needed |
| [`list_prs_needs_review_stale_conflicts`](./list_prs_needs_review_stale_conflicts/) | Open PRs with zero approvals that are stale with conflicts — review + local conflict resolution needed |
| [`list_prs`](./list_prs/) | General-purpose PR lister — all filter options (`--stale`, `--ci-status`, `--min-approvals`, …), importable as a module by the six wrappers below |
| [`list_prs_ready_to_merge`](./list_prs_ready_to_merge/) | Open PRs with ≥ 1 approval, already current with their base branch, **and CI passing** — merge immediately |
| [`list_prs_stale_clean`](./list_prs_stale_clean/) | Open PRs with ≥ 1 approval, behind their base branch, no conflicts, any CI status — server-side rebase then merge |
| [`list_prs_stale_conflicts`](./list_prs_stale_conflicts/) | Open PRs with ≥ 1 approval, behind their base branch, merge conflicts present, any CI status — local clone, resolve, force-push, then merge |
| [`list_prs_needs_review_not_stale`](./list_prs_needs_review_not_stale/) | Open PRs with zero approvals, current with their base branch, any CI status — review only needed |
| [`list_prs_needs_review_stale_clean`](./list_prs_needs_review_stale_clean/) | Open PRs with zero approvals, stale but conflict-free, any CI status — review + server-side rebase needed |
| [`list_prs_needs_review_stale_conflicts`](./list_prs_needs_review_stale_conflicts/) | Open PRs with zero approvals, stale with conflicts, any CI status — review + local conflict resolution needed |
### PR Action Scripts
@@ -36,34 +36,37 @@ buckets. Together they cover the complete PR pipeline used by
## The Six-Bucket Pipeline
The six listing scripts define a complete, non-overlapping classification of
all open PRs. The two approval states (approved vs needs review) are each
further split by three staleness states, giving six buckets in total.
open PRs. The two approval states (approved vs needs review) are each split
by three staleness states, giving six buckets in total. **Bucket 1 additionally
requires CI to be passing**; approved + not-stale PRs with failing or pending CI
are held out of all buckets until their CI resolves.
```
Every open PR belongs to exactly one bucket at any moment:
Every open PR belongs to exactly one bucket at any moment
(or is temporarily absent if CI is blocking it from Bucket 1):
── Approved (≥ 1 non-dismissed approval) ──────────────────────────────────
Bucket 1 — list_prs_ready_to_merge (approved + not stale)
Bucket 2 — list_prs_stale_clean (approved + stale, no conflicts)
Bucket 3 — list_prs_stale_conflicts (approved + stale, has conflicts)
Bucket 1 — list_prs_ready_to_merge (approved + not stale + CI passing)
Bucket 2 — list_prs_stale_clean (approved + stale, no conflicts, any CI)
Bucket 3 — list_prs_stale_conflicts (approved + stale, has conflicts, any CI)
── Needs Review (0 approvals) ─────────────────────────────────────────────
Bucket 4 — list_prs_needs_review_not_stale (no approvals + not stale)
Bucket 5 — list_prs_needs_review_stale_clean (no approvals + stale, no conflicts)
Bucket 6 — list_prs_needs_review_stale_conflicts (no approvals + stale, has conflicts)
Bucket 4 — list_prs_needs_review_not_stale (no approvals + not stale, any CI)
Bucket 5 — list_prs_needs_review_stale_clean (no approvals + stale, no conflicts, any CI)
Bucket 6 — list_prs_needs_review_stale_conflicts (no approvals + stale, has conflicts, any CI)
Exception: PRs with stale_state == stale_unknown (Forgejo still computing
conflict status) are transiently absent from all buckets. They appear in
the appropriate bucket within seconds once Forgejo finishes.
── Temporarily absent (not in any bucket) ─────────────────────────────────
• stale_state == stale_unknown → Forgejo still computing conflict status; appears shortly
approved + not stale + CI failing/pending/unknown → held out of Bucket 1 until CI passes
```
Typical action sequence per bucket:
```
Bucket 1: merge_pr
Bucket 1: merge_pr (CI already green — safe to merge immediately)
Bucket 2: rebase_pr → merge_pr
Bucket 3: clone → rebase locally → resolve conflicts → force-push → merge_pr
Bucket 4: (dispatch reviewer → on approval, moves to Bucket 1)
Bucket 4: (dispatch reviewer → on approval, moves to Bucket 1 if CI passes)
Bucket 5: (dispatch reviewer → on approval, moves to Bucket 2)
Bucket 6: (dispatch reviewer → on approval, moves to Bucket 3)
```
@@ -83,9 +86,18 @@ npx --yes tsx /app/.opencode/skills/auto-agents-system/scripts/<script>.ts \
`merge_pr` and `rebase_pr` additionally require `--pr <number>`.
`merge_pr` has an optional `--dep error|delete|reverse` flag.
The six `list_prs_*` wrappers accept **only** the four connection flags.
`list_prs` itself accepts optional filter flags (`--state`, `--min-approvals`,
`--max-approvals`, `--mergeable`, `--stale`).
The six `list_prs_*` wrappers accept **only** the four connection flags
their filter parameters are hard-coded and cannot be overridden on the CLI.
`list_prs` itself accepts optional filter flags: `--state`, `--min-approvals`,
`--max-approvals`, `--mergeable`, `--stale`, and `--ci-status`.
**`--ci-status`** accepts a comma-separated whitelist of CI states to include:
`passing`, `failing`, `pending`, `unknown` (any combination). Omit to include
all CI states. Example: `--ci-status passing,pending`
Each PR object in all outputs now includes a `ci_status` field
(`passing` | `failing` | `pending` | `unknown`) derived from the Forgejo
combined commit-status endpoint for the PR's head SHA.
All scripts write JSON to **stdout** and progress/diagnostics to **stderr**.
Exit code `0` = success, `1` = error.
@@ -9,7 +9,8 @@
npx --yes tsx .opencode/skills/auto-agents-system/scripts/list_prs.ts \
--url URL --pat TOKEN --owner OWNER --repo REPO \
[--state open|closed|all] [--min-approvals N] [--max-approvals N] \
[--mergeable true|false] [--stale not_stale|stale_no_conflicts|stale_with_conflicts|stale_unknown]
[--mergeable true|false] [--stale not_stale|stale_no_conflicts|stale_with_conflicts|stale_unknown] \
[--ci-status passing|failing|pending|unknown[,...]]
```
**CRITICAL:** Always invoke the script using bash and the format noted above. **Never** try to use the skill as a task, or invoke it through any other method than by directly calling bash with a command of the form listed above under "Invoke:".
@@ -56,6 +57,7 @@ All filter flags default to "no filter" (i.e. all values pass) when omitted.
| `--max-approvals N` | integer ≥ 0 | (none) | Include only PRs with **≤ N** non-dismissed approvals |
| `--mergeable true\|false` | boolean | (none) | Filter by Forgejo's raw `PR.mergeable` field |
| `--stale VALUE` | string | (none) | Filter by computed `stale_state` (see Stale States below) |
| `--ci-status VALUE[,...]` | string | (none) | Comma-separated whitelist of CI statuses to include (see CI Status below). Omit to include all CI statuses. |
| `-h`, `--help` | — | — | Print usage to stderr and exit 0 |
### Argument Validity Rules
@@ -63,6 +65,7 @@ All filter flags default to "no filter" (i.e. all values pass) when omitted.
- `--min-approvals` and `--max-approvals` may be used together; both constraints apply simultaneously (logical AND).
- `--min-approvals 1 --max-approvals 0` is logically impossible and will return an empty result set — the script does not reject it, it simply matches nothing.
- `--mergeable` filters on the raw API field, not on the computed `stale_state`. Use `--stale` for the semantically richer classification.
- `--ci-status` accepts a comma-separated list of values from `passing`, `failing`, `pending`, `unknown`. A PR must match **any one** of the listed values (logical OR within the list). Multiple `--ci-status` flags are not supported — put all desired values in one comma-separated argument.
- Any unknown flag causes exit code 1 and a usage message.
- `--state` only accepts the three literal values `open`, `closed`, `all`; any other value is passed to the API which may behave unexpectedly (this is not validated client-side).
@@ -76,6 +79,7 @@ Forgejo PR object as returned by the API, augmented with four additional fields:
|-------|------|-------------|
| `approvals_count` | integer | Count of distinct non-dismissed `APPROVED` reviews. Per-reviewer: only the reviewer's most recent review is counted; if they later submitted `REQUEST_CHANGES`, that supersedes the approval and it is not counted. |
| `stale_state` | string | One of five stale states (see below) |
| `ci_status` | string | One of four CI status values (see CI Status below) |
| `priority_rank` | integer | 0 = `Priority/CI Blocker` (highest), …, 5 = `Priority/Backlog`, 6 = unlabelled |
| `priority_label` | string \| null | The matched `Priority/*` label name, or `null` if none present |
@@ -92,6 +96,19 @@ first within the result set.
| `stale_unknown` | Base has advanced AND `PR.mergeable == null` — Forgejo is still computing |
| `compute_error` | `PR.merge_base` or base HEAD SHA is missing; classification impossible |
### CI Status
Derived from `GET /api/v1/repos/{owner}/{repo}/commits/{head_sha}/status` — the Forgejo combined commit-status endpoint. The `state` field of that response is mapped as follows:
| `ci_status` value | Forgejo combined state | Meaning |
|-------------------|----------------------|---------|
| `passing` | `success` | All status checks have reported success |
| `failing` | `failure` \| `error` \| `warning` | At least one check has failed or errored |
| `pending` | `pending` | Checks are queued or still running |
| `unknown` | `""` \| `unknown` | No status checks have been reported for this commit |
Use `--ci-status passing` to restrict to PRs where all CI has passed. Use `--ci-status passing,pending` to include both passing and still-running PRs. Omit `--ci-status` entirely to return PRs regardless of their CI state.
### Priority Rank Table
| `priority_rank` | Label |
@@ -115,6 +132,7 @@ Fetching PRs from https://git.example.com/org/repo (state=open)...
Fetched 42 PR(s). Enriching and filtering...
Fetching HEAD SHAs for 3 unique base branch(es)...
Fetching reviews for 42 PR(s) concurrently (limit=20)...
Fetching CI status for 42 PR(s) concurrently (limit=20)...
Result: 12/42 PR(s) matched the filter criteria.
```
@@ -165,6 +183,9 @@ served by one of the four wrapper scripts:
- Find PRs with exactly 2 approvals: `--min-approvals 2 --max-approvals 2`
- Find approved PRs that are still pending Forgejo's conflict check: `--min-approvals 1 --stale stale_unknown`
- List all PRs regardless of state: `--state all`
- Find open PRs ready to merge (approved, not stale, CI passing): `--min-approvals 1 --stale not_stale --ci-status passing`
- Find PRs with broken CI: `--ci-status failing`
- Find PRs with passing or pending CI: `--ci-status passing,pending`
For the four standard operating buckets used by `pr-merge-pool-supervisor`,
prefer the thin wrappers — they are self-documenting and require fewer flags.
@@ -264,8 +285,16 @@ const filters: Filters = {
state: 'open',
minApprovals: 1,
stale: 'not_stale',
ciStatus: ['passing'], // only PRs with all CI checks passing
};
// ciStatus accepts any combination:
// ciStatus: ['passing'] — CI must have passed
// ciStatus: ['pending'] — CI still running
// ciStatus: ['failing'] — CI has failed
// ciStatus: ['passing', 'pending'] — CI passed or still running
// (omit ciStatus entirely) — any CI state passes through
const prs: AugmentedPR[] = await listPRs(config, filters);
```
@@ -35,6 +35,7 @@ hard-codes the following filters and delegates all logic to that script:
state: 'open'
maxApprovals: 0
stale: 'not_stale'
(no ciStatus filter — all CI states are included)
```
@@ -89,6 +90,7 @@ with four fields:
|-------|------|-------------|
| `approvals_count` | integer | Will always be `0` for results of this script |
| `stale_state` | string | Will always be `not_stale` for results of this script |
| `ci_status` | string | `passing`, `failing`, `pending`, or `unknown`**not filtered**; any CI state is included |
| `priority_rank` | integer | 0 (highest) through 6 (unlabelled) |
| `priority_label` | string \| null | Matched `Priority/*` label name, or `null` |
@@ -107,6 +109,7 @@ Fetching PRs from https://git.example.com/org/repo (state=open)...
Fetched 42 PR(s). Enriching and filtering...
Fetching HEAD SHAs for 3 unique base branch(es)...
Fetching reviews for 42 PR(s) concurrently (limit=20)...
Fetching CI status for 42 PR(s) concurrently (limit=20)...
Result: 8/42 PR(s) matched the filter criteria.
```
@@ -32,6 +32,7 @@ hard-codes the following filters and delegates all logic to that script:
state: 'open'
maxApprovals: 0
stale: 'stale_no_conflicts'
(no ciStatus filter — all CI states are included)
```
@@ -85,6 +86,7 @@ with four fields:
|-------|------|-------------|
| `approvals_count` | integer | Will always be `0` for results of this script |
| `stale_state` | string | Will always be `stale_no_conflicts` for results of this script |
| `ci_status` | string | `passing`, `failing`, `pending`, or `unknown`**not filtered**; any CI state is included |
| `priority_rank` | integer | 0 (highest) through 6 (unlabelled) |
| `priority_label` | string \| null | Matched `Priority/*` label name, or `null` |
@@ -103,6 +105,7 @@ Fetching PRs from https://git.example.com/org/repo (state=open)...
Fetched 42 PR(s). Enriching and filtering...
Fetching HEAD SHAs for 3 unique base branch(es)...
Fetching reviews for 42 PR(s) concurrently (limit=20)...
Fetching CI status for 42 PR(s) concurrently (limit=20)...
Result: 5/42 PR(s) matched the filter criteria.
```
@@ -32,6 +32,7 @@ hard-codes the following filters and delegates all logic to that script:
state: 'open'
maxApprovals: 0
stale: 'stale_with_conflicts'
(no ciStatus filter — all CI states are included)
```
@@ -85,6 +86,7 @@ with four fields:
|-------|------|-------------|
| `approvals_count` | integer | Will always be `0` for results of this script |
| `stale_state` | string | Will always be `stale_with_conflicts` for results of this script |
| `ci_status` | string | `passing`, `failing`, `pending`, or `unknown`**not filtered**; any CI state is included |
| `priority_rank` | integer | 0 (highest) through 6 (unlabelled) |
| `priority_label` | string \| null | Matched `Priority/*` label name, or `null` |
@@ -103,6 +105,7 @@ Fetching PRs from https://git.example.com/org/repo (state=open)...
Fetched 42 PR(s). Enriching and filtering...
Fetching HEAD SHAs for 3 unique base branch(es)...
Fetching reviews for 42 PR(s) concurrently (limit=20)...
Fetching CI status for 42 PR(s) concurrently (limit=20)...
Result: 2/42 PR(s) matched the filter criteria.
```
@@ -1,4 +1,4 @@
# list_prs_ready_to_merge — Open PRs Approved and Up-to-Date
# list_prs_ready_to_merge — Open PRs Approved, Up-to-Date, and CI Passing
**Script name:** `list_prs_ready_to_merge`
@@ -19,9 +19,10 @@ list_prs_ready_to_merge.ts --url URL --pat TOKEN --owner OWNER --repo REPO
```
Returns all **open** pull requests that have **at least 1 non-dismissed
approval** and are **not stale** (the PR's head branch is already current with
its base branch). These PRs are ready to be merged immediately — no rebase
or conflict resolution is required.
approval**, are **not stale** (the PR's head branch is already current with
its base branch), and have **passing CI** (all commit status checks have
succeeded). These PRs are ready to be merged immediately — no rebase,
conflict resolution, or CI wait is required.
This script is a thin wrapper around [`list_prs`](../list_prs/). It
hard-codes the following filters and delegates all logic to that script:
@@ -30,6 +31,7 @@ hard-codes the following filters and delegates all logic to that script:
state: 'open'
minApprovals: 1
stale: 'not_stale'
ciStatus: ['passing']
```
@@ -64,6 +66,7 @@ immediate exit 1 — those flags are not accepted by this wrapper.
| `state` | `open` | Only open PRs are returned |
| `minApprovals` | `1` | At least one non-dismissed `APPROVED` review required |
| `stale` | `not_stale` | `PR.merge_base == current base branch HEAD` |
| `ciStatus` | `['passing']` | All CI checks must have succeeded (`ci_status == 'passing'`) |
**"Not stale"** means `PR.merge_base` (the common ancestor SHA between the PR
head and the base) equals the current HEAD of the base branch. In other words,
@@ -74,6 +77,12 @@ merged into the base branch without a rebase.
review in `APPROVED` state (non-dismissed). A reviewer who approved then later
submitted `REQUEST_CHANGES` is not counted.
**"CI passing"** means the Forgejo combined commit-status for the PR's head
SHA returned `state == 'success'`. PRs with `pending` CI (still running),
`failing` CI (at least one check failed), or `unknown` CI (no checks reported)
are excluded. This prevents merging code before the full test suite has
confirmed it is safe.
## Output
@@ -85,6 +94,7 @@ with four fields:
|-------|------|-------------|
| `approvals_count` | integer | Will always be ≥ 1 for results of this script |
| `stale_state` | string | Will always be `not_stale` for results of this script |
| `ci_status` | string | Will always be `passing` for results of this script |
| `priority_rank` | integer | 0 (highest) through 6 (unlabelled) |
| `priority_label` | string \| null | Matched `Priority/*` label name, or `null` |
@@ -102,6 +112,7 @@ Fetching PRs from https://git.example.com/org/repo (state=open)...
Fetched 42 PR(s). Enriching and filtering...
Fetching HEAD SHAs for 3 unique base branch(es)...
Fetching reviews for 42 PR(s) concurrently (limit=20)...
Fetching CI status for 42 PR(s) concurrently (limit=20)...
Result: 5/42 PR(s) matched the filter criteria.
```
@@ -32,6 +32,7 @@ hard-codes the following filters and delegates all logic to that script:
state: 'open'
minApprovals: 1
stale: 'stale_no_conflicts'
(no ciStatus filter — all CI states are included)
```
@@ -85,6 +86,7 @@ with four fields:
|-------|------|-------------|
| `approvals_count` | integer | Will always be ≥ 1 for results of this script |
| `stale_state` | string | Will always be `stale_no_conflicts` for results of this script |
| `ci_status` | string | `passing`, `failing`, `pending`, or `unknown`**not filtered**; any CI state is included |
| `priority_rank` | integer | 0 (highest) through 6 (unlabelled) |
| `priority_label` | string \| null | Matched `Priority/*` label name, or `null` |
@@ -102,6 +104,7 @@ Fetching PRs from https://git.example.com/org/repo (state=open)...
Fetched 42 PR(s). Enriching and filtering...
Fetching HEAD SHAs for 3 unique base branch(es)...
Fetching reviews for 42 PR(s) concurrently (limit=20)...
Fetching CI status for 42 PR(s) concurrently (limit=20)...
Result: 3/42 PR(s) matched the filter criteria.
```
@@ -31,6 +31,7 @@ hard-codes the following filters and delegates all logic to that script:
state: 'open'
minApprovals: 1
stale: 'stale_with_conflicts'
(no ciStatus filter — all CI states are included)
```
@@ -92,6 +93,7 @@ with four fields:
|-------|------|-------------|
| `approvals_count` | integer | Will always be ≥ 1 for results of this script |
| `stale_state` | string | Will always be `stale_with_conflicts` for results of this script |
| `ci_status` | string | `passing`, `failing`, `pending`, or `unknown`**not filtered**; any CI state is included |
| `priority_rank` | integer | 0 (highest) through 6 (unlabelled) |
| `priority_label` | string \| null | Matched `Priority/*` label name, or `null` |
@@ -109,6 +111,7 @@ Fetching PRs from https://git.example.com/org/repo (state=open)...
Fetched 42 PR(s). Enriching and filtering...
Fetching HEAD SHAs for 3 unique base branch(es)...
Fetching reviews for 42 PR(s) concurrently (limit=20)...
Fetching CI status for 42 PR(s) concurrently (limit=20)...
Result: 2/42 PR(s) matched the filter criteria.
```
@@ -19,6 +19,10 @@
// USAGE (as CLI):
// list_prs.ts --url URL --pat TOKEN --owner OWNER --repo REPO [OPTIONS]
//
// Notable filter options:
// --stale not_stale|stale_no_conflicts|stale_with_conflicts|stale_unknown
// --ci-status passing|failing|pending|unknown (comma-separated, any combination)
//
// USAGE (as module, imported by the four wrapper scripts):
// import { listPRs, parseConnectionArgs } from './list_prs.ts';
// =============================================================================
@@ -35,6 +39,13 @@ export type StaleState =
| 'stale_unknown' // base advanced; mergeable=null (Forgejo pending)
| 'compute_error'; // missing data or branch API failure
// CIStatus — combined commit-status result for the PR head SHA
// 'passing' — all checks reported success (Forgejo combined state == 'success')
// 'failing' — at least one check failed/errored (state == 'failure'|'error'|'warning')
// 'pending' — checks are queued or still running (state == 'pending')
// 'unknown' — no checks have been reported yet (state == ''|'unknown')
export type CIStatus = 'passing' | 'failing' | 'pending' | 'unknown';
export interface Config {
url: string; // Forgejo base URL, no trailing slash
pat: string; // Personal Access Token
@@ -48,6 +59,7 @@ export interface Filters {
maxApprovals?: number; // approvals_count <= N
mergeable?: boolean; // filter on PR.mergeable directly
stale?: StaleState; // filter on computed stale_state
ciStatus?: CIStatus[]; // whitelist: PR's ci_status must be one of these values
}
// Raw shape of a Forgejo pull request object (only the fields we use).
@@ -55,6 +67,7 @@ export interface Filters {
interface RawPR {
number: number;
title: string;
head: { sha: string };
base: { ref: string };
merge_base?: string | null;
mergeable: boolean | null;
@@ -65,6 +78,7 @@ interface RawPR {
export interface AugmentedPR extends RawPR {
approvals_count: number;
stale_state: StaleState;
ci_status: CIStatus;
priority_rank: number; // 0 = CI Blocker (highest), higher = lower; PRIORITY_NONE if unlabelled
priority_label: string | null; // matched Priority/* label name, or null if none
}
@@ -80,6 +94,10 @@ interface BranchInfo {
commit: { id: string };
}
interface CombinedStatus {
state: string; // 'success' | 'pending' | 'failure' | 'error' | 'warning' | '' | 'unknown'
}
// ─────────────────────────────────────────────────────────────────────────────
// CONSTANTS
// ─────────────────────────────────────────────────────────────────────────────
@@ -271,6 +289,17 @@ function computeStaleState(
return 'stale_unknown';
}
// ─────────────────────────────────────────────────────────────────────────────
// mapCIState — translate Forgejo's combined commit-status state string into
// our canonical CIStatus enum value
// ─────────────────────────────────────────────────────────────────────────────
function mapCIState(state: string): CIStatus {
if (state === 'success') return 'passing';
if (state === 'pending') return 'pending';
if (state === 'failure' || state === 'error' || state === 'warning') return 'failing';
return 'unknown'; // '' | 'unknown' | anything else → no checks reported
}
// ─────────────────────────────────────────────────────────────────────────────
// listPRs — fetch, enrich, filter, and return matching PRs
//
@@ -347,12 +376,34 @@ export async function listPRs(
REVIEW_CONCURRENCY,
);
// ── Step 4: Compute fields, apply filters, collect results ────────────────
// ── Step 4: Fetch CI (combined commit-status) for every PR concurrently ───
// Uses GET /api/v1/repos/{owner}/{repo}/commits/{sha}/status which returns
// the aggregated combined state for all status checks on the head commit.
log(`Fetching CI status for ${rawPRs.length} PR(s) concurrently (limit=${REVIEW_CONCURRENCY})...`);
const allCIStatuses = await pLimit<CIStatus>(
rawPRs.map(pr => async () => {
try {
const combined = await apiGet<CombinedStatus>(
`${repoBase}/commits/${encodeURIComponent(pr.head.sha)}/status`,
config.pat,
);
return mapCIState(combined.state ?? '');
} catch {
log(`WARNING: Could not fetch CI status for PR #${pr.number}; treating as 'unknown'`);
return 'unknown' as CIStatus;
}
}),
REVIEW_CONCURRENCY,
);
// ── Step 5: Compute fields, apply filters, collect results ────────────────
const results: AugmentedPR[] = [];
for (let i = 0; i < rawPRs.length; i++) {
const pr = rawPRs[i];
const approvalsCount = countApprovals(allReviews[i]);
const ciStatus = allCIStatuses[i];
const baseHead = branchCache.get(pr.base?.ref ?? '') ?? null;
const staleState = computeStaleState(
pr.merge_base ?? null,
@@ -365,12 +416,14 @@ export async function listPRs(
if (filters.maxApprovals !== undefined && approvalsCount > filters.maxApprovals) continue;
if (filters.mergeable !== undefined && pr.mergeable !== filters.mergeable) continue;
if (filters.stale !== undefined && staleState !== filters.stale) continue;
if (filters.ciStatus !== undefined && !filters.ciStatus.includes(ciStatus)) continue;
const { rank: priorityRank, label: priorityLabel } = prPriority(pr);
results.push({
...pr,
approvals_count: approvalsCount,
stale_state: staleState,
ci_status: ciStatus,
priority_rank: priorityRank,
priority_label: priorityLabel,
});
@@ -444,11 +497,12 @@ function parseFullArgs(argv: string[]): { config: Config; filters: Filters } {
case '--pat': config.pat = val; i++; break;
case '--owner': config.owner = val; i++; break;
case '--repo': config.repo = val; i++; break;
case '--state': filters.state = val as Filters['state']; i++; break;
case '--min-approvals': filters.minApprovals = Number(val); i++; break;
case '--max-approvals': filters.maxApprovals = Number(val); i++; break;
case '--mergeable': filters.mergeable = val === 'true'; i++; break;
case '--stale': filters.stale = val as StaleState; i++; break;
case '--state': filters.state = val as Filters['state']; i++; break;
case '--min-approvals': filters.minApprovals = Number(val); i++; break;
case '--max-approvals': filters.maxApprovals = Number(val); i++; break;
case '--mergeable': filters.mergeable = val === 'true'; i++; break;
case '--stale': filters.stale = val as StaleState; i++; break;
case '--ci-status': filters.ciStatus = val.split(',').map(s => s.trim()) as CIStatus[]; i++; break;
case '-h':
case '--help':
printUsage();
@@ -473,6 +527,7 @@ function printUsage(): void {
process.stderr.write(`\
Usage:
list_prs.ts --url URL --pat TOKEN --owner OWNER --repo REPO [OPTIONS]
[--ci-status passing|failing|pending|unknown[,...]]
REQUIRED:
--url URL Forgejo base URL, no trailing slash (e.g. https://git.example.com)
@@ -487,11 +542,20 @@ OPTIONAL FILTERS (all default to no filter):
--mergeable true|false Filter on PR.mergeable field
--stale not_stale|stale_no_conflicts|stale_with_conflicts|stale_unknown
Filter on computed staleness state
--ci-status passing|failing|pending|unknown[,...]
Comma-separated whitelist of CI statuses to include.
passing = all checks succeeded (Forgejo state: success)
failing = at least one check failed/errored (failure|error|warning)
pending = checks are queued or running (state: pending)
unknown = no checks reported yet (state: ''|unknown)
Multiple values: --ci-status passing,pending
Omit flag entirely to include all CI statuses.
OUTPUT (stdout): JSON array sorted by priority (highest first); each object is
the full Forgejo PR plus:
.approvals_count integer distinct non-dismissed APPROVED review count
.stale_state string one of the stale states listed above
.ci_status string passing|failing|pending|unknown (see --ci-status above)
.priority_rank integer 0=CI Blocker, 1=Critical, 2=High, 3=Medium,
4=Low, 5=Backlog, 6=none (unlabelled)
.priority_label string|null matched Priority/* label name, or null
@@ -1,6 +1,6 @@
#!/usr/bin/env -S npx --yes tsx
// =============================================================================
// list_prs_ready_to_merge.ts — Open PRs approved and up-to-date with base
// list_prs_ready_to_merge.ts — Open PRs approved, up-to-date, and CI passing
//
// Part of: .opencode/skills/auto-agents-system/scripts/
//
@@ -9,15 +9,22 @@
// Returns all OPEN pull requests that:
// • have at least 1 review approval (approvals_count >= 1)
// • are NOT stale (stale_state == not_stale)
// • have passing CI (ci_status == 'passing')
//
// These PRs are ready to be merged immediately — they are approved and their
// branch is already up-to-date with the base branch, so no rebase or conflict
// resolution is required before merging.
// These PRs are ready to be merged immediately — they are approved, their
// branch is already up-to-date with the base branch, and all CI checks have
// passed. No rebase, conflict resolution, or CI wait is required.
//
// CI filter rationale: merging a PR with failing or pending CI would push
// broken code to the base branch. Only 'passing' is accepted here. PRs
// with 'pending' or 'failing' CI appear in none of the approved buckets
// until their CI resolves, preventing premature merges.
//
// Hard-coded filters (not configurable):
// state: 'open' only open PRs are considered
// minApprovals: 1 at least one non-dismissed approval required
// stale: 'not_stale' PR head must be current with its base branch
// state: 'open' only open PRs are considered
// minApprovals: 1 at least one non-dismissed approval required
// stale: 'not_stale' PR head must be current with its base branch
// ciStatus: ['passing'] all CI checks must have succeeded
//
// USAGE:
// list_prs_ready_to_merge.ts --url URL --pat TOKEN --owner OWNER --repo REPO
@@ -33,6 +40,7 @@ async function main(): Promise<void> {
state: 'open',
minApprovals: 1,
stale: 'not_stale',
ciStatus: ['passing'],
});
process.stdout.write(JSON.stringify(prs, null, 2) + '\n');
}