Skip to content

feat(bin): report GitLab MR target, approvals, merge status, and project CI settings in fm-pr-status.sh - #41

Merged
alandomio merged 2 commits into
mainfrom
fm/fm-mrstat-gitlab
Sep 26, 2026
Merged

alandomio merged 2 commits into
mainfrom
fm/fm-mrstat-gitlab

Conversation

@alandomio

Copy link
Copy Markdown
Owner

Intent

The captain approved (26/09/2026) building ONE firstmate tool in bin/ that reports the real state of GitLab merge requests, because firstmate derived the state and mergeability of 8 GitLab MRs by hand at least 14 times and twice read it wrong. Given one or more GitLab MR URLs or !, it prints one line per MR with: state (opened/merged/closed), target branch, conflicts, approvals (distinguishing 'approved' from 'no approval required'), detailed_merge_status, and CI on the REAL head - a pipeline that ran on the MR's current head sha (green(head), FAILED(head), running(head), manual(head)) versus NO-HEAD-RUN or a merge-result-only pipeline, because GitLab's head_pipeline is often a merge-result run on a synthetic commit and a green badge can sit on a head that failed or never ran. It also shows whether the project has CI jobs enabled and 'Pipelines must succeed'. Read-only tool: it never merges, approves or comments. Constraints: firstmate shared tracked material, follow firstmate-coding-guidelines (one-owner rule, header-owned help, tests, docs, bash portability incl. stock macOS bash 3.2); use glab (glab api) for GitLab, never print tokens, scrub control characters before parsing JSON; behavioural tests with recorded/mocked API responses, no network in tests. Consider whether bin/fm-pr-merge.sh's GitLab guard should reuse the same head-pipeline logic; if so keep one owner, otherwise note it as a follow-up in the PR. Decisions made: bin/fm-pr-status.sh (PR #11) already owned the head-sha CI verdict (the brief suggested a new bin/fm-mrstat.sh name only as an example), so per the one-owner rule it was EXTENDED rather than duplicated: accepts full MR URLs on any host (parsed by fm-pr-lib.sh fm_pr_url_parse, all reads via glab api --hostname ), adds into=, verbatim merge=<detailed_merge_status>, conflicts= column, approval= from the approvals endpoint (approved(n) / not-required / NOT-APPROVED[(k-left)] / none-given / UNVERIFIED, with detailed_merge_status=not_approved always winning), and the project's jobs=enabled|DISABLED|? and must-succeed=yes|no|? (read once per consecutive project). Output changed to key=value columns; merged rows print 'merged into= on='. Any unread part prints ?/UNVERIFIED and makes the exit non-zero rather than guessing. The existing python3 JSON parsing was kept for consistency within the script (it scrubs control characters first). AGENTS.md section 7 gains a single pointer line so firstmate reads MR state through this tool instead of hand-rolled glab/jq (the tool existed but was undiscoverable); docs/scripts.md row updated. fm-pr-merge.sh deliberately NOT refactored to share the logic: it is stricter (requires head_pipeline itself successful at the live head, never accepts a merge-result-only green) and safety-critical, and uses jq; sharing is a PR follow-up. PR description must not include a Generated-with trailer or orchestration vocabulary.

What Changed

  • bin/fm-pr-status.sh now accepts full merge request URLs from any host. It parses them with fm-pr-lib.sh and sends every read to that host with glab api --hostname. Output is now key=value columns: into=<target>, ci=<head verdict>, approval=, the verbatim merge=<detailed_merge_status>, conflicts=, head=, and the project's jobs=enabled|DISABLED|? and must-succeed=yes|no|?. The project is read once for each run of consecutive rows from the same project.
  • Approval is read from the approvals endpoint, not from the approved flag. It prints approved(n), not-required, NOT-APPROVED[(k-left)], none-given or UNVERIFIED, and detailed_merge_status=not_approved always wins. A merged MR prints merged into=<target> on=<date>. Any part that could not be read prints ? or UNVERIFIED and makes the exit status non-zero, so the tool never guesses. It stays read-only.
  • AGENTS.md gains a line telling agents to read GitLab MR state through this tool, and the docs/scripts.md row is updated. tests/fm-pr-status.test.sh covers the new columns, URL input and failure paths using mocked glab responses. Follow-up: bin/fm-pr-merge.sh keeps its own stricter GitLab head-pipeline guard for now. Sharing that logic with this tool is deferred.

Risk Assessment

✅ Low: The fix round is minimal and correct. Every ? in the project CI settings or the target branch now sets STATUS=1 through direct, non-subshell calls, including cached reuse and merged rows. builds_access_level enabled/private is read as jobs=enabled, and behavioural tests with recorded fixtures cover each new path. It stays within the read-only tool's stated intent.

Testing

I ran the script's own behavioural suite (tests/fm-pr-status.test.sh, fake glab, no network) and all 34 cases pass, including regression tests for the round-1 fix (reduced project view, missing target branch). I then drove the CLI end to end with a recording fake glab over 5 realistic MRs to produce a transcript. It shows each verdict column, NO-HEAD-RUN under a green merge-result badge, FAILED(head) under a green badge, and the URL's own host being used. It also shows that only read-only glab api GETs are made, with project settings read once per consecutive project, and that the ?/UNREACHABLE cases exit 1. Stock macOS bash 3.2 is not available on this Linux host, so portability was checked only by searching the added lines for bash-4-only constructs, and none were found. No temporary files are left in the worktree.

Evidence: fm-pr-status.sh CLI transcript: multi-MR output, glab call log (read-only), reduced-project and unreachable failure exits

Source: fm-pr-status.sh CLI transcript: multi-MR output, glab call log (read-only), reduced-project and unreachable failure exits

