Inspiration

I build one ASP.NET API that serves a Vue web app and a Flutter mobile app, and until now I deployed all of it by hand. The incidents that hurt most were never clever bugs, they were environment mistakes:

  • A production API pointed at the development database. Users kept signing up, their data went to the wrong place, and moving it back was painful.
  • A hotfix merged back and forth between dev and prod branches, and config values silently overwrote each other.
  • A Flutter build shipped to the store while Firebase Remote Config still advertised the old version.
  • A change for a new feature broke an old one, because only the new feature was tested.

Every team "knows" the right practice (keep config out of code, use per-environment secrets). In practice people forget, new teammates don't know, and at 2 a.m. someone takes a shortcut. I wanted something that enforces the rule every time and helps when it fails.

What it does

EnvGuard makes one promise: no build or deploy may ever talk to another environment's API or database. It does that in layers:

  1. Policy as code: envguard/policy.yml says which values belong to which environment (DB names, API hosts) and which keys must never be committed (e.g. ConnectionStrings in Staging/Production).
  2. Deterministic guard in the pipeline: envguard.py check runs first on every push and MR. It fails on cross-environment leaks, committed connection strings or secrets, and merge-conflict leftovers, and writes a Code Quality report. release-check blocks a mobile version bump that forgot Remote Config.
  3. Runtime guard: every database carries an environment marker. The API refuses to start if its marker doesn't match, so even a leaked connection string can't write production users' data into the dev database.
  4. Post-deploy verification and rollback: after deploying to Cloud Run, verify-deploy asks the API which environment and database it is really using, and rolls back automatically if they don't match. Production needs a human approval (Supervised level).
  5. Three GitLab Duo flows handle the parts that need judgment:
    • Guardian (assign as MR reviewer): explains every finding in terms of what would happen to real users, finds affected existing features and callers in web/mobile, opens a fix MR with tests, and posts one summary with a "Human decision needed" list.
    • Pipeline Doctor (pipeline trigger): looks at the failed jobs (their logs when the API returns them, otherwise the failing commit), names the root cause, and opens a fix MR when the cause is in the repository. It leaves config findings to Guardian, so there are no duplicate fixes.
    • Release Captain (mention on an issue): bumps the app version and updates Firebase Remote Config as code in the same change, runs the checks and tests, and opens the release MR with a checklist for the human. (Publishing to Firebase is a manual CI job that isn't connected in this demo.)

Demo scenario

A merge request titled "hotfix: fix production login timeout" quietly adds the dev connection string to appsettings.Production.json and points the production web build at localhost. The guard fails the pipeline in seconds. Guardian reports that every production user's browser would call localhost, so login and registrations would fail, notices the MR doesn't change any timeout at all, and opens a fix MR. The fix agent runs envguard check and the tests itself, and the fix MR's pipeline is green. A human only has to make the final call.

How I built it

  • App: ASP.NET 9 minimal API + EF Core (PostgreSQL on Neon), Vue 3 + Vite, Flutter. One repo, so the agents can see all three callers of the API.
  • Checker: a single Python file using only the standard library (PyYAML is optional), with unit tests. The Duo agent sandbox can't pip install, so the fallback parser lets agents verify their own fixes.
  • Pipeline (.gitlab-ci.yml): guard → build → test (SAST, Secret Detection) → package → deploy staging → verify → manual production approval → verify → release, with automatic rollback jobs. Personal branches without an MR run only the guard, so pushing to save work costs seconds, not minutes.
  • Deploy: Google Cloud Run (staging + production), images built with Cloud Build into Artifact Registry, connection strings only in Secret Manager.
  • Agents: GitLab Duo Agent Platform custom flows (.gitlab/duo/flows/). Guardian separates analysis from action: the analyzer only reads, the fixer acts only on the analysis, and the reporter only posts a note.
  • AGENTS.md gives every agent the same hard rules (never write a secret, update every caller in the same MR, add a test for the existing feature you touched).

Challenges I ran into

  • The flow ran a different prompt than the one I wrote. My first agent runs produced "documentation gap" reports. The Flow Builder editor had kept GitLab's sample template below my pasted YAML. YAML lets the last duplicate key win, so the sample's prompts silently replaced mine, and it still passed validation. Lesson: always check the end of a pasted config.
  • Agents couldn't run my checker. The agent sandbox has no PyYAML, so the first fix MR said "could not verify locally". I made the checker dependency-free; on the next run the agent reported envguard check: PASSED before opening its MR.
  • main and dev had unrelated histories (the workspace was created with its own README), which turned a 2-file demo MR into 178 conflicting files and confused the agents. Merging with --allow-unrelated-histories fixed it without force-pushing.
  • Two agents fixing the same problem. Pipeline Doctor and Guardian both opened fix MRs for the same leak, so I split their jobs: Guardian owns config findings, and Pipeline Doctor owns build/test/deploy failures.
  • The pipeline caught an agent's release. Release Captain correctly bumped the app to 1.1.0 and updated Remote Config, but envguard:selftest failed: my tests assumed the repo was always on 1.0.0. The guardrail worked on AI-made changes too. I pinned versions inside the tests and made Release Captain run the unit tests before opening its MR.
  • First deploy to Cloud Run took several fixes: gcloud builds submit exits 1 when the CI account can't stream logs (now submitted async and polled, keeping least privilege); scripts committed from Windows lacked the executable bit (including the rollback script, so the safety net itself would have failed); a quoted connection string; and CORS, because Cloud Run serves each service on two URLs. Pipeline Doctor diagnosed the executable-bit failure on its own and proposed a fix (MR !18). deploy.sh now prints the container's own logs when a revision fails to start.
  • Windows + Thai locale: the report's emoji crashed the console (cp874). Small, but it's the kind of thing that breaks a teammate's first run.

Accomplishments that I'm proud of

  • A realistic "looks harmless" hotfix is stopped by the pipeline, explained in plain language, and fixed by an agent with a green pipeline. It runs end to end in a few minutes and needs one human decision.
  • Layered defense: even if every check before deploy were bypassed, the API itself refuses to start against the wrong database. In production, the web app can't even reach the staging API (CORS).
  • The deterministic guard does the blocking and the agents do the explaining and fixing, so the safety net still works when AI credits run out.

What I learned

  • CI/CD from scratch: workflow rules, stages, manual gates, and making the cheap check run first.
  • Agent design: least-privilege toolsets, splitting read/act/report steps, and treating MR content as untrusted input.
  • Most of my production pain was environment drift, not code, and that's very automatable.

What's next for EnvGuard

  • Environment-branch mode (DEPLOY_MODE=env-branches) for teams that deploy dev → staging and prod → production, with an automatic back-merge MR after hotfixes.
  • Database migration check before deploy.
  • New module from a template: scaffold API + web + correct config for every environment from an issue.
  • Duplicate-code cleanup across modules on a schedule.

Built With

Share this project:

Updates

Submission history