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
| Call | Purpose |
|---|---|
ci.context | Read where the script is running: provider, event, commit, branch, repository, run, and pull request. |
ci.base | Read the base that affected detection compares against. |
ci.summary | Append Markdown to the job summary. |
ci.output | Set a step output for later steps and jobs. |
ci.env | Export an environment variable to later steps. |
ci.path | Prepend a directory to PATH for later steps. |
ci.mask | Redact a value from everything printed afterwards, including runner logs. |
ci.annotate | Attach an error, warning, or notice to a file and line in the pull request diff. |
ci.comment | Create or update a comment on the pull request, or on the commit when there is no pull request. |
ci.check | Report a named status on the commit, and update it as the work progresses. |
ci.group | Wrap a function's output in a collapsible log group. |
ci.sarif | Upload 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. localA boolean that is
Truewhen no CI provider is detected and thegenericprovider is rendering locally. It isFalsewhen CI mode is forced with--ciorATMOS_CI=true, because thegenericprovider 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/nameform, 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.
elevatedA boolean that is
Trueunder the elevatedpull_request_targetandworkflow_runevents. Elevated events run with a privileged token. See the fork gate.prA struct with
number,head,base,url, andforkfor the current pull request, orNonewhen the run is not for a pull request.forkis a boolean that isTruewhen the pull request head lives in a fork of the base repository. Under the elevatedpull_request_targetevent,numbercomes from the event payload. Underworkflow_run,prisNonewhen the triggering run was not for a pull request, and the struct is present for a pull request that the payload names.numberis0only when the event payload does not carry the number, so useci.commentwith the defaulttarget = "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.
| Call | Flag |
|---|---|
ci.summary | ci.summary.enabled |
ci.output, ci.env, ci.path | ci.output.enabled |
ci.annotate | ci.annotations.enabled |
ci.sarif | ci.results.enabled |
ci.check | ci.checks.enabled |
ci.comment | ci.comments.enabled |
ci.group | ci.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:
| Call | Permission |
|---|---|
ci.comment on a pull request | pull-requests: write |
ci.comment on a commit | contents: write |
ci.check | statuses: write |
ci.sarif | security-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=valueon stderr, environment exports asexport NAME=value(the value is shell-quoted only when it needs quoting), and path additions asexport PATH=dir:"$PATH". - Annotations print as
path:line: level: message. - Comments are previewed and never posted, the returned
urlis empty, and the returnedidis 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.
Related
atmos.ciruns theatmos cicommand group as a subprocess.- CI configuration lists every key that gates these calls.
- Atmos Automation Language and the script step.