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

# CI Integration

> Plan on pull request, apply on merge

The production posture: the config lives in git, every change is planned on a pull request, and the apply runs from CI on merge. No credentials are stored in the pipeline.

```
edit _infra/synthetiq.yaml ──▶ open PR ──▶ plan job posts the diff ──▶ review & merge ──▶ apply job provisions
```

## Authentication

Each job authenticates with short-lived OIDC tokens issued by GitHub Actions for that run. The pull-request and merge contexts map to different identities:

| Context       | GitHub OIDC subject                            | AWS role                                                                             | Synthetiq identity                                                                           |
| ------------- | ---------------------------------------------- | ------------------------------------------------------------------------------------ | -------------------------------------------------------------------------------------------- |
| Pull request  | `repo:<github-org>/<repo>:pull_request`        | Plan role (change-set verbs only)                                                    | None — `generate` doesn't need one                                                           |
| Merge to main | `repo:<github-org>/<repo>:ref:refs/heads/main` | Apply role (full [provisioning policy](/docs/platform-docs/deployments/byoi/permissions)) | [Service account](/docs/platform-docs/deployments/byoi/service-account) holding `infra:provision` |

The apply job exchanges its GitHub token for a short-lived Synthetiq token through the service account's trust; deleting the trust revokes access immediately.

<Warning>
  The identity split is the security boundary — not workflow YAML or CLI flags. A PR branch controls its own workflow file, so the `pull_request` subject must map only to the plan role: no apply permissions, no Synthetiq identity. The apply identities are reachable only from merged code.
</Warning>

## One-time setup

<Steps>
  <Step title="Create the AWS roles">
    Two IAM roles in the target account, trusting GitHub's OIDC provider (`token.actions.githubusercontent.com`), scoped to your repository:

    * **synthetiq-infra-plan** — trusted for `repo:<github-org>/<repo>:pull_request`; policy from `synthetiq infra permissions --stage generate`
    * **synthetiq-infra-apply** — trusted for `repo:<github-org>/<repo>:ref:refs/heads/main`; policy from `synthetiq infra permissions --stage provision`
  </Step>

  <Step title="Create the Synthetiq service account and trust">
    Find the role id for **CI Provision Apply**:

    ```bash theme={null}
    synthetiq role list
    ```

    Create the service account with that role:

    ```bash theme={null}
    synthetiq service-account create "CI Provisioner" --role-id <role-id>
    ```

    Create the OIDC trust so CI can authenticate as it:

    ```bash theme={null}
    synthetiq trust create \
      --service-account-id <service-account-id> \
      --issuer https://token.actions.githubusercontent.com \
      --subject "repo:<github-org>/<repo>:ref:refs/heads/main"
    ```

    See [Service Account](/docs/platform-docs/deployments/byoi/service-account).
  </Step>

  <Step title="Add the workflow">
    The published Synthetiq workflow wires up both jobs with the correct triggers, OIDC permissions, and a concurrency guard so two changes can't apply at once:

    ```yaml theme={null}
    name: synthetiq-infra
    on:
      # The pinned @synthetiq/cli version is a generation input — bumping it
      # changes the rendered stack — so package.json triggers a plan too.
      pull_request:
        paths: ["_infra/**", "package.json", "package-lock.json"]
      push:
        branches: [main]
        paths: ["_infra/**", "package.json", "package-lock.json"]
    permissions:
      id-token: write      # OIDC tokens for AWS and Synthetiq
      contents: write      # plan commits the reviewed changeset to the PR branch
      pull-requests: write # the plan comment
    jobs:
      infra:
        uses: SynthetiqAI/infra-workflow/.github/workflows/plan-apply.yml@v1
        with:
          plan-role-arn: arn:aws:iam::<your-aws-account-id>:role/synthetiq-infra-plan
          apply-role-arn: arn:aws:iam::<your-aws-account-id>:role/synthetiq-infra-apply
          organization-id: <your-org-id>
        secrets: inherit   # forwards the SYNTHETIQ_NPM_KEY repo secret
    ```
  </Step>
</Steps>

## Nested directory layout

By default the infra root — `package.json`, `.npmrc`, and `_infra/` — sits at the repository root. To keep it in a subdirectory instead (for example, inside a shared infrastructure monorepo), move those files together into that directory and point the workflow at it with `working-directory`:

```
your-repo/
└── partner/
    └── synthetiq/          # the infra root
        ├── package.json
        ├── package-lock.json
        ├── .npmrc
        └── _infra/
            └── synthetiq.yaml
```

They move as a unit: the CLI installs from the `package.json` next to `_infra/`, and it discovers the config by walking up from wherever it runs. Only `.github/workflows/synthetiq-infra.yml` stays at the repository root — GitHub requires workflow files there.

**Point the workflow at the infra root** — add `working-directory` to the `with:` block:

```yaml theme={null}
jobs:
  infra:
    uses: SynthetiqAI/infra-workflow/.github/workflows/plan-apply.yml@v1
    with:
      plan-role-arn: arn:aws:iam::<your-aws-account-id>:role/synthetiq-infra-plan
      apply-role-arn: arn:aws:iam::<your-aws-account-id>:role/synthetiq-infra-apply
      organization-id: <your-org-id>
      working-directory: partner/synthetiq
    secrets: inherit
```

**Prefix the trigger paths** with the same directory:

```yaml theme={null}
on:
  pull_request:
    paths:
      - "partner/synthetiq/_infra/**"
      - "partner/synthetiq/package.json"
      - "partner/synthetiq/package-lock.json"
  push:
    branches: [main]
    paths:
      - "partner/synthetiq/_infra/**"
      - "partner/synthetiq/package.json"
      - "partner/synthetiq/package-lock.json"
```

<Warning>
  GitHub Actions path filters are static — they can't read the `working-directory` value, so you must prefix them by hand. If they don't match the nested path, editing your config won't trigger the plan or apply job, and the workflow will silently never run.
</Warning>

Nothing else changes: `synthetiq.yaml`, the AWS roles, and the Synthetiq service account are all unaffected. Run local CLI commands (`npm install`, `synthetiq infra init`) from inside the infra root — the CLI finds `_infra/` from any directory at or below it.

## The flow on every change

1. Edit `_infra/synthetiq.yaml` in a branch; open a PR.
2. The **plan job** runs `synthetiq infra generate`: change sets are parked in your AWS account, the rendered CloudFormation template and changeset are committed to the branch, and a single PR comment (updated on later pushes) summarizes the impact — change counts by area and any replacements. The full template diff is in the PR's Files changed.
3. Review and merge through your normal branch-protection process.
4. The **apply job** runs `synthetiq infra provision`, executing exactly the parked change set. If the stack moved since the plan, CloudFormation refuses it and the job fails asking for a re-plan. On the first provision, the job log ends with the three app [DNS records](/docs/platform-docs/deployments/byoi/dns) to create.
5. Re-running with no config change is a green no-op — schedule the workflow for a free drift check.

<Note>
  For a second gate after merge, put the apply job behind a [GitHub environment](https://docs.github.com/en/actions/deployment/targeting-different-environments/using-environments-for-deployment) with required reviewers.
</Note>

## Staying current

Synthetiq publishes infrastructure updates as new CLI versions (all packages share one version number); an update can mean a diff to your stacks. An upgrade is just another pull request:

1. **Pin `@synthetiq/cli`** in the infra repo (`package.json` + lockfile, installed by both jobs). Never `@latest` — a floating version injects surprise diffs into unrelated PRs and lets the plan and apply jobs run different versions.
2. **Upgrades arrive as version-bump PRs** — point Renovate or Dependabot at the pin. Because `package.json`/`package-lock.json` are in the workflow's trigger paths, the bump alone runs the plan job on the new version, and the PR comment shows the exact diff that release causes in your account.
3. **Review and merge** like any other change; security patches are just fast merges.
4. Same config + same version is a green no-op, so scheduled runs only go red for genuine drift.

Two guards backstop this: CloudFormation refuses a parked change set if the stack moved since planning, and `provision` refuses a changeset generated by a different CLI version (`--allow-version-skew` overrides).

## Security model

* **No stored credentials** — AWS and Synthetiq access are per-run OIDC exchanges; tokens expire in minutes.
* **Immediate revocation** — delete the trust or demote the service account's role and the next exchange fails.
* **The AWS side is yours** — your OIDC provider, roles, and trust policies; Synthetiq only specifies the policy contents.
* **Fork PRs are inert** — GitHub withholds OIDC tokens from fork pull requests.
* **Audit trail** — tokens minted through a trust carry the trust, issuer, and subject in their claims, so every CI action is attributable to the repository and ref that performed it.
