> ## Documentation Index
> Fetch the complete documentation index at: https://www.ravion.com/docs/llms.txt
> Use this file to discover all available pages before exploring further.

# CI integration

> Plan Ravion configuration changes on every pull request and apply them on merge using GitHub Actions, GitLab CI, or your own runner.

Keep your [project](/config-as-code/project-config-file) and [pipeline](/config-as-code/pipeline-config-file) config files in your repository and let GitHub Actions manage them. Open a pull request to preview the planned changes as a comment, then apply them automatically when the pull request merges.

The workflow uses path filters so unrelated changes do not allocate a runner. YAML anchors define each config file path once and reuse it in the pull request and push filters.

Two jobs cover projects and pipelines. Only the job for the current event runs:

* **Plan on pull request** — shallow-fetches the comparison commit, runs `ravion ... config apply --dry-run` for each changed resource, and posts every plan in a single, updating PR comment.
* **Apply on merge** — shallow-fetches the comparison commit and runs `ravion ... config apply` for each changed resource when the branch merges into your default branch.

## Authenticate in CI

The CLI reads a service-account API key from the `RAVION_API_KEY` environment variable.

<Steps>
  <Step title="Create a service-account API key">
    Create a service account for CI and an API key for it. See [`ravion
            service-account`](/cli/reference/service-account) and [`ravion
            api-key`](/cli/reference/api-key).
  </Step>

  <Step title="Store the key as a repository secret">
    In your GitHub repository, go to **Settings → Secrets and variables → Actions** and add a secret
    named `RAVION_API_KEY` with the API key value.
  </Step>
</Steps>

## Add the workflow

Copy the prompt below into your coding agent to add and configure the GitHub action.

