# CI Templates

The `ci.templates` section configures custom Markdown templates for job summaries and PR comments.
Templates use Go template syntax with access to plan/apply context data.

## Configuration

**File:** `atmos.yaml`

```yaml
ci:
  templates:
    base_path: ".atmos/ci/templates"
    terraform:
      plan: "plan.md"
      apply: "apply.md"
    helm:
      template: "helm-template.md"
      diff: "helm-diff.md"
      apply: "helm-apply.md"
      delete: "helm-delete.md"
    helmfile:
      template: "helmfile-template.md"
      diff: "helmfile-diff.md"
      apply: "helmfile-apply.md"
      destroy: "helmfile-destroy.md"
    container:
      image: "image.md"
```

- **`ci.templates.base_path`**

  Directory containing custom template files. A relative path resolves against the Atmos
  [`base_path`](/cli/configuration#base-path), not against the repository root or the working directory. An
  absolute path is used as written. The `.atmos/ci/templates` value in the examples on this page is a suggested
  location, not a default.

  **Default:** None. Without it, only files named by absolute path and the built-in templates are used.
- **`ci.templates.terraform.plan`**

  Filename of the custom plan summary template within the base path.

  **Default:** Built-in template
- **`ci.templates.terraform.apply`**

  Filename of the custom apply summary template within the base path.

  **Default:** Built-in template
- **`ci.templates.helm.template`**
  Filename of the custom native Helm template/render summary template within the base path.
- **`ci.templates.helm.diff`**
  Filename of the custom native Helm diff/plan summary template within the base path.
- **`ci.templates.helm.apply`**
  Filename of the custom native Helm apply/deploy summary template within the base path.
- **`ci.templates.helm.delete`**
  Filename of the custom native Helm delete/destroy summary template within the base path.
- **`ci.templates.helmfile.template`**
  Filename of the custom Helmfile template summary template within the base path.
- **`ci.templates.helmfile.diff`**
  Filename of the custom Helmfile diff summary template within the base path.
- **`ci.templates.helmfile.apply`**
  Filename of the custom Helmfile apply/sync/deploy summary template within the base path.
- **`ci.templates.helmfile.destroy`**
  Filename of the custom Helmfile destroy summary template within the base path.
- **`ci.templates.container.image`**

  Filename of the custom container image summary template within the base path. A configured file that does not
  exist is an error, so a typo cannot silently revert to the built-in template. Without the key, Atmos uses
  `<base_path>/container/image.md` when that file exists, and the built-in template otherwise. The same
  Markdown is used for the job summary and for the per-image comment, keyed `container:image:<registry/repository>`.
  The comment targets the pull request when one is known, or the commit otherwise. GitHub commit comments require
  `contents: write` permission.

  The template receives these fields, all already escaped Markdown: `.Image`, `.Badges`, `.Description`,
  `.Tag`, `.Digest`, `.ID`, `.Revision`, `.Source`, `.Entrypoint`, `.Command`, `.StopSignal`, `.StorageDriver`,
  `.ExposedPorts`, `.Env` (rows with `.Name` and `.Value`), `.Labels` (rows with `.Name` and `.Value`),
  `.Layers` (rows with `.Index` and `.Digest`), and `.RawJSON`.

## Template Context

Templates receive a context object for the operation being summarized.

All native CI summary templates receive these common fields when available:

| Variable | Type | Description |
|----------|------|-------------|
| `.Component` | string | Component name |
| `.Stack` | string | Stack name |
| `.Command` | string | Normalized command name used for template selection |
| `.Success` | bool | Whether the operation succeeded |
| `.ExitCode` | int | Command exit code |
| `.Error` | string | Error message when the command failed |
| `.ReproductionCommand` | string | Local command to reproduce the operation |

### Terraform Plan Template Variables

| Variable | Type | Description |
|----------|------|-------------|
| `.Component` | string | Component name |
| `.Stack` | string | Stack name |
| `.Command` | string | Terraform command (`plan`) |
| `.HasChanges` | bool | Whether the plan has changes |
| `.Additions` | int | Number of resources to create |
| `.Changes` | int | Number of resources to change |
| `.Destructions` | int | Number of resources to destroy |
| `.Imports` | int | Number of resources to import |
| `.Summary` | string | One-line plan summary |
| `.Resources` | object | Resource lists by action type |
| `.Warnings` | \[]string | Terraform warning messages |

### Terraform Apply Template Variables

| Variable | Type | Description |
|----------|------|-------------|
| `.Component` | string | Component name |
| `.Stack` | string | Stack name |
| `.Command` | string | Terraform command (`apply`) |
| `.Success` | bool | Whether apply succeeded |
| `.Summary` | string | One-line apply summary |
| `.Resources` | object | Resource lists by action type |
| `.Outputs` | map | Terraform outputs |
| `.Warnings` | \[]string | Terraform warning messages |

### Helm Template Variables

Native Helm summary templates also receive Helm metadata when available:

| Variable | Type | Description |
|----------|------|-------------|
| `.ReleaseName` | string | Helm release name |
| `.Namespace` | string | Kubernetes namespace |
| `.Chart` | string | Chart reference |
| `.Target` | string | Provision target, when one was selected |
| `.ObjectCount` | int | Number of rendered or applied Kubernetes objects |
| `.Kinds` | map | Rendered or applied Kubernetes object counts by kind |
| `.ManifestBytes` | int | Rendered manifest size in bytes |
| `.Diff` | string | Unified diff produced by `diff`/`plan` (rendered as a collapsible diff block; Secret values redacted) |

`render` uses the `template` template, `plan` uses `diff`, `deploy` uses `apply`, and `destroy` uses `delete`.

### Helmfile Template Variables

Helmfile summary templates also receive captured command output:

| Variable | Type | Description |
|----------|------|-------------|
| `.Output` | string | Combined masked stdout/stderr captured from the Helmfile command |

`sync` and `deploy` use the `apply` template.

### Kubernetes Summary Template Variables

Native [Kubernetes components](/cli/commands/kubernetes/usage) render a single summary
template (default name `summary`) for every command (`render`, `plan`, `diff`, `apply`,
`deploy`, `delete`, `validate`). For `plan`/`diff`, the `.Diff` variable holds the aggregated
unified diff of all changed objects (rendered as a collapsible diff block using GitHub's `diff`
fenced code syntax). `Secret` objects are deliberately omitted from `.Diff` so their data never
reaches the job summary.

| Variable | Type | Description |
|----------|------|-------------|
| `.Component` | string | Component name |
| `.Stack` | string | Stack name |
| `.Command` | string | Kubernetes command (`plan`, `diff`, `apply`, …) |
| `.Status` | string | `failed`, `changed`, `no changes`, or `succeeded` |
| `.ObjectsTotal` | int | Number of objects processed |
| `.Counts` | \[]object | Action counts, each with `.Action` and `.Count` |
| `.Actions` | \[]object | Per-object rows: `.Action`, `.Resource`, `.Namespace`, `.Name`, `.Diff` |
| `.Diff` | string | Aggregated unified diff across changed objects (Secrets omitted, truncated) |
| `.Error` | string | Command error message, if any |

## Customizing the Kubernetes Summary

Kubernetes summaries resolve by the base-path convention rather than the `terraform`/`helmfile`
override maps. Place a file at `<base_path>/kubernetes/summary.md` to override the embedded
default, or set [`ci.summary.template`](/cli/configuration/ci/summary) to load a different
template name from `<base_path>/kubernetes/<name>.md`.

**File:** `atmos.yaml`

```yaml
ci:
  templates:
    base_path: ".atmos/ci/templates"   # kubernetes summary read from kubernetes/summary.md
  summary:
    template: "summary"                 # optional: load kubernetes/<template>.md instead
```

## Example Custom Template

**File:** `.atmos/ci/templates/plan.md`

```markdown
## {{ .Component }} / {{ .Stack }}

{{ if .HasChanges }}
**Changes detected:** {{ .Additions }} to add, {{ .Changes }} to change, {{ .Destructions }} to destroy

{{ if gt .Destructions 0 }}
> **Warning:** This plan destroys resources!
{{ end }}
{{ else }}
No changes. Infrastructure is up-to-date.
{{ end }}
```

## Script report templates

Scripts that call [`ci.summary`](/functions/automation/ci.summary) or [`ci.comment`](/functions/automation/ci.comment)
can render their text from a template file instead of building the Markdown in code. The call names the template, and
the configuration only says where template files live. Scripts have no configured default template.

**File:** `atmos.yaml`

```yaml
ci:
  templates:
    base_path: ".atmos/ci/templates"
```

```python
ci.summary(template = "deploy.md", data = {"stack": "prod", "count": 2, "services": ["api", "web"]})
ci.comment(template = "release.md", data = {"version": "1.4.0"}, key = "release")
```

A call that passes `data` must also pass `template`. A call with `data` and no `template` fails with an argument error
that asks for `template=`.

A template name resolves against `ci.templates.base_path`, so `deploy.md` above is read from
`.atmos/ci/templates/deploy.md`. An absolute path is used as written. When no file has exactly that name, the name with
a `.md` suffix is tried, so `deploy` finds `deploy.md`. The `data` dict that the script passes is the template context,
and keys are read as `.key`. A key that the template reads but `data` does not contain fails the render and names the
key, so a typo never renders `<no value>`. A template that cannot be found fails the script with the resolved path and
the value of `ci.templates.base_path`.

**File:** `.atmos/ci/templates/deploy.md`

```markdown
## Deploy to {{ .stack }}

{{ .count }} services deployed.
{{ range .services }}
- {{ . }}
{{ end }}
```

The [`ci.summary.template`](/cli/configuration/ci/summary) key belongs to the native commands. They read it as the
name of a summary template for the component type (for example `<base_path>/kubernetes/summary.md`), and scripts never
read it, so setting it cannot change what a script renders. There is no comment counterpart: native plan comments
reuse the rendered plan summary, and scripts name their comment template in the call.

## Built-in Templates

The default templates are embedded in the Atmos binary:

- Terraform: `pkg/ci/plugins/terraform/templates/`
- Helm: `pkg/ci/plugins/helm/templates/`
- Helmfile: `pkg/ci/plugins/helmfile/templates/`
- Kubernetes: `pkg/ci/plugins/kubernetes/templates/`

You can use these as a starting point for customization.

## Related

- [CI Configuration](/cli/configuration/ci) - Full configuration reference
- [Summary](/cli/configuration/ci/summary) - Job summary configuration
- [Comments](/cli/configuration/ci/comments) - PR comment configuration
- [`ci.summary`](/functions/automation/ci.summary) and [`ci.comment`](/functions/automation/ci.comment) - Script functions that render these templates
- [Job Summaries](/ci/job-summaries) - Summary format and template customization
