fix(ci): fail fast and isolate per-MR failures in the stale-MR sweeper
What does this MR do and why?
The stale-MR sweeper has failed every scheduled run since 2026-07-17: its token cannot write to this project, and the first failed write aborts the run with every older merge request never evaluated. A permission preflight now fails in the first second naming the cause, and per-merge-request error handling keeps one failure from hiding the rest. Dry runs keep working and downgrade the preflight to a warning.
Related Issues
Relates to #1142 (closed). The token replacement is maintainer-only and stays on the issue.
Testing
Reproduced the abort, then ran the patched script against live fixture MRs: the
preflight fails fast on the broken token and warns under DRY_RUN, a forced write
failure continues to the next MR and exits 1 with a count, and a full warn-then-close
cycle with a Developer token is unchanged. shellcheck clean. Probe matrix below.
Performance Analysis
One additional API read per run.
- This merge request does not introduce any performance regression. If a performance regression is expected, explain why.
Agent context — long-form analysis, file-by-file walkthroughs, profiler output, alternatives considered
The preflight reads GET /projects/:id/members/all/:user_id. Against tokens in five
grant shapes it matched the actual write outcome in all five; permissions on
GET /projects/:id reports null for invited-group grants and would false-negative a
working sweep.
| token shape | permissions |
members/all |
label write |
|---|---|---|---|
| minted on the project, Developer | proj=30 | 30 | OK |
| minted in another project | null | 404 | 403 |
| group token on an ancestor group | grp=30 | 30 | OK |
| group token via invited group | null | 30 | OK |
| minted on the project, Reporter | proj=20 | 20 | 403 |
Reporter also receives 403 on close, so Developer is the floor, matching the setup
comment in .gitlab/ci/stale-mr.yml.
The caller now inspects the per-MR function's status, which suspends set -e inside
it, so every API-touching step carries an explicit || return 1: read failures skip
the MR instead of acting on defaulted timestamps, write failures move to the next MR,
and the run exits non-zero with a count when any MR failed. Under DRY_RUN the
preflight warns instead of failing so previews still run during a token rotation.