Home / Stories / GitLab, GitHub & Forgejo
Story · Platforms Live

Proposals land where your code lives. Cluster access rides the same forge.

A forge is the service that hosts your git repositories. The assistant runs against GitLab in production every day. It opens draft merge requests (MRs), opens issues, reviews MRs, and reviews the security of the GitLab instance itself. It also reads your clusters through the GitLab agent for Kubernetes. It needs no kubeconfig on its side and no inbound path to your API servers. For GitHub and Forgejo, reads and draft pull requests are built. Both wait for their first live write.

Starts fromThe forge that hosts your repositories, and the GitLab agent that already runs in your clusters.
Looks atRepository contents, CI configuration, project access and branch protection, the runner fleet, and live cluster state through the proxy of the agent.
You getDraft MRs and issues on your forge. A GitLab security review. Cluster reads with no firewall change.
01 · One pipeline, three forges

Every proposal goes through one router.

Agents produce proposals without knowing the forge. A router picks the client for each repository from your configuration. The assistant publishes a proposal only to a repository that the configuration positively names. It refuses anything unmatched at publish time. The GitLab client also refuses repositories that your configuration assigns to another forge.

The same rule holds on every forge. The assistant opens drafts and never merges them.

02 · The usual way in

A kubeconfig, and a network path to the API server.

A tool that reads a cluster normally needs two things. The first is a kubeconfig with long-lived credentials. The second is a route to the API server. That route can be a public endpoint, a VPN, or an allowlisted static egress address on the tool's side.

For a customer cluster behind a VPN, that means a firewall change and a credential to rotate for each cluster.

03 · Through the GitLab agent

The cluster dials out. Nothing dials in.

The GitLab agent (agentk) runs inside the cluster. It keeps an outbound connection to GitLab's agent server (KAS). The assistant talks to the Kubernetes proxy of KAS with a GitLab token scoped to k8s_proxy. It builds its kubeconfig in memory.

There is no inbound port, no VPN and no kubeconfig file. You grant access in the agent's config file in git, to a project or group that the assistant belongs to.

04 · What it may do, in git

The assistant's cluster role is a file you review.

On GitLab CE, every proxied request runs as the agent's service account. So the agent's RBAC (Kubernetes access rules) is the assistant's access. The access is read-only by contract, apart from a few narrow, separately bound extras, such as a request for a Flux reconcile.

That RBAC file lives in your deployment repository. The assistant checks it for drift. If the assistant itself wants to change the file, it escalates the change to human review before it opens an MR.

05 · Keep the path up

Two agent replicas, two zones, one disruption budget.

Both agent replicas can land on one node. Then a single spot reclaim or drain takes the whole KAS context down. Your CI deploys and the assistant's reads stop together. The baseline pins zone and host spread and a PodDisruptionBudget. The chart has no value for these settings.

06 · Review the forge itself

Branch protection, CI secrets, the runner fleet.

Three rule-based reviewers read GitLab through a GET-only client. They check project access and branch protection. They check the merged CI configuration of every project. They check the runners that each project can use. No AI model (LLM) is involved.

Sometimes the assistant's own role cannot see an endpoint. Then it opens one "could not review" advisory. It never reports a blind review as a clean one.

07 · GitHub In development

Draft pull requests, same rules.

The GitHub client reads repositories, pull requests and history. It writes draft pull requests. Writing needs three things: an explicit write token, a writable repository row and a positive config match. Without all three, the client makes no HTTP call at all.

The client is built and tested. It waits for its first live write. Known gaps: it cannot mark a PR ready, and it cannot assign reviewers.

08 · Forgejo In development

Read path proven. Draft writer built.

The assistant reads a Forgejo repository with the same reader as GitHub. A thin dialect layer handles Forgejo's API. At the same commit, the reader returns byte-identical files to a local clone. If a tree is too large to page through, the reader reports it as incomplete. It never reports it as complete.

The writer for draft pull requests is built, but it has not made a live write yet. Forgejo marks drafts with a WIP: title prefix. So the writer reads the PR back and closes it if it did not come out as a draft.

09 · The proposal

A one-line CI fix, found by the forge review.

Most forge findings are issues. A committed secret needs rotating, not a diff. One finding is mechanical: CI_DEBUG_TRACE left on. It prints every masked variable into the job log. When the setting appears verbatim in the project's own .gitlab-ci.yml, you get a draft MR. The full MR is below.

Agentsproposals
Forge routerpositive config match
GitLabdraft MRs · issues
Live
GitHubdraft PRs
In development
Forgejoread path · draft PRs built
In development
no positive config match → not published · foreign-forge repos refused by the GitLab client
Assistantkubeconfig file · long-lived credential
VPN / firewallallowlisted egress address
API servermust be reachable
inbound path per clustercredential per clusterrotation per cluster
a firewall change and a secret to manage, for every cluster you add
Assistantin-memory kubeconfig
GitLab KAS/k8s-proxy · token scope k8s_proxy
your cluster · no inbound port
agentkdials out to KAS
API serverprivate
outbound from the cluster onlyaccess declared in gitrevoke = one token
infrastructure/rbac/gitlab-agent-rbac.yaml
kind: ClusterRole
metadata:
  name: gitops-assistant-read
  labels: { …/operator-pack: agentk-rbac }
rules:
  - apiGroups: ["", apps, helm.toolkit.fluxcd.io, …]
    verbs: [get, list, watch]
