Skip to main content

Failed Commands and Policy Denials as CI Annotations

· 3 min read
Erik Osterman
Founder @ Cloud Posse

A CI job fails, and the run page says only "Process completed with exit code 1." To learn why, you open the job, scroll through the log, and hunt for the one error that matters. When the failure is a policy denial, the rule that blocked the run and the reason its author wrote are in that log too, out of sight of the person reviewing the pull request. Atmos now reports a failed command as a CI annotation, so the reason appears on the run page.

The Problem​

A failed job hides its cause. CI providers show a step's exit status at a glance, but the explanation stays in the log. That costs a reviewer a click and a search on every failure. For policy denials it is worse: the policy author's guidance, such as "EIPs cannot be destroyed; use prevent_destroy instead," is only useful if someone reads it.

The Fix​

When a command fails in CI, Atmos reports the failure through the CI provider's annotation support:

  • Policy denials produce one annotation per rule that denied the run. The title names the rule, and the message is the rule's reason as plain text, or "Denied by <rule>." when the rule has none.
  • Any other failure produces one annotation from the error's title, message, and explanation.
  • Location: when Atmos knows the file and line where the cause is defined, the annotation is anchored there.
  • Scripts: a nested Atmos command that fails inside a script reports its own failure once. The script that called it does not repeat it.

On GitHub Actions, these are ::error workflow commands, which appear in the run's annotations:

::error title=Policy protect-prod/guard denied execution::Production components cannot be changed from this workstation.

How to Use It​

Failure annotations follow the same switches as other CI annotations:

ci:
enabled: true
annotations:
enabled: true # default when ci.enabled is true

A command policy is evaluated before atmos.yaml loads. A denial at that point is annotated whenever a CI provider is detected, because an annotation is the only CI signal it can produce.

Try it out​

Copy the example into a new directory and make it your own:

atmos init examples/policies
cd policies

Only Atmos is required. Preview a failure annotation locally by running an intentionally denied command:

ATMOS_POLICY_DIR=./policy.d GITHUB_ACTIONS=true atmos terraform destroy demo -s local

ATMOS_POLICY_DIR selects the example's policy. GITHUB_ACTIONS=true enables GitHub provider detection for this command, so Atmos emits the following annotation on stderr alongside the normal error output:

::error title=Policy local-safety/deny-destroy denied execution::Destructive Terraform operations are disabled in this example. Use a reviewed teardown process instead.

The terminal displays this raw workflow-command text. In GitHub Actions, the runner renders it as an annotation. The command intentionally exits with code 77: the admission policy blocks it before Terraform runs or any infrastructure is accessed. No Terraform installation or cloud credentials are needed. This denial occurs before atmos.yaml loads, so the preview does not require ci.enabled configuration.

Get Involved​

Enable CI annotations in one workflow, trigger a policy denial, and share feedback in GitHub Discussions.