Skip to main content

ci

The ci module lets a script report into the CI provider that is running it: job summaries, step outputs, environment exports, annotations, pull request comments, commit statuses, log groups, and SARIF uploads. When no provider is detected, the same calls render on your terminal, so one script runs unchanged on a laptop and in CI.

The ci module is predeclared in every Atmos Automation Language script: standalone scripts and the script steps of custom commands, workflows, and hooks alike. It is not the same as atmos.ci, which runs the atmos ci command group as a subprocess.

Calls​

CallPurpose
ci.contextRead where the script is running: provider, event, commit, branch, repository, run, and pull request.
ci.baseRead the base that affected detection compares against.
ci.summaryAppend Markdown to the job summary.
ci.outputSet a step output for later steps and jobs.
ci.envExport an environment variable to later steps.
ci.pathPrepend a directory to PATH for later steps.
ci.maskRedact a value from everything printed afterwards, including runner logs.
ci.annotateAttach an error, warning, or notice to a file and line in the pull request diff.
ci.commentCreate or update a comment on the pull request, or on the commit when there is no pull request.
ci.checkReport a named status on the commit, and update it as the work progresses.
ci.groupWrap a function's output in a collapsible log group.
ci.sarifUpload a SARIF file to Code Scanning.

The context​

Reading ci.context returns a read-only struct that describes where the script is running. It resolves the first time you read it, so scripts that never read it pay nothing.

provider
The detected CI provider as a string, such as "github-actions", or "generic" when none is detected.
local

A boolean that is True when no CI provider is detected and the generic provider is rendering locally. It is False when CI mode is forced with --ci or ATMOS_CI=true, because the generic provider then stands in as the detected provider.

event
The event that triggered the run, such as "push" or "pull_request", as a string.
sha
The commit being built, as a string.
branch
The branch being built, as a string.
repo
The repository in owner/name form, as a string.
actor
The user or bot that triggered the run, as a string.
run_id
The identifier of the run, as a string.
run_url
A link to the run, as a string.
elevated

A boolean that is True under the elevated pull_request_target and workflow_run events. Elevated events run with a privileged token. See the fork gate.

pr

A struct with number, head, base, url, and fork for the current pull request, or None when the run is not for a pull request. fork is a boolean that is True when the pull request head lives in a fork of the base repository. Under the elevated pull_request_target event, number comes from the event payload. Under workflow_run, pr is None when the triggering run was not for a pull request, and the struct is present for a pull request that the payload names. number is 0 only when the event payload does not carry the number, so use ci.comment with the default target = "auto" rather than testing the number.

ctx = ci.context
if ctx.local:
ui.info("Running locally; CI reports are previews only")
elif ctx.pr != None and ctx.pr.number > 0:
ui.info("Reporting on pull request #" + str(ctx.pr.number))
else:
ui.info("No pull request; comments go to commit " + ctx.sha[:7])

Gating​

Inside CI, every write honors the same switches as the native reporting that Atmos performs for Terraform commands: ci.enabled plus a flag for each feature.

CallFlag
ci.summaryci.summary.enabled
ci.output, ci.env, ci.pathci.output.enabled
ci.annotateci.annotations.enabled
ci.sarifci.results.enabled
ci.checkci.checks.enabled
ci.commentci.comments.enabled
ci.groupci.groups.mode

Comments and checks are opt-in and stay off until you set enabled: true. Summaries, outputs, and annotations are on by default once ci.enabled is true.

A call whose flag is off does not fail. It warns, names the switch that is off, renders locally, and the script continues:

ci.comment: ci.comments.enabled is off; rendered locally

When the ci.enabled master switch is off, every call names ci.enabled instead of its own flag, because turning on a per-feature flag changes nothing until the master switch is on:

ci.summary: ci.enabled is off; rendered locally

ci.enabled is never turned on for you inside a CI run. Set it in atmos.yaml. Locally the flags are irrelevant and there is no warning. See CI configuration for the full list of keys.

Fork gate​

Under pull_request_target and workflow_run, comments, checks, ci.env, ci.path, and ci.sarif are held only when the pull request comes from a fork (ci.context.pr.fork), unless ci.allow_unsafe_fork_execution (or ATMOS_ALLOW_UNSAFE_FORK_EXECUTION) is set. A pull request whose head repository was deleted, or whose event payload cannot be read, is treated as a fork. Writing the environment or PATH files from a fork pull request under an elevated event is a known injection vector, which is why those calls are held too. A held call warns and renders locally instead. ci.summary, ci.output, ci.annotate, and ci.mask only affect the current job and are not held. A pull request from the same repository posts normally, even under these events. Read ci.context.elevated and ci.context.pr.fork to find out whether a script is in that situation, and skip work that only makes sense when posting is allowed.

Masking​

Summaries, comments, and check descriptions are masked by Atmos before they leave the process. Step outputs, environment exports, and SARIF bodies are written exactly as given, because later steps consume them. To keep a value out of logs, register it with ci.mask before you print or export it. The runner's own mask command is emitted only when ci.enabled is true.

Errors​

A real provider failure fails the script with the usual Starlark error presentation. Examples are an API error, a missing token, or a pull request number that does not exist. Gating never fails a script.

Permissions​

On GitHub Actions, the job token needs these permissions for the calls that write to GitHub APIs:

CallPermission
ci.comment on a pull requestpull-requests: write
ci.comment on a commitcontents: write
ci.checkstatuses: write
ci.sarifsecurity-events: write

Local parity​

Without a detected CI provider, the generic provider renders every call on your terminal, so you can develop and debug a reporting script before it ever runs in CI.

  • Summaries render as terminal Markdown on stderr.
  • Outputs print as name=value on stderr, environment exports as export NAME=value (the value is shell-quoted only when it needs quoting), and path additions as export PATH=dir:"$PATH".
  • Annotations print as path:line: level: message.
  • Comments are previewed and never posted, the returned url is empty, and the returned id is a synthetic number that starts at 1.

To capture the values in files instead, set the ATMOS_CI_OUTPUT, ATMOS_CI_SUMMARY, ATMOS_CI_ENV, or ATMOS_CI_PATH environment variable. The generic provider appends to each file that is set. The files are read each time a call runs, so a step's env: block can set them.

On a CI platform that Atmos does not recognize, pass the --ci flag or set ATMOS_CI=true to force CI mode. Forced CI mode makes the generic provider the detected provider, so the ci.* switches apply as they do on a real platform, ci.context.local is False, and the ATMOS_CI_* files are written. Without forced CI mode, ci.context.local stays True on any platform Atmos does not recognize.

Example​

This script builds a service, tests it, and reports the result to the pull request, the job summary, and the commit:

ctx = ci.context
build = ci.check("build", state = "in_progress", description = "Building")

def run_tests():
return exec.run(["go", "test", "./...", "-json"], check = False)

result = ci.group("Test", run_tests)

if result.exit_code != 0:
ci.annotate("error", "Tests failed", file = "main.go", line = 1, title = "go test")
build.update("failure", description = "Tests failed")
ci.summary("## Test results\n\nTests failed with exit code " + str(result.exit_code) + ".")
fail("tests failed")

ci.output("artifact", "dist/app.tar.gz")
build.update("success", description = "Built and tested " + ctx.sha[:7])

ci.comment("Built **" + ctx.sha[:7] + "** and all tests passed.", key = "build-report")

The final comment needs no guard. With the default target = "auto", it goes to the pull request when the run has one and to a comment on the commit otherwise, so a push run reports too. See ci.comment.

On a laptop the same script prints the group heading, the status, and the comment preview on stderr.