facebook / facebook/astryx

Prevent stale PRs from breaking main: staleness gate + auto-refresh cron

Open
#4,086 3 comments 0 reactions 0 assignees View on GitHub
enhancement needs-scoping
Dominant language
TypeScript
Stars
13k
Forks
1.1k
Avg merge
1d 15h
Merged PRs (30d)
690

Description

## Problem

CI "green" means *passed against the main the PR was branched from* — **not** *passes against current main*. When main moves under a PR, that green goes stale. Branch protection on `main` is **non-strict** (`required_status_checks.strict = false`), so GitHub will merge a stale-green PR without re-checking it against the latest main.

This bit us repeatedly in a single review session:

- **#3805** (a test PR) was green on **Jul 11**. On **Jul 17**, #3922 changed `formatFilterValue`'s signature (added a required `t: TranslatorFn`). #3805 merged **Jul 19** on its 8-day-old green → **broke `main`'s `test`/`typecheck`** (semantic merge conflict; the call now passed `timezoneID` into the `t` slot). Fixed by #4084.
- **#4082** and **#3800** then inherited the red main and couldn't merge until rebased.
- **#4085** was a *second-order* collision: #3842's new doc-literals guard + #3881's `@deprecated` union member were each green in isolation but conflicted on main.

Every one of these traces to the same root: **a PR merged (or evaluated) against a main it hadn't been re-tested against.**

## Data (last 150 merged PRs, gathered Jul 19)

| metric | value |
|---|---|
| median open→merge | ~6h |
| p90 | ~2.1d |
| p95 | ~3.8d |
| p99 | ~7.7d |
| main velocity | ~20–30 commits/day (~75/week) |

So ~95% of PRs merge within ~4 days, and **a single day of main is ~20–30 commits** of drift surface. The healthy population is already fresh; the danger is the long tail.

## Why not just turn on strict mode

Strict branch protection ("require branches up to date before merging") would force a rebase + full CI re-run on *every* PR for *every* main commit — at 20–30 commits/day that's constant churn and a miserable contributor experience. We want drift protection **without** that tax.

## Proposal — two cooperating pieces

### 1. Staleness gate (guardrail; a new required status)
Extend the existing `review-signal.yml` pattern with a `not-stale` commit status:
- Compute how far the PR's **merge-base** is behind `main` (in days + commits).
- 🟢 **pass** when merge-base is **< 1 day** behind (recent enough that drift risk is low).
- 🟡 **advisory** at 1–7 days ("rebase recommended" — nudge, does not block).
- 🔴 **fail** past **7 days** (blocks auto-merge; requires a manual refresh + fresh CI).
- Add `not-stale` as a **required status check** so a stale PR cannot merge — the targeted equivalent of strict mode, only biting the dangerous tail.

### 2. Auto-refresh cron (ergonomics; keeps PRs mergeable)
A scheduled workflow (mirroring `prune-branches.yml`: daily/every-few-hours cron + `workflow_dispatch`) that:
- Lists open, non-draft, **approved or auto-merge-armed** PRs whose branch is behind main.
- Updates the branch (`gh pr update-branch` / merge-main), which re-triggers CI on the fresh head.
- **Skips conflicts** (labels/comments instead of forcing).
- **Fork handling** (most contributor PRs here are forks — #3800, #3805): the cron can't push to a fork. Either rely on `gh pr update-branch` (works via the update API when *Allow edits by maintainers* is on) or, where it can't, **comment a nudge** and let the gate hold the line.

Together: the cron *keeps* PRs fresh so they stay mergeable; the gate *catches* anything that slips through.

## Proposed cutoffs (for discussion)

| behind main | signal | action |
|---|---|---|
| < 1 day | 🟢 fresh | mergeable |
| 1–7 days | 🟡 stale-ish | auto-refresh + advisory |
| > 7 days | 🔴 stale | block; manual rebase required |

## Open questions for the team

1. **Merge cutoff** — is **< 1 day** the right freshness bar for merge, or looser (e.g. 2 days)?
2. **Hard block** — **7 days** to `not-stale=fail`, or shorter?
3. **Cron cadence** — every 3–4h vs daily? (Faster = fewer PRs ever cross the line.)
4. **Fork strategy** — auto-update via API where possible + nudge-comment otherwise, or nudge-only for forks?
5. **`allow_update_branch` is currently `false`** on the repo — enabling it is a prerequisite for `gh pr update-branch`. OK to turn on?
6. Scope the auto-refresh to **approved/auto-merge-armed** PRs only (don't churn WIP), or all non-draft?

Implementation PR to follow once cutoffs + fork strategy are agreed. Will mirror the `review-signal.yml` conventions (`actions/github-script`, `permissions: {}`, per-PR concurrency, `workflow_dispatch` recovery lever).

Contributor guide

Open the contributing guide

Assessment

This issue has not been assessed yet.

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.