-
-
Preflight on every merge request: review risks and Play Console tasks, each cited to the exact file and line that caused it.
-
The GitLab pipeline — build, preflight, release — all green on GitLab's SaaS runners.
-
The release job uploads versionCode 2 to Google Play's internal track via the Play Developer API — a real, committed upload.
-
A Play rejection pasted into an issue — replayed from Google's published policy and labelled — triggers the Duo fix flow.
-
The GitLab Duo rejection-to-fix flow registered on the project: it reads the rejection and opens a fix merge request.
Inspiration
The code is the easy part. For a solo Android developer the hard part is everything after the merge: the Data Safety form, the restricted-permission declarations, the Accessibility API video, and the Play review rejection that arrives by email a week later with a policy name and no diff. That is the real "life after code" — and it's where indie Android apps quietly die.
Our anchor is a real app: Unrot, which counts doomscrolled reels with an AccessibilityService, draws a system overlay, reads usage stats, and ships Firebase Analytics. Every one of those is a Play review trap. We built Greenlight to carry a real app through them.
What it does
- Preflight on every merge request. A deterministic, zero-dependency CLI reads the merged manifest of the signed build, the manifest-merger report, the Gradle dependency tree and the Kotlin source, and posts one MR note with: upload blockers (target API level), review risks (no prominent disclosure before the Accessibility permission — the most common Accessibility rejection), Play Console tasks (Accessibility API declaration, Advertising ID), and a Data Safety draft where every row is cited to the line — or the library — that caused it (e.g.
AD_IDwas added byplay-services-measurement-api, not by your manifest). It also drafts the declaration text and a video checklist. - Upload on merge. Signs the AAB, asks the Play Developer API for the highest existing versionCode and bumps it, then pushes to the internal testing track (
edits.insert → bundles.upload → tracks.update → edits.commit). Auth is a service-account key or Workload Identity Federation. - Rejection to fix MR. Paste Google's rejection email into a
play-rejectionissue. A GitLab Duo Agent Platform custom flow (rejection reader → fixer → release clerk) maps it to one policy note, finds the exact code, applies the policy's fix recipe, reruns the preflight until the finding passes, and opens an MR that also carries the Play Console text the human still owes.
Principle: rules decide, AI explains and writes the fix. The preflight is deterministic; the Duo agents only act on what it found — they never invent a policy or guess a verdict.
How we built it
- Preflight CLI — Node 20+, zero dependencies: a manifest-merger reader, a known-SDK permission attributor, and a rule table that emits findings as JSON and a Markdown MR note.
- Play upload —
google-auth-library; handles the "draft app" and "changes not sent for review" edge cases of a brand-new app. - GitLab CI —
build → preflight → releaseon GitLab SaaS runners,cirruslabs/android-sdk:36image; release gated to the default branch. - GitLab Duo Agent Platform — a three-agent custom flow triggered from the rejection issue.
- Policy knowledge — short paraphrased notes per policy (no copied Google text), each with a fix recipe the fixer follows as a hard constraint.
Challenges we ran into
- Play's API can't create an app, so the first AAB is a manual upload — the pipeline is designed around that one human step.
- Duo flow schemas are strict (no
max_cycles, noresponse_schema_id, no emoji/HTML in prompts); each agent's contract is a single word or one-line JSON. - The fixer runs with no Android SDK, so it reasons about imports from the Gradle dependency list rather than compiling — the CI pipeline is what actually compiles the fix.
Accomplishments that we're proud of
- The whole pipeline runs live: a merge request builds a signed AAB on GitLab's runners, the preflight posts real, line-cited findings on the MR, and the release job performs a real upload of versionCode 2 to the Play internal testing track.
- Every Data Safety row and declaration says why it's there, traced to a file and line.
- 8/8 unit tests, including "a shown disclosure string resolves the review risk."
What we learned
Most of the Play Store release burden is deterministic — it can be read straight off the build. The LLM's job is the narrow, human part: writing the disclosure, mapping an email to a policy, and editing the smallest patch. Keeping rules and AI on opposite sides of that line is what makes the output trustworthy.
What's next for Greenlight
A Duo flow that drafts the Data Safety submission, more policy recipes (foreground-service types, photo/video permissions, health data), and a GitLab component other Android projects include in one line.
Honesty note: the rejection in the demo is replayed — written from Google's published Accessibility API policy and labelled on screen. It is not an email Google sent. policy/ paraphrases Google's policy pages and links each source.
Log in or sign up for Devpost to join the conversation.