> ## Documentation Index
> Fetch the complete documentation index at: https://sponson.mintlify.site/llms.txt
> Use this file to discover all available pages before exploring further.

# GitLab CI/CD

> Merge request pipelines: plan, apply, finish after the deploy, destroy when the environment stops.

[gitlab-ci.yml](https://github.com/MishaBear94/Sponson/blob/main/integrations/gitlab/gitlab-ci.yml) runs Sponson in merge request pipelines: it plans and applies when a merge request is
opened or updated, finishes the lines that wait on the deploy, and destroys the preview resources when the merge
request is merged or closed. Production is a manual job on the default branch.

| Moment | Job | What runs |
| - | - | - |
| MR opened or updated | `sponson:plan` | `sponson plan` (read-only diff in the job log) |
| | `sponson:apply` | `sponson apply`: creates the database branch, injects variables, stops with status `partial` at the deploy barrier |
| | `sponson:finish` | `sponson apply --wait`: after your `deploy` stage (or the host's own deploy), finishes the lines that needed the preview URL |
| MR merged or closed | `sponson:destroy` | `sponson apply --destroy`, run by GitLab when it stops the `review/mr-<iid>` environment |
| Pipeline on the default branch | `sponson:production` | `sponson apply --env production`, a manual job; whoever starts it is the approver |

## Prerequisites

* GitLab 16 or later, with [merge request pipelines](https://docs.gitlab.com/ci/pipelines/merge_request_pipelines/)
  (the template's `workflow:` switches to them for every branch with an open MR).
* A `release.plan.yaml` at the repository root (`sponson init` writes one).
* Runners that can pull `node:22` and reach npm and the providers' APIs.

## Setup

1. Copy [gitlab-ci.yml](https://github.com/MishaBear94/Sponson/blob/main/integrations/gitlab/gitlab-ci.yml) to `.gitlab-ci.yml`, or merge its `workflow:`, `stages:` and jobs into yours.
   Put your own deploy jobs in the `deploy` stage, if GitLab deploys the app.
2. Replace `0.5.0` with the Sponson version you have reviewed. Every job pins it, so an npm release never changes
   what runs.
3. Create the receipts token and the CI/CD variables below (**Settings → CI/CD → Variables**).
4. Protect the `production` environment (**Operate → Environments → Protected environments**) so only the people
   allowed to deploy can start `sponson:production`.

## Variables

| Variable | Flags | Value |
| - | - | - |
| `SPONSON_GITLAB_TOKEN` | masked, not protected | project access token with `write_repository` (below) |
| `VERCEL_TOKEN`, `NEON_API_KEY`, `CLERK_SECRET_KEY` | masked, not protected, environment scope `review/*` | the preview tokens; one per adapter your plan uses |
| the same names | masked, protected, environment scope `production` | the production tokens |
| every `env://NAME` a line references | masked, scoped like the tokens | the secret values |

Merge request pipelines of unprotected branches cannot read protected variables, so the preview tokens must not be
protected. Scoping them to `review/*` and the production ones to `production` keeps each job to its own tokens.
Every adapter's variable is listed under [built-in ops](/reference/plan-format#built-in-ops).

## Receipts: push credentials

Sponson writes its receipts to orphan branches of the project, `sponson-receipts/<env>/<scope>`, from a clone of its
own. The job token (`CI_JOB_TOKEN`) cannot push to the repository, so the template points that clone at a token that
can:

1. **Settings → Access tokens → Add new token**: name `sponson-receipts`, role **Developer**, scope
   **`write_repository`** only, an expiry date you will rotate before.
2. Save it as the CI/CD variable `SPONSON_GITLAB_TOKEN` (masked).
3. The `.sponson` job template exports
   `SPONSON_RECEIPTS_REMOTE=${CI_SERVER_PROTOCOL}://oauth2:${SPONSON_GITLAB_TOKEN}@${CI_SERVER_HOST}:${CI_SERVER_PORT}/${CI_PROJECT_PATH}.git`.
   Sponson strips credentials from every git error it prints.

If a protected-branch rule matches `sponson-receipts/*` (for example `*`), allow Developers to push to it or give the
token the Maintainer role. The receipts branches carry no `.gitlab-ci.yml`, so pushing them starts no pipeline. To keep
receipts out of git instead, pass `--receipts local` and keep `.sponson/receipts` as a job artifact or cache; a fresh
runner then starts without the last run's receipt, so prefer the git store.

## How the flags are filled

Every job passes `--pr`, `--branch` and `--sha` from GitLab's predefined variables, so the template works with any
Sponson version:

| Flag | Variable |
| - | - |
| `--pr` | `CI_MERGE_REQUEST_IID` (the `!N` of the project) |
| `--branch` | `CI_MERGE_REQUEST_SOURCE_BRANCH_NAME` (`CI_COMMIT_BRANCH` on the default branch) |
| `--sha` | `CI_MERGE_REQUEST_SOURCE_BRANCH_SHA` in merged-results pipelines (the head that is deployed, not the synthetic merge commit), else `CI_COMMIT_SHA` |

Sponson 0.5.0 and later read the same variables themselves when `GITLAB_CI` is set, so the flags may then be
dropped.

## Notes

* `sponson:destroy` is the environment's `on_stop` job. GitLab runs it when the merge request is merged or closed, and
  you can run it from the environment's page. It checks out the merge request's ref (`refs/merge-requests/<iid>/head`),
  which GitLab keeps after the source branch is deleted, so the plan file is there to destroy from.
* `interruptible: false` on the applying jobs: GitLab's auto-cancel of redundant pipelines must not kill an apply half
  way. A killed apply is resumed by the next one; a cancelled one is not rolled back.
* To clean up previews whose merge request is forgotten, add `auto_stop_in: 2 weeks` to the `environment:` of
  `sponson:apply` and `sponson:finish`.
* What has been run: this file, unchanged but for the version, through
  [gitlab-ci-local](https://github.com/firecow/gitlab-ci-local) in `node:22` containers against the fake cloud
  (`scripts/try-ci-templates.sh gitlab`): a merge request pipeline (`sponson:plan`; `sponson:apply` stopping at the
  deploy barrier; `sponson:finish` waiting for the deploy), the manual `sponson:production` on the default branch
  with `GITLAB_USER_LOGIN` recorded as the approver, and `sponson:destroy`, with the tokens as variables scoped to
  `review/*` and `production`. Not run on GitLab itself; see
  [how the templates are checked](/integrations/overview#how-the-templates-are-checked).

## The template: gitlab-ci.yml

```yaml gitlab-ci.yml theme={null}
# Sponson on GitLab CI/CD, with merge request pipelines. Copy into .gitlab-ci.yml and merge `stages:` with yours.
# integrations/gitlab/README.md lists the CI/CD variables to set and how receipts are pushed.
#
#   MR opened or updated          sponson:plan, sponson:apply (stops at the deploy barrier with status partial),
#                                 your deploy jobs, then sponson:finish (waits for the deploy, finishes the lines)
#   MR merged or closed           GitLab stops the review environment, which runs sponson:destroy
#   pipeline on the default branch   sponson:production, a manual job a human starts (the approver)
#
# Replace 0.5.0 everywhere with the Sponson version you have reviewed. The flags are explicit so the template works
# with any version; Sponson 0.5.0 and later also read the same variables by themselves.

workflow:
  rules:
    # Merge request pipelines for MRs, branch pipelines otherwise, never both for the same push.
    - if: $CI_PIPELINE_SOURCE == "merge_request_event"
    - if: $CI_COMMIT_BRANCH && $CI_OPEN_MERGE_REQUESTS
      when: never
    - if: $CI_COMMIT_BRANCH

stages:
  - sponson-plan
  - sponson-apply
  - deploy            # your own deploy jobs, if GitLab deploys the app; leave it empty when the host deploys on push
  - sponson-finish

.sponson:
  image: node:22
  variables:
    # Full history: lets Sponson recognise a late build of an older commit.
    GIT_DEPTH: "0"
  before_script:
    # Receipts are pushed to orphan branches sponson-receipts/<env>/<scope> of this project. CI_JOB_TOKEN cannot
    # push, so SPONSON_GITLAB_TOKEN is a project access token with write_repository (README.md).
    - export SPONSON_RECEIPTS_REMOTE="${CI_SERVER_PROTOCOL}://oauth2:${SPONSON_GITLAB_TOKEN}@${CI_SERVER_HOST}:${CI_SERVER_PORT}/${CI_PROJECT_PATH}.git"

.sponson-mr:
  extends: .sponson
  rules:
    - if: $CI_PIPELINE_SOURCE == "merge_request_event"

sponson:plan:
  extends: .sponson-mr
  stage: sponson-plan
  interruptible: true
  # `prepare` gives the job the environment's scoped variables (the preview tokens) without recording a deployment.
  environment:
    name: review/mr-$CI_MERGE_REQUEST_IID
    action: prepare
  script:
    - npx -y sponson@0.5.0 plan --env preview --pr "$CI_MERGE_REQUEST_IID" --branch "$CI_MERGE_REQUEST_SOURCE_BRANCH_NAME" --sha "${CI_MERGE_REQUEST_SOURCE_BRANCH_SHA:-$CI_COMMIT_SHA}"

sponson:apply:
  extends: .sponson-mr
  stage: sponson-apply
  # Never cancel an apply half way: Sponson rolls back a failed run, not a killed one (the next run resumes it).
  interruptible: false
  environment:
    name: review/mr-$CI_MERGE_REQUEST_IID
    on_stop: sponson:destroy
  script:
    - npx -y sponson@0.5.0 apply --env preview --pr "$CI_MERGE_REQUEST_IID" --branch "$CI_MERGE_REQUEST_SOURCE_BRANCH_NAME" --sha "${CI_MERGE_REQUEST_SOURCE_BRANCH_SHA:-$CI_COMMIT_SHA}"

sponson:finish:
  extends: .sponson-mr
  stage: sponson-finish
  interruptible: false
  environment:
    name: review/mr-$CI_MERGE_REQUEST_IID
    on_stop: sponson:destroy
  script:
    # Waits for the preview deployment of this commit, then applies the lines that needed its URL.
    - npx -y sponson@0.5.0 apply --wait --wait-timeout 900 --env preview --pr "$CI_MERGE_REQUEST_IID" --branch "$CI_MERGE_REQUEST_SOURCE_BRANCH_NAME" --sha "${CI_MERGE_REQUEST_SOURCE_BRANCH_SHA:-$CI_COMMIT_SHA}"

sponson:destroy:
  extends: .sponson
  stage: sponson-finish
  interruptible: false
  # Run by GitLab when the MR is merged or closed (it stops the environment), or by hand from the environment page.
  rules:
    - if: $CI_PIPELINE_SOURCE == "merge_request_event"
      when: manual
      allow_failure: true
  environment:
    name: review/mr-$CI_MERGE_REQUEST_IID
    action: stop
  script:
    - npx -y sponson@0.5.0 apply --destroy --env preview --pr "$CI_MERGE_REQUEST_IID" --branch "$CI_MERGE_REQUEST_SOURCE_BRANCH_NAME" --sha "${CI_MERGE_REQUEST_SOURCE_BRANCH_SHA:-$CI_COMMIT_SHA}"

sponson:production:
  extends: .sponson
  stage: sponson-apply
  interruptible: false
  # A manual job: whoever starts it is the approver. Protect the `production` environment so only the people
  # allowed to deploy can start it, and scope the production tokens to it (README.md).
  rules:
    - if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH
      when: manual
  environment:
    name: production
    deployment_tier: production
  script:
    - npx -y sponson@0.5.0 apply --env production --pr "" --branch "$CI_COMMIT_BRANCH" --sha "$CI_COMMIT_SHA" --approved-by "$GITLAB_USER_LOGIN"
```


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.