Inspiration
Small internal tools grow. Somebody writes a lunch list for the team, and a year later it has a login, customer data, a nightly job and a production container, and the sales team takes phone orders with it. Nobody registered it, because nobody noticed the day it changed.
Citizen developers, vibe coding - all that is seen as a competitive advantage to a company, but there still should be controls in place. Something that started as "test tool" was later found it to have become a full-blown inventory and job management system, partly replacing real ERP.
The usual answer is a form to fill when a project starts. It does not help with software that starts as a toy, and with AI coding tools a lot more software starts as a toy now. So I wanted the repository to tell when it has become important, and GitLab to do the paperwork at that moment.
What it does
Vastuu reads an app's code, config and git history in every pipeline and gives it a tier from 0 (toy) to 3 (business-critical). The tier comes from plain rules with points: business entities like customer and order, potential personal data, a local copy of ERP data with no ERP integration, production deployment, schedules, public exposure, login, and a single active author. Every point links to a file and line.
When an app jumps to Tier 3, the release gate in the pipeline fails until somebody has named an owner and a backup and written a runbook. The failed pipeline starts a GitLab Duo flow, which commits a RESPONSIBILITY.md, a CODEOWNERS proposal and a runbook stub to a branch, opens a merge request for them, opens an issue "Who owns ...?" and comments on the original merge request with the evidence. A person fills in the names and merges, and the gate passes.
There is also a radar on GitLab Pages with all the demo apps, their tiers and how each tier changed per commit, refreshed by a daily scheduled pipeline.
How I built it
The scanner is a Python package with no dependencies outside the standard library. Extractors propose findings, an integrity check drops every finding that does not match the files on disk, and scoring rules from a JSON file turn the rest into a tier. Comparing the tier at the merge base with the tier now gives the transition, so nothing is stored anywhere.
The flow is a custom GitLab Duo Agent Platform flow with five agent steps: scan, plan, record, ownership, summary. It is started by a mention or by a failed pipeline. Every step runs one script in the flow's copy of the repository. The script scans, renders the files from templates, commits and pushes, and returns the exact texts as JSON. The agents read the trigger, call the GitLab tools with values from that JSON, and report what happened.
The pipeline has tests on two Python versions, the scan, GitLab SAST and Secret Detection, a package upload, the gate, a manual production deploy behind the gate, and Pages for the radar. The demo is eight made-up apps, and the main one, order-tracker, has its whole story in six stages from lunch list to production.
I built it with Claude Code, one slice at a time from a written build plan. I did the GitLab web UI parts myself: creating the flow, the triggers, the schedule, and filling in the records.
Challenges
Most of what I learned came from the first flow runs, not from the docs.
- The flow environment has Python 3.9 and no PyYAML. The scanner was planned for 3.11 with YAML config, so it went to 3.9 and JSON on day two.
- In the first test run the model was asked to copy command output into a comment and changed it on the way: a file size and a file listed twice. After that nothing exact is written by the model. A script renders the record files and the texts, and the agent posts them. The ownership issue it opened later was the rendered text byte for byte.
- GitLab ignored the push options that open a merge request when the flow's service account pushed. The same options worked from my own account. The flow opens the merge request with a tool instead.
- When the record merge request was filled in for the first time, the owner went into the visible
Owner:line, not the YAML front matter at the top of the file, which the rendered page hides. The gate kept blocking. Now both places count. - One push makes two pipelines here, a branch pipeline and a merge request pipeline, and both failed pipelines started the flow. The second run found everything already there and created nothing, but now the flow skips the branch pipeline.
- A bot's push cannot start a flow. That was good to see: the record branch's own pipeline fails the gate too, and nothing loops.
What I learned
Keep the decisions in code the model cannot change, and give the model the boring part: reading the trigger and calling the right tools. It was reliable at that. It was not reliable at copying text.
What's next
Read real owners from the GitLab group structure instead of asking, support apps outside one demo folder, and connect it to whatever service catalog an organisation already has.
Log in or sign up for Devpost to join the conversation.