AI-Drafted GitLab Merge Request and CODEOWNERS Governance
Use AI to draft GitLab MR templates, CODEOWNERS path rules, and approval policies that CI actually enforces — so risky paths never merge unreviewed again.
- #gitlab
- #ci-cd
- #ai
- #codeowners
- #merge-requests
I found out our governance was fiction the hard way. A merge request touched our Terraform state backend and the auth middleware in the same change, got a single thumbs-up from someone who only looked at the CSS, and shipped to production by lunch. No reviewer from the platform team. No reviewer from security. When I went looking for the guardrails, there weren’t any: our CODEOWNERS file was an empty stub someone committed in 2022, and the merge request template was a blank text box that everyone ignored. The risky paths merged with no review because nobody had ever told GitLab which paths were risky.
That afternoon I rebuilt the whole thing — and I leaned on an AI assistant to do the tedious parts fast. What follows is the setup I landed on, plus an honest account of where AI helped and where it would have quietly burned me.
Why empty governance files are worse than none
A blank CODEOWNERS and a blank MR template feel harmless. They’re not. They create the appearance of process — there’s a file there, the template box exists — while enforcing nothing. Developers assume someone configured it. Reviewers assume the right people were pinged. Nobody actually owns the dangerous code paths.
GitLab gives you three real levers, and they only work together:
- CODEOWNERS maps file paths to the people or groups who must review them.
- Merge request approval rules turn those owners into required approvals (this needs a Premium/Ultimate tier for the enforcement, but CODEOWNERS suggestions work on Free).
- CI jobs catch the stuff policy can’t, like an empty MR description or a missing changelog entry.
Skip any one and the other two leak.
Letting AI read the repo tree and propose owners
The slowest part of writing CODEOWNERS is mapping your directory structure to the teams that actually know each area. This is exactly the kind of structured, boring inference an AI is good at. I piped my tree into a model and asked it to draft sections:
find . -type d -not -path '*/.git/*' -maxdepth 3 | sort > tree.txt
Then a prompt along the lines of: “Here is a repo directory tree and a list of GitLab groups (@platform, @security, @frontend, @data). Draft a CODEOWNERS file using GitLab sections. Be conservative — when ownership is ambiguous, leave a comment instead of guessing.”
The draft it produced was genuinely useful as a starting point. Treat it the way you’d treat a fast junior engineer’s first pass: it’s a great proposal that you must read line by line before you trust it. I keep a few reusable prompts for this in my prompt library, and the broader prompt packs have governance-specific ones. Whether you drive it from Claude, Copilot, or Cursor, the workflow is the same: generate, then verify.
Pro Tip: Never paste real CI/CD variables, deploy tokens, or .gitlab-ci.yml secrets into a chat prompt while you’re “just showing it the repo.” Strip them first. AI needs your structure, not your credentials.
A real CODEOWNERS file with sections
GitLab supports sections (the bracketed headers), which let you group rules and even set per-section approval counts. Here’s roughly what I shipped, after editing the AI draft:
# CODEOWNERS
# Default owners for everything, unless a later rule overrides.
* @platform-leads
[Infrastructure][2] @platform @sre
/terraform/ @platform
/.gitlab-ci.yml @platform @sre
/helm/ @platform
[Security] @security
/auth/ @security
/**/secrets.yaml @security
/middleware/auth*.ts @security
^[Docs] @tech-writers
/docs/ @tech-writers
*.md @tech-writers
Three things worth knowing, because the AI got two of them subtly wrong on the first pass:
- Order matters. The last matching pattern wins, just like
.gitignore. The catch-all*goes at the top. [Section][2]requires two approvals from that section’s owners. The model initially put the2in the wrong bracket position — exactly the kind of plausible-but-wrong detail you have to catch yourself.^[Optional]with a caret marks a section as optional — owners are requested but not required. Handy for docs.
A wrong rule here isn’t a syntax error you’ll notice — it’s a review silently routed to the wrong team. If /middleware/auth*.ts had landed under @frontend instead of @security, every auth change would skip the security review and nobody would see a red X. That’s why this file gets human eyes before merge, every time.
The merge request template
The MR template lives at .gitlab/merge_request_templates/Default.md. Naming a file Default.md makes it the default for new MRs. I asked AI to draft a checklist tuned to our risky paths — it pulled sensible categories straight from the CODEOWNERS sections:
## What does this change do?
<!-- One paragraph. If you can't, the MR is probably too big. -->
## Risk & blast radius
- [ ] Touches infrastructure (`terraform/`, `helm/`, CI)
- [ ] Touches auth / security paths
- [ ] Requires a database migration
- [ ] Changes a public API contract
## Verification
- [ ] Tested locally
- [ ] Added or updated automated tests
- [ ] Updated `CHANGELOG.md`
## Reviewers
<!-- CODEOWNERS auto-assigns. Add anyone else who should look. -->
/assign_reviewer @me
Those quick actions (/assign_reviewer, you can also use /label ~needs-review) run automatically when the MR is created. Small touch, big consistency win.
Making CI fail on empty descriptions
A template only helps if people fill it in. This is where .gitlab-ci.yml earns its keep. GitLab exposes merge-request metadata through CI_MERGE_REQUEST_* variables, but only in merge request pipelines — so the rules guard is mandatory or the job errors on branch pipelines:
mr:guard:
stage: test
image: alpine:3.20
rules:
- if: '$CI_PIPELINE_SOURCE == "merge_request_event"'
script:
- |
DESC="$CI_MERGE_REQUEST_DESCRIPTION"
# Strip HTML comments and whitespace, then measure length.
CLEAN=$(printf '%s' "$DESC" | sed 's/<!--.*-->//g' | tr -d '[:space:]')
if [ "${#CLEAN}" -lt 40 ]; then
echo "MR description is empty or too short. Fill in the template."
exit 1
fi
case "$CI_MERGE_REQUEST_TITLE" in
WIP:*|Draft:*) echo "Still a draft — not ready to merge."; exit 1 ;;
esac
Because it’s gated by merge_request_event, it never runs (and never fails) on a plain push to a feature branch. It runs exactly when a human opens or updates the MR.
A danger-style lint job
For the policy checks that are too nuanced for a one-liner, I run a “danger-style” review job — warnings and hard failures on the diff itself. You can use the actual Danger gem, but a small script covers most needs:
danger:lint:
stage: test
image: alpine:3.20
rules:
- if: '$CI_PIPELINE_SOURCE == "merge_request_event"'
script:
- apk add --no-cache git
- git fetch origin "$CI_MERGE_REQUEST_TARGET_BRANCH_NAME"
- CHANGED=$(git diff --name-only "origin/$CI_MERGE_REQUEST_TARGET_BRANCH_NAME"...HEAD)
- |
# Migrations must ship with a changelog note.
if echo "$CHANGED" | grep -q '^db/migrate/' && ! echo "$CHANGED" | grep -q 'CHANGELOG'; then
echo "Migration without a CHANGELOG entry. Add one."; exit 1
fi
# Warn loudly when security paths are touched.
if echo "$CHANGED" | grep -qE '^(auth|middleware)/'; then
echo "::warning:: Security-sensitive paths changed — @security review required."
fi
This is also a perfect spot to let AI draft the rules: describe the policy in plain English, have it generate the grep conditions, then test the script against a known-bad branch before you trust it. I sanity-check these the same way I’d sanity-check any generated code in our code review dashboard — generate fast, verify deliberately.
Wiring up required approvals
CODEOWNERS suggests reviewers; approval rules make them mandatory. In Settings → Merge requests → Approval rules, enable “Require approval from code owners” and turn on “Prevent approval by the author” and “Reset approvals when new commits are pushed”. Then protect main so it can’t be merged without a green pipeline.
The combination is what closes the loop: CODEOWNERS routes the review, approval rules force it, and the CI jobs above stop the MR before a reviewer ever wastes time on a description-less change. None of the three is optional.
Pro Tip: Add a required-codeowners approval rule scoped to your protected branches and set “approvals required” to match the [Section][N] counts in CODEOWNERS. If those two numbers drift apart, your governance lies again — keep them in sync in code review.
Conclusion
The fix for ghost governance wasn’t more process — it was making the process real: CODEOWNERS that names the dangerous paths, an MR template people can’t skip, and CI that fails loudly when they try. AI made the grind of mapping a repo tree to owners and drafting checklists genuinely fast, and that’s its sweet spot here: a quick junior engineer that proposes, while you dispose. It will route auth reviews to the frontend team if you let it, and it should never see your CI secrets. Read every line, run the jobs against a bad branch, and only then merge. If you want a head start, the GitLab CI/CD guides and reusable prompts above will save you the afternoon I lost.
Download the Free 500-Prompt DevOps AI Toolkit
500 battle-tested, copy-paste AI prompts engineered by a senior systems engineer — every one with fill-in placeholders and safety/back-out notes. Drop your email and it's yours.
- 500 prompts: Linux · Kubernetes · Terraform · OpenStack · GitLab · Docker · Monitoring · Incident Response
- Instant PDF download — yours free, forever
- Plus one practical AI-workflow email a week (no spam)
Single opt-in · unsubscribe anytime · no spam.