Add the CI components

Takes about 20 minutes

Four GitLab CI/CD components connect your app's merge requests to Bugoro. Include all four in your app's .gitlab-ci.yml.

Before you start

  • GitLab 18.10 or later: the components use spec:component and spec:description.
  • A runner tagged bugoro that can reach the farm (the Self-host page sets one up).
  • Your project connected in the Hub. Otherwise the build registration fails with 401 invalid_ci_token.
  • The CI/CD variables below. deploy.sh ci-vars sets all of them but the optional keystore.

No runner of your own? The jobs also run on GitLab's hosted runners. Give them the hosted runner's tag, saas-linux-small-amd64, as runner_tag, the Hub's public URL as hub_api_url, and device-shots the Hub's farm relay, https://<hub-host>/farm, as farm_url. The relay checks the job's CI ID token and passes FARM_TOKEN on to the farm. For Bugoro's own pipeline, deploy.sh ci-vars --hosted-runners sets these.

Include the components

<group>/<project> is where the Bugoro components are published on your GitLab, and <version> a released version such as 1.0.0.

include:
  - component: $CI_SERVER_FQDN/<group>/<project>/qa-build@<version>
    inputs:
      module: app                       # the app's Gradle module
      hub_url: $BUGORO_HUB_URL
      hub_api_url: $BUGORO_HUB_API_URL
  - component: $CI_SERVER_FQDN/<group>/<project>/device-shots@<version>
    inputs:
      farm_url: http://172.31.250.10:8080
      hub_url: $BUGORO_HUB_URL
      hub_api_url: $BUGORO_HUB_API_URL
      package: com.example.app.qa       # the QA build's application id
      default_deep_link: myapp://app/home
  - component: $CI_SERVER_FQDN/<group>/<project>/store-check@<version>
    inputs:
      module: app                       # the app's Gradle module
  - component: $CI_SERVER_FQDN/<group>/<project>/size-guard@<version>
    inputs:
      module: app
      hub_url: $BUGORO_HUB_URL
      hub_api_url: $BUGORO_HUB_API_URL
  • Two Hub URLs. hub_url is the Hub's public URL, for phones and the links in the notes. hub_api_url is where the jobs call the Hub's API. On the Bugoro host, CI egress drops the host's public address, so jobs call the Hub over the Bugoro network instead: BUGORO_HUB_API_URL is http://bugoro-hub:3000 there. If your jobs can reach the public URL, leave hub_api_url out.
  • The API URL may use http only for bugoro-hub, localhost or a private address. Anything else must be https, so the CI ID token never crosses the internet in cleartext.
  • If the Bugoro project isn't bugoro/bugoro, set the bugoro_project input on each component: the jobs fetch their scripts from it, so it must be public.
  • store-check stands alone. size-guard measures store-check's bundle when both run, so its stage must not be earlier.

Run merge request pipelines

One pipeline per push: an MR pipeline while the branch has an open MR, otherwise a branch pipeline. The fix agent's branches (bugoro/*) get no branch pipeline; their MR pipeline follows.

workflow:
  rules:
    - if: $CI_PIPELINE_SOURCE == "merge_request_event"
    - if: $CI_COMMIT_BRANCH =~ /^bugoro\//
      when: never
    - if: $CI_COMMIT_BRANCH && $CI_OPEN_MERGE_REQUESTS
      when: never
    - if: $CI_COMMIT_BRANCH
    - if: $CI_COMMIT_TAG

What each job does

qa-build

Builds the QA APK, uploads it to the project's package registry, registers it with the Hub, and points the qa/<branch> environment at the install page, so the MR gets a View app button.

InputDefaultWhat it does
moduleappThe app's Gradle module.
variantqaThe build variant to assemble.
hub_urlrequiredThe Hub's public URL. Also passed to Gradle as -Pbugoro.hubUrl.
hub_api_urlemptyWhere the job calls the Hub's API, when it can't reach hub_url. Empty means hub_url.
qa_keystore_variableBUGORO_QA_KEYSTOREAn optional File variable with the QA keystore.

device-shots

Opens the reported screen on the farm's emulators for the default branch's build (Before) and the MR's build (After), and posts a Before/After table on the MR. When the two shots match, the note says there is no visible difference: the fix may not be visual, or may show only after interaction. It opens the screen by its deep link and signs nothing in, so an app whose screens need a signed in user should sign in a test account in its QA build. It never fails the pipeline.

InputDefaultWhat it does
farm_urlrequiredThe device farm's base URL; the runner's jobs must reach it.
hub_urlrequiredThe same as qa-build's.
hub_api_urlemptyWhere the job calls the Hub's API, when it can't reach hub_url. Empty means hub_url.
packageemptyThe QA build's application id.
default_deep_linkbugoro-sample://app/homeThe screen to open when the MR has no linked report. Set your own.
profilessmall,largeThe farm's device profiles.

