# ci.comment

The `ci.comment` function posts a comment on the pull request, or on the commit when the run has no pull request.
Give the comment a key and later runs update the same comment instead of adding a new one each time. Locally, the
comment is previewed on stderr and never posted.

## Usage

```python
ci.comment(body = "", key = "", behavior = "upsert", pr = 0, target = "auto", template = "", data = None)
```

## Arguments

- **`body`**

  The comment text, as Markdown in a string. Required unless `template` is given, and it cannot be combined with
  `template` or `data`, even when it is an empty string.
- **`key`**

  (Optional) A stable name for the comment. The comment carries a hidden marker built from the key, which later
  runs use to find it. Without a key, the call always creates a new comment.
- **`behavior`**

  (Optional) What to do when a comment with the key already exists. The value `"upsert"` (the default) updates it
  and creates one when none exists, `"create"` always adds a new comment, and `"update"` only changes a comment
  that already exists. The `"update"` behavior needs a `key`, because the key is how the comment is found. In
  CI, `"update"` fails when no comment with the key exists yet, and the local preview fails the same way.
- **`pr`**
  (Optional) The pull request number to comment on, as an integer. It defaults to the current pull request.
- **`target`**

  (Optional) Where the comment goes, as a string. The value `"auto"` (the default) posts to the pull request when
  one is known and to a comment on the commit (`ci.context.sha`) otherwise. The value `"pr"` always posts to the
  pull request and fails when none is known. The value `"commit"` always posts a comment on the commit, even
  when the run has a pull request. Any other value fails with an argument error.
- **`template`**

  (Optional) The name of a [template file](#templates) to render into the comment body, instead of passing
  `body`. It is a file name under [`ci.templates.base_path`](/cli/configuration/ci/templates), or an absolute
  path. There is no default template, so `data` always needs a `template`.
- **`data`**

  (Optional) A dict that becomes the template context. Keys are read in the template as `.key`. Passing `data`
  without `template` fails with an argument error that says `template=` is required.

## Templates

A script can keep the layout of a comment in a template file and pass only the values:

- A call with `template=` renders that file, with `data` as the context. A call with only `body` posts the text as
  written.
- There is no configured default template. Every call that renders a template names it with `template=`.
- The name is resolved against [`ci.templates.base_path`](/cli/configuration/ci/templates). A relative `base_path`
  resolves against the Atmos `base_path`. When no file has exactly that name, the name with a `.md` suffix is tried.
- Templates use Go template syntax with the same functions as the built-in plan templates. The `data` dict is the
  template context.
- A key that the template reads but `data` does not contain fails the render and names the key, so a typo does not
  post `<no value>`.
- The `key`, `behavior`, `pr`, and `target` arguments work the same with a rendered body.

See [script report templates](/cli/configuration/ci/templates#script-report-templates) for the configuration.

## Returns

A struct that describes the comment:

- **`id`**
  The identifier of the comment. A comment that was only previewed locally has a synthetic id that starts at 1.
- **`url`**
  A link to the comment, as a string. It is empty when the comment was only previewed locally.
- **`created`**
  A boolean that is 
  `True`
   when the call created a new comment and 
  `False`
   when it updated one.
- **`target`**
  The place the comment landed, as a string: 
  `"pr"`
   or 
  `"commit"`
  . With 
  `target = "auto"`
   it tells you which one was chosen.

## Behavior

- In CI, the comment is posted through the provider's API. A pull request comment needs `pull-requests: write` on
  GitHub Actions, and a commit comment needs `contents: write`.
- A commit comment is attached to `ci.context.sha`. It appears on the commit page and, for a commit that belongs to an
  open pull request, in that pull request's timeline. Keys work the same way, so a keyed commit comment is updated on
  the next run for the same commit.
- A run with no pull request, such as a `push`, comments on the commit with the default `target = "auto"`. Under
  `workflow_run` and `pull_request_target`, the pull request number comes from the event payload, so `"auto"` still
  finds the pull request.
- Locally, the comment is previewed on stderr and never posted. The returned `url` is empty.
- The body is masked before it leaves the process.
- Comments are opt-in. In CI the call is gated by [`ci.comments.enabled`](/cli/configuration/ci/comments), which is
  off by default. When it is off, the call warns, renders locally, and the script continues.
- Under `pull_request_target` and `workflow_run`, the comment is held only when the pull request comes from a fork
  (`ci.context.pr.fork`), unless `ci.allow_unsafe_fork_execution` is `true`. A held call warns and renders locally
  instead. See the [fork gate](/functions/automation/ci#fork-gate).

## Examples

### Keep one comment up to date

```python
ci.comment("### Plan\n\n3 to add, 1 to change, 0 to destroy.", key = "plan")
```

Every run updates the same comment, so the pull request never collects a stack of stale ones.

### Comment on a push

```python
ci.comment("Deployed **" + ci.context.sha[:7] + "** to prod.", key = "deploy")
```

This needs no guard. On a pull request run it comments on the pull request. On a `push` run it comments on the commit.

### Require a pull request

```python
ci.comment("Preview ready.", key = "preview", target = "pr")
```

The call fails when the run has no pull request, which is useful when a missing pull request is a bug in the workflow.

### Post to a specific pull request

```python
result = ci.comment("Released in v1.4.0.", pr = 214)
ui.info("Posted " + result.url)
```

### Always add a new comment

```python
ci.comment("Rebased onto main.", behavior = "create")
```

### Render a comment from a template

```text title=".atmos/ci/templates/release.md"
### Release {{ .version }}

Deployed to {{ .stack }}.
```

```python
ci.comment(template = "release.md", data = {"version": "1.4.0", "stack": "prod"}, key = "release")
```

## Errors

- An API error, a missing token, or a pull request number that does not exist fails the script. See
  [errors](/functions/automation/ci#errors).
- An unknown `behavior` or `target` fails with an argument error that names `ci.comment`.
- Passing `body` together with `template` or `data` (an empty `body` counts), a `data` value that is not a dict, or
  `data` without `template` fails with an argument error.
- `target = "pr"` with no pull request fails with a pull-request-unknown error.
- A template that does not exist fails the script with an error that names the resolved path and the value of
  `ci.templates.base_path`. A template that reads a key missing from `data`, or that fails to render, fails the script.

## Related

- [`ci`](/functions/automation/ci) describes the module and the shared behavior rules.
- [CI pull request comments](/cli/configuration/ci/comments) configures the native comments that Atmos posts for commands.
- [`ci.summary`](/functions/automation/ci.summary) writes to the job summary, which needs no pull request.
