# Deploy configuration from CI/CD

**Goal:** import a [Source CMS](https://dev.acquia.com/start-here/glossary/#source-cms) site's configuration from the CI system you already run, on every push to your main branch.

## What you'll have when you're done

- A pipeline job that imports the committed configuration on every push to your main branch, with no prompts.
- A job that fails when the site refuses the import.

## Prerequisites

- A working copy with `.acquia/config` committed and the site recorded, from [Sync configuration with acli](https://dev.acquia.com/source-cms/config-sync/cli/).
- A Source CMS site you hold site-administrator rights on. Organization administrators hold those rights today; see [what the pipeline's credentials can do](https://dev.acquia.com/source-cms/config-sync/ci-cd/#what-the-pipelines-credentials-can-do) before you create the token.
- Cloud Platform API credentials (key and secret). Create them as shown on the [Cloud Platform API page](https://dev.acquia.com/cloud-platform/cli/platform-api/).
- A CI system that can run a Linux x86_64 or macOS aarch64 job and hold two secrets.

## Steps

1. ### Give the pipeline credentials and a site

   Store a Cloud Platform API key and secret as CI secrets, and expose them as `ACLI_KEY` and `ACLI_SECRET`, the names `acli` reads in place of `acli auth:login`. The examples store them as `ACQUIA_CLOUD_KEY` and `ACQUIA_CLOUD_SECRET`.

   Supply the site with a committed `.acquia-cli.yml` or `--site`, as on [Sync configuration with acli](https://dev.acquia.com/source-cms/config-sync/cli/), and pass `--yes` to the import.

2. ### Add the job

   Each example installs the native `acli` binary and imports on every push to `main`.

   Only one import runs per site at a time, so a job that starts while the previous one is still importing is refused. Each example first waits for a running import to clear: `acli source:cms:config:push --status` reports the site's latest import without starting one, and exits `2` while it is still running. The CI system's retry handling repeats that check until it exits anything else.

   GitHub Actions has no built-in retry for one step, so its example uses the community action [`nick-fields/retry`](https://github.com/nick-fields/retry). Bitbucket retries a step on any failure, so its check runs in a step of its own.

   **GitHub Actions**

   ```yaml
   name: Push Source config
   on:
     push:
       branches: [main]
   jobs:
     push-config:
       runs-on: ubuntu-latest
       steps:
         - uses: actions/checkout@v4
         - name: Install Acquia CLI
           run: |
             curl -fsSL -o acli.tar.gz https://github.com/acquia/cli/releases/latest/download/native-acli-linux-x86_64.tar.gz
             tar -xzf acli.tar.gz
             sudo mv acli /usr/local/bin/acli
         - name: Wait for any running import to clear
           uses: nick-fields/retry@v3
           env:
             ACLI_KEY: ${{ secrets.ACQUIA_CLOUD_KEY }}
             ACLI_SECRET: ${{ secrets.ACQUIA_CLOUD_SECRET }}
           with:
             max_attempts: 5
             retry_wait_seconds: 60
             timeout_minutes: 1
             retry_on_exit_code: 2 # only retry while an import is running
             # 2 = running, retry; anything else (including a previous import's own refusal) means proceed
             command: 'acli source:cms:config:push --status --site=my-site-id; [ $? -eq 2 ] && exit 2 || exit 0'
         - name: Push configuration
           env:
             ACLI_KEY: ${{ secrets.ACQUIA_CLOUD_KEY }}
             ACLI_SECRET: ${{ secrets.ACQUIA_CLOUD_SECRET }}
           run: acli source:cms:config:push --site=my-site-id --yes
   ```

   **GitLab CI**

   ```yaml
   push-config:
     stage: deploy
     image: alpine:latest
     rules:
       - if: '$CI_COMMIT_BRANCH == "main"'
     variables:
       ACLI_KEY: $ACQUIA_CLOUD_KEY
       ACLI_SECRET: $ACQUIA_CLOUD_SECRET
     retry:
       max: 5
       exit_codes: 2 # only retry the job while an import is running
     before_script:
       - apk add --no-cache curl
       - curl -fsSL -o acli.tar.gz https://github.com/acquia/cli/releases/latest/download/native-acli-linux-x86_64.tar.gz
       - tar -xzf acli.tar.gz && mv acli /usr/local/bin/acli
     script:
       # 2 = running, fail the job so retry.exit_codes retries it; anything else
       # (including a previous import's own refusal) means proceed
       - 'acli source:cms:config:push --status --site=my-site-id; [ $? -eq 2 ] && exit 2 || exit 0'
       - acli source:cms:config:push --site=my-site-id --yes
   ```

   **Bitbucket Pipelines**

   ```yaml
   pipelines:
     branches:
       main:
         - step:
             name: Wait for any running import to clear
             image: alpine:latest
             script:
               - apk add --no-cache curl
               - curl -fsSL -o acli.tar.gz https://github.com/acquia/cli/releases/latest/download/native-acli-linux-x86_64.tar.gz
               - tar -xzf acli.tar.gz && mv acli /usr/local/bin/acli
               - export ACLI_KEY=$ACQUIA_CLOUD_KEY
               - export ACLI_SECRET=$ACQUIA_CLOUD_SECRET
               # 2 = running, fail the step so on-fail.strategy retries it; anything else
               # (including a previous import's own refusal) means proceed
               - 'acli source:cms:config:push --status --site=my-site-id; [ $? -eq 2 ] && exit 2 || exit 0'
             on-fail:
               strategy: retry
               maxRetryCount: 5 # kept in its own step: this retries on ANY step failure, not just exit 2
         - step:
             name: Push Source config
             image: alpine:latest
             script:
               - apk add --no-cache curl
               - curl -fsSL -o acli.tar.gz https://github.com/acquia/cli/releases/latest/download/native-acli-linux-x86_64.tar.gz
               - tar -xzf acli.tar.gz && mv acli /usr/local/bin/acli
               - export ACLI_KEY=$ACQUIA_CLOUD_KEY
               - export ACLI_SECRET=$ACQUIA_CLOUD_SECRET
               - acli source:cms:config:push --site=my-site-id --yes
   ```

   The native binary is built for Linux x86_64 and macOS aarch64 only. On an ARM Linux or Windows runner, use `acli.phar` with PHP instead.

3. ### Read the outcome in the pipeline

   A refused or failed import fails the job. For what each outcome means, see [Read the outcome](https://dev.acquia.com/source-cms/config-sync/cli/#read-the-outcome).

## What the pipeline's credentials can do

Syncing requires site-administrator rights, which today only organization administrators hold. The pipeline's token can therefore reach every site in the organization, not only the one it syncs. Keep it in your CI system's secret store, limit who can run the pipeline, and rotate it with your other Acquia credentials.

## What just happened

The job runs the same import as a terminal session. `ACLI_KEY` and `ACLI_SECRET` replace `acli auth:login`, and `--yes` with a known site replaces every prompt. A refusal appears only in the command's output, so read the failed job's log.

## When something goes wrong

**A sync is already running for the site.** The second import is refused without touching the site, and nothing is queued. Rerun the job once the running import finishes, or raise the wait step's retry budget.

For the site's other refusals, see [When something goes wrong](https://dev.acquia.com/source-cms/config-sync/cli/#when-something-goes-wrong) on the acli page.

## Next steps

- [Deploy from your own CI/CD](https://dev.acquia.com/source-cms/deploy/external-ci/): the same shape of job for a headless frontend's build and deploy.