---  separately bound, separately revocable
kind: ClusterRole
metadata: { name: gitops-assistant-flux-poke }
# patch Flux objects to request a reconcile
  • drift from the canonical rules convergence MR
  • bot-proposed change to this file held for human review
zone a
node-1
ag
·
·
zone b
node-4
ag
·
gitlab-agent baseline · beyond the chart values
topologySpreadConstraints: # zone + hostname, via post-renderer
PodDisruptionBudget: minAvailable: 1
one spot reclaim no longer takes CI deploys and the assistant down together
GitLab review · GET-only · deterministic
  • shop/orders-api · main allows force-push access · issue
  • platform/tooling · private key committed in CI config CI/CD · issue, redacted
  • payments/ledger · CI_DEBUG_TRACE: "true" CI/CD · draft MR
  • runners for 12 projects not visible to the bot's role · 1 advisory
never proposes touching its own access, token or runner · never removes the last owner
write authority · GitHub
  • repository row is kind github and writable
  • write token resolved
  • positive config match for this repo
  • any one missing 0 HTTP calls · proposal stays unpublished
reads: incomplete lists are marked incomplete, and "unknown" is never read as "absent"
reader compare · Forgejo API vs local clone · same commit
✓ path set          identical
✓ file bytes        identical
✓ parsed manifests  identical
✓ resolved commit   identical
  exit 0 · IDENTICAL
create PRtitle "WIP: …"
read backdraft is true?
else closeproposal fails
Draft · Disable CI_DEBUG_TRACE in payments/ledger
variables:
-  CI_DEBUG_TRACE: "true"
+  CI_DEBUG_TRACE: "false"
category:securitysource:gitlab_cicdrisk:lowxrev::LGTM
↓ the full merge request is below

The merge request you get.

The project names are illustrative. The finding, the verbatim-source check and the single-line flip are what the CI/CD reviewer does today.

Draft

Disable CI_DEBUG_TRACE in payments/ledger

from gitops-ai/cicd-debug-trace-ledger into main · 1 file · +1 −1
category:securitysource:gitlab_cicdrisk:lowxrev::LGTM
OverviewWhere it comes fromAfter mergeChanges
Summary

This project's .gitlab-ci.yml sets CI_DEBUG_TRACE to true. With debug tracing on, the runner prints every command and every CI/CD variable, including masked ones, into the job log. Anyone who can read the job log can read the secrets.

This MR sets it to false. Nothing else changes.

Also do
  • Rotate any secret that jobs could read while debug tracing was on.
  • Consider deleting job logs from that period.
Why this is an MR and not an issue

The reviewer reads the merged CI configuration. That configuration also contains what comes from includes, parent groups and UI variables. The reviewer proposes a diff only after it re-reads the project's own .gitlab-ci.yml and finds the setting there verbatim. If the setting came from an include or a UI variable, you would get an issue that points at the source instead.

CheckResult
Merged CI config has CI_DEBUG_TRACE truthyyes
Present verbatim in .gitlab-ci.ymlyes, top-level variables:
Key is unique in the fileyes
  • The next pipeline's job logs no longer contain the shell trace of every command and variable.
  • The CI/CD review stops reporting this project on its next run.
  • Someone has rotated the secrets that this project could read (see Overview).
   variables:
-    CI_DEBUG_TRACE: "true"
+    CI_DEBUG_TRACE: "false"
     DOCKER_DRIVER: overlay2
R
independent reviewer second opinion, no pipeline context

The flip is minimal and matches the title. The rotation advice is appropriate. LGTM.

What keeps it honest.

A tool with access to your forge and your clusters must be boring about both.

No inbound pathCluster reads go through the outbound tunnel of the GitLab agent. You declare access in the agent config in git. You revoke it when you remove that declaration or the token.
Your RBAC, in your repoThe assistant's cluster permissions are a file that you review. The assistant checks that file for drift. It cannot widen the permissions quietly, because a human reviews its own changes to the file.
Never about itselfThe GitLab reviewers never propose a change to the assistant's own access, token or runner. They never remove the last owner. They never tighten a protection past the assistant's own push path.
Blind is not cleanIf the assistant's role cannot read an endpoint, it skips the affected checks. One advisory says so. A partial read never produces a "healthy" result.
Positive match to publishThe assistant publishes a proposal only to a repository that your configuration names. It refuses unmatched repositories at publish time. The GitLab client refuses repositories that your configuration assigns to another forge. The assistant also treats an unknown forge kind as foreign and does not publish to it.
Drafts only, everywhereThe assistant opens drafts and never merges them. On some forges, a draft is only a title convention. There the assistant reads the draft state back. It closes a PR that did not come out as a draft.

Where this stands.

Live
  • GitLab draft MRs, issues and MR review
  • Cluster access through the GitLab agent (KAS) on production clusters
  • Agent RBAC drift checks
  • GitLab access, CI/CD and runner review, and a CI_DEBUG_TRACE fix as a draft MR
In development
  • GitHub reads and draft pull requests (built; first live write pending)
  • Forgejo reads (identical to a local clone) and draft pull requests (built; first live write pending)
Roadmap
  • Forgejo: first proposal type, Ansible role version bumps as draft pull requests
  • GitHub: mark ready for review, reviewer assignment
← Previous storyCross-domain context