feat(bin): report GitLab MR target, approvals, merge status, and project CI settings in fm-pr-status.sh - #41
Merged
Merged
Conversation
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.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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.shnow accepts full merge request URLs from any host. It parses them withfm-pr-lib.shand sends every read to that host withglab api --hostname. Output is nowkey=valuecolumns:into=<target>,ci=<head verdict>,approval=, the verbatimmerge=<detailed_merge_status>,conflicts=,head=, and the project'sjobs=enabled|DISABLED|?andmust-succeed=yes|no|?. The project is read once for each run of consecutive rows from the same project.approvedflag. It printsapproved(n),not-required,NOT-APPROVED[(k-left)],none-givenorUNVERIFIED, anddetailed_merge_status=not_approvedalways wins. A merged MR printsmerged into=<target> on=<date>. Any part that could not be read prints?orUNVERIFIEDand makes the exit status non-zero, so the tool never guesses. It stays read-only.AGENTS.mdgains a line telling agents to read GitLab MR state through this tool, and thedocs/scripts.mdrow is updated.tests/fm-pr-status.test.shcovers the new columns, URL input and failure paths using mockedglabresponses. Follow-up:bin/fm-pr-merge.shkeeps 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
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 noid. 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 omitsjobs_enabledandonly_allow_merge_if_pipeline_succeeds. In that case the row printsjobs=? 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 setsbuilds_access_level: enabledbut omitsjobs_enabledalso printsjobs=?with exit 0, even though the value can be determined. The same gap applies tointo=?: 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 asjobs=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 recordedManual CLI run with a reduced project view (settings omitted) gives jobs=? must-succeed=? and exit 1Manual CLI run where the MR read fails gives UNREACHABLE and exit 1Searched 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.