$ fm-pr-status.sh acme/api!11 acme/api!12 acme/api!13 acme/api!14 https://gitlab.example.org/acme/docs/-/merge_requests/5
api!11                              opened  into=main ci=NO-HEAD-RUN approval=not-required merge=mergeable conflicts=no head=1111aaaa jobs=enabled must-succeed=yes - badge is merge-result (success on 9999ffff)
api!12                              opened  into=release/2.0 ci=FAILED(head) approval=approved(2) merge=ci_must_pass conflicts=no head=1212bbbb jobs=enabled must-succeed=yes - badge is merge-result (success on 8888eeee)
api!13                              opened  into=main ci=green(head) approval=NOT-APPROVED(1-left) merge=not_approved conflicts=YES head=1313cccc jobs=enabled must-succeed=yes
api!14                              merged  into=main on=2026-09-20
docs!5                              opened  into=main ci=manual(head) approval=not-required merge=draft_status conflicts=no head=0505eeee jobs=DISABLED must-succeed=no - DRAFT - cannot merge until marked ready
[exit 0]

# every glab call made (read-only GETs only; project settings read once per consecutive project):
glab api projects/acme%2Fapi/merge_requests/11
glab api projects/acme%2Fapi/merge_requests/11/pipelines?per_page=30
glab api projects/acme%2Fapi/merge_requests/11/approvals
glab api projects/acme%2Fapi
glab api projects/acme%2Fapi/merge_requests/12
glab api projects/acme%2Fapi/merge_requests/12/pipelines?per_page=30
glab api projects/acme%2Fapi/merge_requests/12/approvals
glab api projects/acme%2Fapi/merge_requests/13
glab api projects/acme%2Fapi/merge_requests/13/pipelines?per_page=30
glab api projects/acme%2Fapi/merge_requests/13/approvals
glab api projects/acme%2Fapi/merge_requests/14
glab api projects/acme%2Fdocs/merge_requests/5 --hostname gitlab.example.org
glab api projects/acme%2Fdocs/merge_requests/5/pipelines?per_page=30 --hostname gitlab.example.org
glab api projects/acme%2Fdocs/merge_requests/5/approvals --hostname gitlab.example.org
glab api projects/acme%2Fdocs --hostname gitlab.example.org

# reduced project view (non-member token): settings omitted
$ fm-pr-status.sh acme/api!13
api!13                              opened  into=main ci=green(head) approval=NOT-APPROVED(1-left) merge=not_approved conflicts=YES head=1313cccc jobs=? must-succeed=?
[exit 1]

# MR read fails
$ fm-pr-status.sh acme/api!99
api!99                              UNREACHABLE (no data - check repo/number/auth)
[exit 1]

Pipeline

Updates from git push no-mistakes

✅ **intent** - passed

✅ No issues found.

✅ **Rebase** - passed

✅ No issues found.

🔧 **Review** - 1 issue found → auto-fixed ✅
  • ⚠️ bin/fm-pr-status.sh:181 - The tool can print ? and still exit 0. The intent requires that "Any unread part prints ?/UNVERIFIED and makes the exit non-zero rather than guessing", but fm_prstat_project only sets STATUS=1 when the project read fails outright or the response has no id. GitLab returns a reduced project representation to non-members and low-permission tokens (for example a public project read by a Guest), and that response omits jobs_enabled and only_allow_merge_if_pipeline_succeeds. In that case the row prints jobs=? must-succeed=? and the run exits 0. A caller that gates on the exit code would then treat the CI settings as verified when they were not. A response that sets builds_access_level: enabled but omits jobs_enabled also prints jobs=? with exit 0, even though the value can be determined. The same gap applies to into=?: a missing target_branch at line 266 prints ? without failing the run. Fix: set STATUS=1 whenever the project value or the target contains ?, and treat builds_access_level enabled/private as jobs=enabled. The header currently says "? when the project could not be read or did not say", so whether a readable-but-silent response should fail the run needs the author's call.

🔧 Fix: Fail fm-pr-status run on unknown project settings or target
✅ Re-checked - no issues remain.

✅ **Test** - passed

✅ No issues found.

  • bash tests/fm-pr-status.test.sh: all 34 behavioural cases (A–AH) pass against a fake glab with no network. They include AD (URL host), AF (approval verdicts), AG (project settings, one read per project, reduced view gives exit 1, builds_access_level=private counts as enabled) and AH (merged row, missing target gives into=? and exit 1)
  • Manual end-to-end CLI run: a fake glab served fixtures for 5 MRs (green merge-result badge with no head run, green badge over a failed head, green head needing approval with conflicts, a merged MR, and a full URL on another host with CI disabled and a draft whose head run is manual); the output and every glab call were recorded
  • Manual CLI run with a reduced project view (settings omitted) gives jobs=? must-succeed=? and exit 1
  • Manual CLI run where the MR read fails gives UNREACHABLE and exit 1
  • Searched the diff's added lines for bash-4-only constructs (associative arrays, mapfile, case-modification expansions, ;;&, |&, coproc) and found none
✅ **Document** - passed

✅ No issues found.

✅ **Lint** - passed

✅ No issues found.

✅ **Push** - passed

✅ No issues found.

Alan Domio added 2 commits September 26, 2026 15:46
…-status.sh

Accept full GitLab merge request URLs and read them from the URL's own
host. Each row now names the target branch, the verbatim
detailed_merge_status, conflicts, and approvals read from the approvals
endpoint so an approval is told apart from none being required. The
project's CI capability and Pipelines must succeed setting are shown,
and any unread part fails the run instead of being guessed.

Point AGENTS.md at the tool so merge request state is read through it
rather than derived by hand.
@alandomio
alandomio merged commit 30f2bf7 into main Sep 26, 2026
24 of 25 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant