> For the complete documentation index, see [llms.txt](https://docs-lunar.earthly.dev/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://docs-lunar.earthly.dev/install/git-platforms/gitlab.md).

# GitLab

The Hub authenticates as a **service account**: a machine user that you create, grant group memberships to, and issue a token for.

This page covers the GitLab side only. The install steps that consume it are in [Prerequisites → Step 5](/install/lunar-hub/self-hosted/prerequisites.md#step-5-connect-your-git-platform) for self-hosted, or the [Dedicated](/install/lunar-hub/dedicated/setup.md) setup steps.

## Authentication

How many accounts and tokens you need depends on where your GitLab runs:

| Your GitLab                      | Service account                                                                            | Tokens                       |
| -------------------------------- | ------------------------------------------------------------------------------------------ | ---------------------------- |
| GitLab Dedicated or self-managed | A single **instance service account**, made a member of every top-level group Lunar serves | **One token** for everything |
| GitLab.com                       | One **group service account** per top-level group                                          | One token per group          |

Prefer the single instance service account wherever you administer the instance. GitLab.com has no such thing, so each top-level group there brings its own account and token.

You do not need to tell Lunar which groups it serves. It reads that from the service account's memberships: every group where the account holds the Maintainer role, each covering its whole subtree. Lunar follows those memberships as they change, so you bring a group into scope by inviting the service account to it. If several tokens' accounts cover the same group, Lunar load-balances its traffic across them.

### Create the service account

On **GitLab Dedicated or self-managed**, an instance administrator does this once:

1. Go to **Admin → Settings → Service accounts** and select **Add service account**.
2. Name it something recognizable, e.g. `Earthly Lunar`. The account appears as the author of Lunar's merge-request comments and commit statuses.
3. Add the account to **every top-level group** Lunar should serve, with the **Maintainer** role (each group's **Manage → Members → Invite members**). A membership covers all of the group's subgroups and projects, and it is how Lunar decides what to serve. Inviting the account to a new group later onboards that group without any configuration change.

On **GitLab.com**, a group Owner does it once per top-level group:

1. In the group, go to **Settings → Service accounts** and select **Add service account**.
2. Add the account to the group with the **Maintainer** role.

### Create the token

On the service accounts page, select the vertical ellipsis (**⋮**) next to the account, then **Manage access tokens → Add new token**. Give it the `api` scope, and pick an expiry (see [Expiry and rotation](#expiry-and-rotation) below).

What each token can do decides which features are available for the groups it serves:

| Permission | Value        | What it enables                                                                                                                                                                                          |
| ---------- | ------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| scope      | `api`        | Everything Lunar does over the API: reading repositories, commits, and merge requests; posting commit statuses and MR comments                                                                           |
| role       | `Maintainer` | Registering project webhooks, maintaining [project badges](#project-badges), and on Ultimate provisioning the [merge gate](#merge-gate): the status check and the **Status checks must succeed** setting |

Copy the token from GitLab. You leave this section with either a **single token** (instance service account) or **one token per top-level group** (GitLab.com).

### Expiry and rotation

GitLab access tokens carry a fixed expiry (by default at most a year out), and the Hub reads each token once at startup.

Set a calendar reminder ahead of the expiry date. When a token lapses, Lunar stops posting results and stops reacting to webhooks for the groups it served, and because the Hub only reads tokens at startup, nothing fails loudly to tell you.

A service account can hold several tokens at once, so replacement is a rollover rather than a cutover: create the new token next to the old one on the **Manage access tokens** page, deliver it, and revoke the old one once the Hub is running with the new. The delivery mechanics depend on your install: [self-hosted](/install/lunar-hub/self-hosted/day-2-operations.md#git-platform-credentials) is a secret update plus a Hub restart; on [Dedicated](/install/lunar-hub/dedicated/setup.md#step-4-deposit-your-secrets) you re-deposit the token into your install's secret drop.

{% hint style="warning" %}
GitLab 19.2 added an instance setting that **enforces fine-grained tokens after a chosen date**, which blocks the creation and rotation of classic-scope tokens. Service accounts cannot use fine-grained tokens yet, so if your instance plans to turn that enforcement on, keep its date clear of Lunar's rotation schedule until GitLab extends them to service accounts.
{% endhint %}

## Hub configuration (Dedicated)

There is nothing to configure: Earthly runs the Hub and configures it for you. Continue at [Dedicated → Deposit your credentials](/install/lunar-hub/dedicated/setup.md#step-4-deposit-your-secrets), where the token goes to your install rather than to Earthly. What you deposit is what the steps above produced:

* **GitLab Dedicated or self-managed**: the instance service account's single token, alongside your instance hostname.
* **GitLab.com**: one token per **group service account**.

Everything below is for self-hosted installs; you can skip to [Merge gate](#merge-gate).

## Hub configuration (self-hosted)

Lunar reads GitLab credentials from `HUB_GITLAB_TOKENS`, a JSON array with one entry per token, typically a single entry holding the instance service account's token. [Install Step 4](/install/lunar-hub/self-hosted/install-walkthrough.md#git-platform-credentials) has the values block to paste; this is the field reference behind it.

### Entry fields

| Field            | Required | Meaning                                                                                                                                                                                                       |
| ---------------- | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `token_path`     | Yes      | Where the token file is mounted inside the Hub container.                                                                                                                                                     |
| `webhook_secret` | No       | Overrides the chart-generated signing secret (`<release>-gitlab-webhook`) for the groups this entry serves. Set it when one group should not be able to forge deliveries for another; leave it out otherwise. |
| `host`           | No       | Defaults to `gitlab.com`. Set it to your instance hostname for self-managed.                                                                                                                                  |
| `base_url`       | No       | Defaults to `https://<host>/api/v4`. Set it only if your instance serves the API somewhere else.                                                                                                              |

{% hint style="warning" %}
**A self-managed host must be listed here.** Lunar decides whether a host is GitLab from this configuration. If you omit `host` for a self-managed instance, Lunar treats that host as GitHub, and every operation against it fails in confusing ways. `gitlab.com` is recognized without configuration.
{% endhint %}

### Multiple groups

With an instance service account, one entry serves every group the account is a Maintainer of. To serve a new group, invite the account to it; the configuration does not change. Each further token gets its own entry, whether that is a group service account on GitLab.com or an extra account added to spread a busy group's load:

```yaml
hub:
  extraEnv:
    - name: HUB_GITLAB_TOKENS
      value: >-
        [{"host":"gitlab.example.com","token_path":"/secrets/gitlab/lunar-sa.token"},
         {"host":"gitlab.example.com","token_path":"/secrets/gitlab/checkout-sa.token"}]
```

Here the first account is a Maintainer everywhere, and the second belongs to one busy subgroup only. Wherever several tokens cover the same group, Lunar load-balances between them, so the busy subgroup draws on both accounts' rate limits while everything else stays on the shared one. Give overlapping accounts the same role, so that what Lunar can do in a group does not depend on which token serves a call.

{% hint style="info" %}
**Scaling tip.** GitLab applies API rate limits per account. If one group or subgroup pushes constantly, invite an extra service account there and add its token as an entry: every account covering a group adds its own rate limit to that group's pool. (On a self-managed instance you can also raise the limits; on gitlab.com you cannot.)
{% endhint %}

## Merge gate

Lunar can block a merge request on every tier. What differs is the mechanism, how precisely it targets Lunar, and who switches it on.

| Tier          | Mechanism                                                 | Blocking                                                                                                                            |
| ------------- | --------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------- |
| Ultimate      | [External status check](#external-status-checks-ultimate) | Lunar provisions and enables it for you                                                                                             |
| Free, Premium | [Commit status](#commit-status-default)                   | Available, but you enable **Pipelines must succeed** yourself, and it gates on the whole pipeline rather than on Lunar specifically |

Only policies at a gating enforcement level participate. See [policies](/configuration/lunar-config/policies.md).

### External status checks (Ultimate)

On **GitLab Ultimate**, Lunar blocks a merge request whose gating policies do not pass.

{% hint style="warning" %}
**There is no setting to turn this on, and Lunar changes your projects to enable it.** Read what it changes below before rolling Lunar out to an Ultimate namespace.
{% endhint %}

Merge blocking is capability-driven: Lunar detects that a namespace is on Ultimate and switches on automatically. There is no configuration flag, so if you upgrade a namespace's licence later, blocking activates on the next sync without anyone touching Lunar's configuration.

To make blocking work, Lunar changes each project it tracks:

| Change                                       | Where you see it in GitLab                    |
| -------------------------------------------- | --------------------------------------------- |
| Creates a status check named `Earthly Lunar` | **Settings → Merge requests → Status checks** |
| Enables **Status checks must succeed**       | **Settings → Merge requests**                 |
| Subscribes its webhook to comment events     | **Settings → Webhooks**                       |

**Lunar re-applies these settings on every sync.** Turning them off in the GitLab UI is temporary; they come back. This is deliberate: it keeps the gate from being quietly disabled on one project and drifting out of policy. But it does mean that switching the gate off is not a supported way to stop it; see [Turning the gate off](#turning-the-gate-off).

This requires the service account to hold the `Maintainer` or `Owner` role, since Lunar creates the status check and changes merge-request settings.

#### Overriding a block

When a merge request is blocked and it needs to merge anyway, comment on it:

```
/lunar bypass: rolling back the checkout regression, incident 4417
```

A reason is required: `/lunar bypass:` on its own is refused. Lunar replies directly to the command comment, confirming who bypassed, which commit it applies to, and why. If it cannot honour the request it replies saying so, rather than staying silent.

To customize the successful reply, set [`customization.bypass_template`](/configuration/lunar-config/customization.md#bypass_template) to a template file in the configuration repository. For example:

```yaml
customization:
  bypass_template: templates/bypass-acknowledgement.tmpl.md
```

**A bypass covers one commit.** Pushing again to the merge request re-blocks it, and needs a fresh bypass. This is the property that makes the override safe to grant: it cannot be used to wave through code that has not been looked at.

| Rule        | Detail                                                                        |
| ----------- | ----------------------------------------------------------------------------- |
| Who         | Maintainer or above on the project                                            |
| Reason      | Required, and recorded                                                        |
| Scope       | The merge request's current commit only                                       |
| Self-bypass | The merge request's own author may bypass; it is allowed and recorded as such |

Every bypass is recorded (who, their **verified** role, the reason, the commit, and whether it was a self-bypass) on the same audit ledger the [CLI bypass commands](/docs/lunar-cli.md#bypassing-a-block) write. That means a comment-driven override shows up in `lunar policy bypass-ls`, can be ended early with `lunar policy bypass-rm`, records [what it actually masked](/sql-api/views/bypassed-checks.md), and is queryable over the [SQL API](/sql-api/views/bypasses.md) alongside every other override. Unlike CLI bypasses it carries no expiry: the commit bound is the tighter one, since a new push re-arms the gate.

The comment is not the only way through: a [`lunar policy bypass-pr`](/docs/lunar-cli.md#lunar-policy-bypass-pr) from the CLI also clears the check.

#### Turning the gate off

The gate is **fail-closed**. If Lunar is unavailable, gated merge requests stay blocked rather than falling open. That is the safe default for a compliance control, but it means you should know the lever before you need it.

To unblock merges during an incident, a project **Owner** disables **Status checks must succeed** in **Settings → Merge requests**.

### Commit status (default)

On **GitLab Free and Premium** there are no external status checks, so Lunar posts a commit status named `Earthly Lunar` instead. Lunar provisions nothing and changes nothing in your projects on these tiers; it only writes the status.

A commit status folds into the commit's pipeline result, so it **can** gate merges, but you enable that yourself, per project, in **Settings → Merge requests → Merge checks → Pipelines must succeed**. With it on, a failing Lunar status makes the pipeline fail and the merge request unmergeable.

Two consequences worth knowing before relying on it:

* **It is not Lunar-specific.** The setting gates on the pipeline as a whole, so a Lunar failure and a broken test look the same to it, and you cannot require Lunar without also requiring everything else in the pipeline. The Ultimate status check is a named requirement; this is not.
* **Pending counts as not-succeeded.** Lunar reports `running` while a commit's checks are still evaluating, so a merge request stays unmergeable until Lunar resolves. That is the point, but it means a stuck evaluation blocks merges with no bypass comment available. There is no `/lunar bypass:` on these tiers; the lever is the project setting itself.

## MR status reporting

On a merge request, Lunar reports each evaluation as:

* **A named result**: the `Earthly Lunar` status check on Ultimate, or a commit status on Free and Premium. See [Merge gate](#merge-gate) for which of those can block.
* **A merge-request comment** summarising the policy results, updated in place as new commits arrive rather than posted repeatedly.

Reporting depends on merge-request webhooks, which the Hub registers on each project automatically.

## Main branch status reporting

On commits to the default branch, Lunar posts a **commit status**, on every tier.

Status checks exist only on merge requests, so there is no Ultimate-specific behaviour here and nothing to configure: default-branch reporting looks the same whichever tier you are on. These statuses are informational; there is no merge to block.

### Project badges

Lunar maintains a **project badge** that reflects the latest main-branch result, so a project's standing is visible from its overview page without opening a commit or a dashboard.

Lunar provisions the badge with the service account's token, as it does the merge gate's status check, so the account needs the same `Maintainer` or `Owner` role.

## Next steps

Self-hosted installs carry the token into a Kubernetes secret at [prereqs Step 6](/install/lunar-hub/self-hosted/prerequisites.md#step-6-plan-your-kubernetes-secrets), and into your chart values at [install Step 4](/install/lunar-hub/self-hosted/install-walkthrough.md#git-platform-credentials). On Dedicated you [deposit it](/install/lunar-hub/dedicated/setup.md#step-4-deposit-your-secrets) into your install's secret drop instead.

After that, set up [config sync from GitLab CI](/install/lunar-hub/self-hosted/sync-config.md#gitlab-ci) so the Hub's copy of your configuration stays current as you edit it. [Git Platforms](/install/git-platforms.md) has the full capability comparison with GitHub.


---

# Agent Instructions
This documentation is published with GitBook. GitBook is the documentation platform designed so that both humans and AI agents can read, navigate, and reason over technical content effectively. Learn more at gitbook.com.

## Querying This Documentation
If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter, and the optional `goal` query parameter:

```
GET https://docs-lunar.earthly.dev/install/git-platforms/gitlab.md?ask=<question>&goal=<endgoal>
```

`ask` is the immediate question: it should be specific, self-contained, and written in natural language.
`goal` is optional and describes the broader end goal you are ultimately trying to accomplish on behalf of the user. GitBook uses it to tailor the answer towards what is most useful for that goal.

The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