store-check

Checks the release bundle against Google Play's rules (Android lint, Greenlight and Bugoro's own rules), writes a Code Quality report and one sticky MR note, and fails only a release pipeline (a tag, or an MR into release/* or labelled release), and only for a critical finding.

InputDefaultWhat it does
moduleappThe app's Gradle module.
variantreleaseThe release variant.
metadata_dirfastlane/metadata/androidThe store listing, in the fastlane layout.
declarationsstore/data-safety.jsonThe data safety declarations.
fail_oncriticalThe lowest severity that fails a gating pipeline.
release_branches^release/MRs into matching branches gate.
readiness_accountemptyThe release-readiness flow's account, for the note's mention line.

size-guard

Measures what a phone downloads with bundletool, records it in the Hub (the dashboard's trend), and keeps one sticky MR note with the change against the MR's target branch (or the default branch when the target has no recorded size) and the largest files. It never fails the pipeline.

InputDefaultWhat it does
moduleappThe app's Gradle module.
hub_urlrequiredWhere the size is recorded, and the note's dashboard link.
hub_api_urlemptyWhere the job calls the Hub's API, when it can't reach hub_url. Empty means hub_url.
device_specphone-arm64The device the download size is for.
budget_kb"0"An absolute budget in KiB; over it, the note warns.
warn_increase_kb"100"The note warns when the download grows by more.

Every input is listed in the reference doc.

CI/CD variables and their scopes

Under Settings > CI/CD > Variables, or with deploy.sh ci-vars from the host. None is protected: fix branches aren't protected, so protected variables would never reach their pipelines.

VariableEnvironment scopeUsed by and value
BUGORO_GITLAB_TOKENbugoro-*device-shots, store-check and size-guard post and edit their MR notes with it. A project access token, Reporter, api scope. Masked.
FARM_TOKENbugoro-shots/*device-shots, to use the farm. The farm's own token. Masked.
BUGORO_HUB_URLallThe Hub's public URL, https://<hub-host>, for hub_url. Not a secret.
BUGORO_HUB_API_URLallWhere the jobs call the Hub's API, for hub_api_url: http://bugoro-hub:3000 on the Bugoro host. Not a secret.
BUGORO_QA_KEYSTOREqa/*Optional. qa-build's QA keystore, type File, base64. Masked.

Why the scopes. Each job declares an environment only so its variables can be scoped. device-shots gets both tokens; store-check and size-guard run the MR's Gradle build, so they get BUGORO_GITLAB_TOKEN only, never the farm's. Unit test jobs, which run code the agent wrote, get neither. qa-build needs neither: it uses a CI ID token, so the Hub needs no CI secret.

Sign every QA build with the same key

Android installs an update only when it is signed by the same key. If your qa build type signs with the debug config, give qa-build a stable debug keystore:

keytool -genkeypair -keystore qa-debug.keystore -storetype PKCS12 \
  -alias androiddebugkey -storepass android -keypass android \
  -keyalg RSA -keysize 2048 -validity 10000 -dname "CN=<App> QA"
base64 -i qa-debug.keystore        # on Linux: base64 -w0 qa-debug.keystore

Add the base64 text as BUGORO_QA_KEYSTORE: type File, masked, scope qa/*, not protected. Keep the keystore file out of the repository.

GitLab's security scans

Bugoro's own pipeline also runs GitLab's Secret Detection and SAST (Semgrep) on every merge request and the default branch, the fix agent's MRs included. You can add them to your app's pipeline the same way:

include:
  - template: Jobs/Secret-Detection.gitlab-ci.yml
  - template: Jobs/SAST.gitlab-ci.yml
  • With the workflow rules above, an open MR has no branch pipeline, so set the variable AST_ENABLE_MR_PIPELINES: "true", or the scans never run in MR pipelines.
  • Both jobs may fail, so a scanner outage never fails the pipeline and never starts an agent repair. GitLab reports what they find.
  • The Bugoro runner runs only images pinned by digest. Override each scanner job's image: with a pinned one and add it to the runner's allowed_images, as Bugoro's pipeline does.
  • Each scan can hold back "Fix ready" until it ends, because the return trip waits for the MR pipeline to pass. Bugoro's pipeline gives each scanner a 10 minute timeout.

Bugoro's pipeline also publishes the sample app's privacy policy with GitLab Pages, from pages/ on the default branch. Its store listing links the Hub's copy, /chai/privacy/, which anyone can read, so store-check's privacy policy check has a public page to fetch even when Pages is visible to project members only.