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.

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.

Merge request reports

Loading