<Prompt description="Discover your Ravion config files and add the CI workflow." icon="sparkles" actions={["copy", "cursor"]}>
  Add a GitHub Actions workflow to this repository that plans Ravion config changes on pull requests and applies them on merge to the default branch. Work through these steps:

  1. Fetch the workflow template from the Ravion docs at [https://www.ravion.com/docs/config-as-code/ci-integration](https://www.ravion.com/docs/config-as-code/ci-integration) and use the `.github/workflows/ravion.yml` YAML block there as the starting point. Keep its jobs and logic unchanged. Only edit the indexed resource variables, their path aliases, `RAVION_CLI_VERSION`, optionally `RAVION_AUTOAPPROVE`, the default branch, and the `runs-on` values as described below.

  2. Find every Ravion config file in the repo:
     * Search files with these extensions: `.yaml`, `.yml`, `.json`, `.jsonc`, `.cue`. The names usually include `ravion` (project configs) or `pipeline` (pipeline configs), so prioritize those, but confirm each by its contents.
     * Classify each by its top-level keys (ignore everything else, such as `package.json`, CI configs, or Kubernetes manifests):
       * Project config: has a top-level `project` object AND an `environments` array whose entries contain `moduleInstances` (modules have a `type` like `rvn-...` and reference each other with `moduleGivenIdRef`).
       * Pipeline config: has top-level `steps` AND `variants` arrays (often also `triggers` and `inputs`).
     * Exclude legacy Flightcontrol config files. Skip a file if any of these are true: it is named `flightcontrol.json`, `flightcontrol.cue`, or `flightcontrol-*.json`; it has a `$schema` pointing at `app.flightcontrol.dev`; or it has a top-level `environments` array whose entries contain `services` (with fields like `buildType`, `ci`, `region`, or `source`) and there is NO top-level `project` object. These are a different product.
     * Read each file's unique ID from inside the file. Project: use `project.id` (a value starting with `proj_`); do not put `project.givenId` in the workflow, and if only `project.givenId` is present, look up the unique ID by running `ravion project list --json` (when the `ravion` CLI is installed and authenticated) and using the `id` of the entry whose `givenId` matches, otherwise treat the ID as undetermined. Pipeline: use the `pipe_` ID, which Ravion writes into a header comment at the top of pulled files; if the body has no ID, take it from that comment.
     * For `.jsonc` and `.cue`, account for comments when parsing. Use each file's path relative to the repo root for its `*_FILE` value.

  3. Add one contiguous indexed environment-variable pair for each config file:
     * Projects use `RAVION_PROJECT_N_ID` and `RAVION_PROJECT_N_FILE`, starting at `N=1`. Set the ID to the `proj_` ID. Define a unique YAML anchor on the file value, for example `RAVION_PROJECT_1_FILE: &ravion_project_1_file packages/web/ravion.yaml`.
     * Pipelines use `RAVION_PIPELINE_N_ID` and `RAVION_PIPELINE_N_FILE`, starting at `N=1`. Set the ID to the `pipe_` ID and define a unique YAML anchor on the file value.
     * Keep indexes contiguous within each resource type. Omit that resource type when none exist.

  4. Add every file anchor once to `on.pull_request.paths`. Keep the `&ravion_paths` anchor on that list and `paths: *ravion_paths` under `on.push`, so both events use the same filters without repeating any path string.

  5. Set `env.RAVION_CLI_VERSION` to the latest Ravion CLI version number, without the leading `v`. Find it from the latest release at [https://github.com/flightcontrolhq/cli/releases/latest](https://github.com/flightcontrolhq/cli/releases/latest) (the URL redirects to the newest tag, such as `v0.1.6` — use `0.1.6`), or run `ravion --version` if the CLI is installed.

  6. Set `on.push.branches` to the repository's default branch. Match the runner to the repo by checking the other workflows under `.github/workflows/`. If they run on a custom or self-hosted runner (a `runs-on` value other than the GitHub-hosted `ubuntu-latest`/`ubuntu-*` labels — for example a self-hosted label or a runner group), set `runs-on` to that same runner for both jobs. Otherwise keep `ubuntu-latest`.

  7. Write the workflow to `.github/workflows/ravion.yml`. If you could not determine the ID for any file, add its indexed pair with a TODO placeholder and list those files so I can fill them in.
</Prompt>

Create `.github/workflows/ravion.yml`. Define each project or pipeline as a contiguous indexed ID/file pair. Anchor every `*_FILE` value and reuse those aliases in `ravion_paths`, so each config file path appears once.

Set `RAVION_CLI_VERSION` to the [CLI release](/cli/releases) you want CI to use. Leave `RAVION_AUTOAPPROVE` as `"false"` to require manual approval of [stack change pipeline runs](/modules/stack#stacks-change-through-pipelines) in the Ravion dashboard, or set it to `"true"` to auto-approve project applies.

````yaml .github/workflows/ravion.yml theme={null}
name: Ravion config

env:
  RAVION_API_KEY: ${{ secrets.RAVION_API_KEY }}
  RAVION_AUTOAPPROVE: "false"
  RAVION_CLI_VERSION: "0.1.6"
  # Keep indexes contiguous within each resource type.
  RAVION_PROJECT_1_ID: proj_3AV4cGr0t841Bstn4swCCrHrgly
  RAVION_PROJECT_1_FILE: &ravion_project_1_file packages/web/ravion.yaml
  RAVION_PROJECT_2_ID: proj_1CV4cGr0t841Bstn4swCCrHrgh7
  RAVION_PROJECT_2_FILE: &ravion_project_2_file packages/api/ravion.yaml
  RAVION_PIPELINE_1_ID: pipe_8Kd2mZq0t41Bstn4swCCrHrAa9
  RAVION_PIPELINE_1_FILE: &ravion_pipeline_1_file ravion-pipeline.yaml

on:
  pull_request:
    paths: &ravion_paths
      - *ravion_project_1_file
      - *ravion_project_2_file
      - *ravion_pipeline_1_file
  push:
    branches:
      - main
    paths: *ravion_paths

jobs:
  plan:
    if: github.event_name == 'pull_request'
    runs-on: ubuntu-latest
    permissions:
      contents: read
      pull-requests: write
    steps:
      - uses: actions/checkout@v4
        with:
          fetch-depth: 1

      - &list_changed_resources
        id: list
        name: List changed resources
        env:
          BASE: ${{ github.event_name == 'pull_request' && github.event.pull_request.base.sha || github.event.before }}
          HEAD: ${{ github.sha }}
        run: |
          all_changed=false
          if [ "$BASE" = "0000000000000000000000000000000000000000" ]; then
            all_changed=true
          else
            if ! git cat-file -e "$BASE^{commit}" 2> /dev/null; then
              git fetch --no-tags --depth=1 origin "$BASE"
            fi
            git diff --name-only "$BASE" "$HEAD" > changed.txt
          fi

          resources='[]'
          configured=0
          for resource in project:RAVION_PROJECT pipeline:RAVION_PIPELINE; do
            kind=${resource%%:*}
            prefix=${resource#*:}
            i=1
            while true; do
              id_var="${prefix}_${i}_ID"
              file_var="${prefix}_${i}_FILE"

              if [[ ! -v $id_var && ! -v $file_var ]]; then
                break
              fi
              if [[ ! -v $id_var || ! -v $file_var ]]; then
                echo "$kind $i must define both $id_var and $file_var" >&2
                exit 1
              fi

              configured=$((configured + 1))
              id="${!id_var}"
              file="${!file_var}"
              changed=$all_changed
              if [ "$changed" = false ]; then
                while IFS= read -r changed_file; do
                  if [ "$changed_file" = "$file" ]; then
                    changed=true
                    break
                  fi
                done < changed.txt
              fi

              if [ "$changed" = true ]; then
                resources=$(jq -cn \
                  --argjson current "$resources" \
                  --arg kind "$kind" \
                  --arg id "$id" \
                  --arg file "$file" \
                  '$current + [{kind: $kind, id: $id, file: $file}]')
              fi

              i=$((i + 1))
            done
          done

          if [ "$configured" -eq 0 ]; then
            echo "At least one Ravion project or pipeline must be configured" >&2
            exit 1
          fi

          echo "resources=$resources" >> "$GITHUB_OUTPUT"

      - name: Install the Ravion CLI
        if: steps.list.outputs.resources != '[]'
        run: curl -fsSL "https://github.com/flightcontrolhq/cli/releases/download/v${RAVION_CLI_VERSION}/install.sh" | sh

      - name: Build plan comment
        if: steps.list.outputs.resources != '[]'
        id: plan
        env:
          RESOURCES: ${{ steps.list.outputs.resources }}
        run: |
          {
            echo 'body<<BODY_EOF'
            echo '<!-- ravion-plan -->'
            echo '## Ravion planned changes'
            echo

            while read -r resource; do
              kind=$(jq -r '.kind' <<< "$resource")
              id=$(jq -r '.id' <<< "$resource")
              file=$(jq -r '.file' <<< "$resource")
              name="$id"
              label="Pipeline"

              if [ "$kind" = project ]; then
                label="Project"
                name=$(ravion project get "$id" --json 2> /dev/null | jq -r '.name // empty') || true
                name=${name:-$id}
              fi

              echo "### [$name](https://app.ravion.com/go/$id)"
              echo
              echo "$label: \`$file\`"
              echo
              plan_file=$(mktemp)
              if [ "$kind" = project ]; then
                NO_COLOR=1 ravion project config apply "$id" --file "$file" --dry-run > "$plan_file" 2>&1 || true
              else
                NO_COLOR=1 ravion pipeline config apply "$id" --file "$file" --dry-run > "$plan_file" 2>&1 || true
              fi
              echo '```diff'
              cat "$plan_file"
              echo '```'
              echo
            done < <(jq -c '.[]' <<< "$RESOURCES")

            echo 'BODY_EOF'
          } >> "$GITHUB_OUTPUT"

      - name: Comment the plan on the PR
        if: steps.list.outputs.resources != '[]'
        uses: actions/github-script@v7
        with:
          script: |
            const marker = '<!-- ravion-plan -->';
            const body = ${{ toJSON(steps.plan.outputs.body) }};
            const { data: comments } = await github.rest.issues.listComments({
              owner: context.repo.owner,
              repo: context.repo.repo,
              issue_number: context.issue.number,
            });
            const existing = comments.find((c) => c.body.includes(marker));
            if (existing) {
              await github.rest.issues.updateComment({
                owner: context.repo.owner,
                repo: context.repo.repo,
                comment_id: existing.id,
                body,
              });
            } else {
              await github.rest.issues.createComment({
                owner: context.repo.owner,
                repo: context.repo.repo,
                issue_number: context.issue.number,
                body,
              });
            }

  apply:
    if: github.event_name == 'push'
    runs-on: ubuntu-latest
    permissions:
      contents: read
    steps:
      - uses: actions/checkout@v4
        with:
          fetch-depth: 1

      - *list_changed_resources

      - name: Install the Ravion CLI
        if: steps.list.outputs.resources != '[]'
        run: curl -fsSL "https://github.com/flightcontrolhq/cli/releases/download/v${RAVION_CLI_VERSION}/install.sh" | sh

      - name: Apply changes
        if: steps.list.outputs.resources != '[]'
        env:
          RESOURCES: ${{ steps.list.outputs.resources }}
        run: |
          while read -r resource; do
            kind=$(jq -r '.kind' <<< "$resource")
            id=$(jq -r '.id' <<< "$resource")
            file=$(jq -r '.file' <<< "$resource")

            if [ "$kind" = project ]; then
              autoapprove_args=()
              if [ "$RAVION_AUTOAPPROVE" = "true" ]; then
                autoapprove_args=(--autoapprove)
              fi
              ravion project config apply "$id" --file "$file" "${autoapprove_args[@]}"
            else
              ravion pipeline config apply "$id" --file "$file"
            fi
          done < <(jq -c '.[]' <<< "$RESOURCES")
````

<Note>
  GitHub leaves a path-filtered workflow's check pending when the workflow is configured as a
  required status check but none of its paths match. Keep this workflow optional, or use a separate
  always-running check for branch protection.
</Note>

<Warning>
  `RAVION_AUTOAPPROVE: "true"` makes project applies approve supported stack change pipeline runs
  without a manual gate. Removing a module instance still runs a Terraform destroy. Keep it
  `"false"` if you want a human to approve runs in the Ravion dashboard. It applies to projects
  only; pipeline applies always create a new pipeline version.
</Warning>

<Note>
  In CI the CLI isn't attached to a terminal, so its output is already plain text. The plan step
  also sets `NO_COLOR=1` to strip any ANSI color codes before the output goes into the PR comment.
</Note>

## Next steps

<CardGroup cols={2}>
  <Card title="Project config file" icon="folder" href="/config-as-code/project-config-file">
    Pull, edit, and apply a project's config.
  </Card>

  <Card title="Pipeline config file" icon="diagram-project" href="/config-as-code/pipeline-config-file">
    Pull, edit, and apply a pipeline's config.
  </Card>
</CardGroup>
