> ## 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.

# CircleCI

> Plan and apply on pull request branches, destroy through the API, production behind a hold job.

[config.yml](https://github.com/MishaBear94/Sponson/blob/main/integrations/circleci/config.yml) runs Sponson on every push to a branch with an open pull request, finishes the lines that
wait on the deploy, and applies production after a human approves a hold job on `main`. CircleCI has no "pull request
closed" trigger, so destroying a preview is a pipeline you start through the API (below).

| Moment | Workflow, job | What runs |
| - | - | - |
| Push to a branch with an open PR | `sponson-preview`: `sponson-plan`, `sponson-apply`, `sponson-finish` | plan; apply (stops with status `partial` at the deploy barrier); `apply --wait` (finishes the lines that needed the preview URL) |
| PR merged or closed | `sponson-destroy` | `sponson apply --destroy`, when the pipeline parameter `sponson-destroy-pr` is set |
| Push to `main` | `sponson-production`: `hold`, `sponson-production` | `sponson apply --env production --approved-by <who approved hold>` |

Jobs on a branch without an open pull request stop at their first step (`circleci-agent step halt`) and succeed.

## Prerequisites

* A project on CircleCI with the repository on GitHub. For Bitbucket, change the receipts remote (below).
* A `release.plan.yaml` at the repository root (`sponson init` writes one).
* The docker image `cimg/node:lts` (Node.js 22 or later).

## Setup

1. Copy [config.yml](https://github.com/MishaBear94/Sponson/blob/main/integrations/circleci/config.yml) to `.circleci/config.yml`, or merge its parameters, commands, jobs and workflows
   into yours. If CircleCI deploys the app, put your deploy jobs between `sponson-apply` and `sponson-finish`.
2. Replace `0.5.0` with the Sponson version you have reviewed.
3. Create two [contexts](https://circleci.com/docs/contexts/) (**Organization Settings → Contexts**) with the
   variables below. Restrict `sponson-production` to a security group of the people allowed to deploy.

## Variables

| Context | Variable | Value |
| - | - | - |
| both | `SPONSON_GIT_TOKEN` | a token that may push to the repository (below) |
| `sponson-preview` | `VERCEL_TOKEN`, `NEON_API_KEY`, `CLERK_SECRET_KEY` | the preview tokens; one per adapter your plan uses |
| `sponson-production` | the same names | the production tokens |
| `sponson-production` | `CIRCLE_TOKEN` | a CircleCI personal API token, to read who approved `hold` |
| either | every `env://NAME` a line references | the secret values |

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 repository, `sponson-receipts/<env>/<scope>`, from a clone of
its own. CircleCI checks out with a read-only deploy key, so the template points that clone at a token that can push:

* **GitHub:** a fine-grained personal access token (or a GitHub App installation token) for this repository only,
  with **Contents: Read and write**. The `sponson-setup` command exports
  `SPONSON_RECEIPTS_REMOTE=https://x-access-token:${SPONSON_GIT_TOKEN}@github.com/${CIRCLE_PROJECT_USERNAME}/${CIRCLE_PROJECT_REPONAME}.git`.
* **Bitbucket:** a repository access token with **Repositories: Write**, and the remote
  `https://x-token-auth:${SPONSON_GIT_TOKEN}@bitbucket.org/${CIRCLE_PROJECT_USERNAME}/${CIRCLE_PROJECT_REPONAME}.git`.
* **SSH instead of a token:** add a read-write deploy key under **Project Settings → SSH Keys**, load it with
  `add_ssh_keys` before `checkout`, and delete the `Receipts remote` step: Sponson then pushes to `origin`.

Sponson strips credentials from every git error it prints. The receipts branches carry no `.circleci/config.yml`;
if CircleCI reports a failed pipeline for each receipts push, turn on **Only build pull requests** in the project's
advanced settings.

## Destroying a closed pull request's preview

Start a pipeline on `main` with both parameters set, for example from a GitHub workflow on `pull_request: closed`, a
chat command, or by hand:

```bash theme={null}
curl -fsS -X POST "https://circleci.com/api/v2/project/gh/<org>/<repo>/pipeline" \
  -H "Circle-Token: $CIRCLE_TOKEN" -H "Content-Type: application/json" \
  -d '{"branch": "main", "parameters": {"sponson-destroy-pr": "42", "sponson-destroy-branch": "feat/x"}}'
```

The scope is `pr-42` whatever branch the pipeline runs on; running it on `main` works after the PR's branch is
deleted. Only the `sponson-destroy` workflow runs when `sponson-destroy-pr` is set.

## How the flags are filled

| Flag | Variable |
| - | - |
| `--pr` | the number at the end of `CIRCLE_PULL_REQUEST` (`${CIRCLE_PULL_REQUEST##*/}`) |
| `--branch` | `CIRCLE_BRANCH` |
| `--sha` | `CIRCLE_SHA1` |

Sponson 0.5.0 and later read the same variables themselves when `CIRCLECI` is set (and `CIRCLE_PR_NUMBER` for
pull requests from forks), so the flags may then be dropped.

## Notes

* `CIRCLE_PULL_REQUEST` is only set when the pull request already existed when the pipeline started. A branch pushed
  before its PR was opened gets its first preview on the next push.
* CircleCI has no variable naming who approved a hold job. `sponson-production` reads it from the API (the hold
  job's `approved_by`, then the user's `login`) and fails when no approval is recorded; Sponson refuses a production
  apply without an approver in any case.
* Projects that use CircleCI's GitHub App integration trigger pipelines differently from the `/pipeline` endpoint
  above; see CircleCI's documentation for triggering a pipeline with parameters.
* What has been run (`scripts/try-ci-templates.sh circleci`): every job's steps verbatim in `cimg/node:lts`, in the
  order the workflows run them, against the fake cloud: `sponson-preview` on the pull request's branch (apply stopping
  at the deploy barrier, finish waiting for it), `sponson-plan` halting on `main`, `sponson-production` with the
  approver read through a stand-in for CircleCI's API, and `sponson-destroy` with the pipeline parameters. The
  checkout, the predefined variables and the API are emulated; not run on CircleCI itself. See
  [how the templates are checked](/integrations/overview#how-the-templates-are-checked).

## The template: config.yml

```yaml config.yml theme={null}
# Sponson on CircleCI. Copy into .circleci/config.yml (or merge the jobs and workflows into yours).
# integrations/circleci/README.md lists the environment variables to set and how receipts are pushed.
#
#   push to a branch with an open pull request   sponson-plan, sponson-apply, sponson-finish (waits for the deploy)
#   pull request merged or closed                CircleCI has no such trigger: start the sponson-destroy workflow
#                                                through the API with the pipeline parameters below (README.md)
#   push to main                                 hold (an approval job), then sponson-production
#
# 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.

version: 2.1

parameters:
  # Set both through the API to destroy a closed pull request's preview resources.
  sponson-destroy-pr:
    type: string
    default: ""
  sponson-destroy-branch:
    type: string
    default: ""

executors:
  node:
    docker:
      - image: cimg/node:lts

commands:
  sponson-setup:
    steps:
      - checkout
      - run:
          name: Receipts remote
          # Receipts are pushed to orphan branches sponson-receipts/<env>/<scope>. CircleCI's checkout key is
          # read-only, so SPONSON_GIT_TOKEN is a token that may push to this repository (README.md).
          command: |
            echo "export SPONSON_RECEIPTS_REMOTE=\"https://x-access-token:${SPONSON_GIT_TOKEN}@github.com/${CIRCLE_PROJECT_USERNAME}/${CIRCLE_PROJECT_REPONAME}.git\"" >> "$BASH_ENV"
  sponson-pr-only:
    steps:
      - run:
          name: Skip branches without a pull request
          command: |
            if [ -z "$CIRCLE_PULL_REQUEST" ]; then echo "no open pull request: nothing to plan"; circleci-agent step halt; fi

jobs:
  sponson-plan:
    executor: node
    steps:
      - sponson-pr-only
      - sponson-setup
      - run:
          name: sponson plan
          command: npx -y sponson@0.5.0 plan --env preview --pr "${CIRCLE_PULL_REQUEST##*/}" --branch "$CIRCLE_BRANCH" --sha "$CIRCLE_SHA1"

  sponson-apply:
    executor: node
    steps:
      - sponson-pr-only
      - sponson-setup
      - run:
          name: sponson apply
          command: npx -y sponson@0.5.0 apply --env preview --pr "${CIRCLE_PULL_REQUEST##*/}" --branch "$CIRCLE_BRANCH" --sha "$CIRCLE_SHA1"

  sponson-finish:
    executor: node
    steps:
      - sponson-pr-only
      - sponson-setup
      - run:
          name: sponson apply --wait (finish the lines that needed the deploy)
          no_output_timeout: 20m
          command: npx -y sponson@0.5.0 apply --wait --wait-timeout 900 --env preview --pr "${CIRCLE_PULL_REQUEST##*/}" --branch "$CIRCLE_BRANCH" --sha "$CIRCLE_SHA1"

  sponson-destroy:
    executor: node
    steps:
      - sponson-setup
      - run:
          name: sponson apply --destroy
          environment:
            DESTROY_PR: << pipeline.parameters.sponson-destroy-pr >>
            DESTROY_BRANCH: << pipeline.parameters.sponson-destroy-branch >>
          command: npx -y sponson@0.5.0 apply --destroy --env preview --pr "$DESTROY_PR" --branch "$DESTROY_BRANCH" --sha "$CIRCLE_SHA1"

  sponson-production:
    executor: node
    steps:
      - sponson-setup
      - run:
          name: Who approved the hold job
          # CircleCI exposes no approver variable: read it from the API (CIRCLE_TOKEN, README.md).
          command: |
            approver_id=$(curl -fsS -H "Circle-Token: $CIRCLE_TOKEN" "https://circleci.com/api/v2/workflow/$CIRCLE_WORKFLOW_ID/job" \
              | node -e 'let s="";process.stdin.on("data",(d)=>(s+=d)).on("end",()=>{const j=JSON.parse(s).items.find((i)=>i.name==="hold");process.stdout.write(j?.approved_by??"")})')
            [ -n "$approver_id" ] || { echo "no approval recorded for the hold job"; exit 1; }
            login=$(curl -fsS -H "Circle-Token: $CIRCLE_TOKEN" "https://circleci.com/api/v2/user/$approver_id" | node -e 'let s="";process.stdin.on("data",(d)=>(s+=d)).on("end",()=>process.stdout.write(JSON.parse(s).login??""))')
            echo "export SPONSON_APPROVED_BY=\"${login:-$approver_id}\"" >> "$BASH_ENV"
      - run:
          name: sponson apply --env production
          command: npx -y sponson@0.5.0 apply --env production --pr "" --branch "$CIRCLE_BRANCH" --sha "$CIRCLE_SHA1"

workflows:
  sponson-preview:
    when:
      equal: ["", << pipeline.parameters.sponson-destroy-pr >>]
    jobs:
      - sponson-plan:
          context: sponson-preview
      - sponson-apply:
          context: sponson-preview
          requires: [sponson-plan]
      # Your own deploy jobs go here, if CircleCI deploys the app; make sponson-finish require them.
      - sponson-finish:
          context: sponson-preview
          requires: [sponson-apply]

  sponson-destroy:
    when:
      not:
        equal: ["", << pipeline.parameters.sponson-destroy-pr >>]
    jobs:
      - sponson-destroy:
          context: sponson-preview

  sponson-production:
    when:
      and:
        - equal: [main, << pipeline.git.branch >>]
        - equal: ["", << pipeline.parameters.sponson-destroy-pr >>]
    jobs:
      - hold:
          type: approval
      - sponson-production:
          context: sponson-production
          requires: [hold]
```


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