diff --git a/.mintlify/workflows/update-changelog.md b/.mintlify/workflows/update-changelog.md
index ea7c1ed..0b67f62 100644
--- a/.mintlify/workflows/update-changelog.md
+++ b/.mintlify/workflows/update-changelog.md
@@ -5,6 +5,7 @@ on:
context:
- repo: "kosli-dev/cli"
- repo: "kosli-dev/terraform-provider-kosli"
+ - repo: "kosli-dev/setup-cli-action"
- repo: "kosli-dev/server"
notify:
slack:
@@ -17,6 +18,7 @@ notify:
Check each repository for **new tags** published since the last changelog entry for that product in changelog/index.mdx. Only document changes that are part of a tagged release — do not include unreleased work on `main`.
- **kosli-dev/cli** and **kosli-dev/terraform-provider-kosli** use semver GitHub Releases (e.g., `v2.17.5`, `v0.6.3`). Check the Releases list for new versions.
+- **kosli-dev/setup-cli-action** uses semver GitHub Releases (e.g., `v5.2.1`) with moving major/minor tags (`v5`, `v5.2`). Check the Releases list for new versions. **Most releases of this action are Dependabot or internal chore bumps** — only write a changelog entry when a release changes something user-facing: the action's inputs or outputs, its version-selection behavior, the supported runners, a breaking change (e.g., a changed default), or a bumped default Kosli CLI version worth calling out. Skip releases that are purely dependency bumps or internal chores. If several releases have accumulated since the last entry, consolidate their user-facing changes into a single entry keyed to the newest release.
- **kosli-dev/server** does **not** use GitHub Releases. It uses timestamp-based git tags (e.g., `release-2026-04-30-10-56-05`). You must check git **tags** (not Releases) and look at the commits between the last covered tag and the most recent tag to identify user-facing changes. Consolidate all server changes since the last Platform changelog entry into a single entry.
For each new release found, write a changelog entry in changelog/index.mdx. If no new tags exist for a repository since its last changelog entry, skip it. If there are no new tags across any repository, do not open a PR.
@@ -27,6 +29,7 @@ Label should be the date the workflow runs, like "March 16, 2026". Description s
Tags should be the product(s) affected by the release:
- kosli-dev/cli → `["CLI"]`
- kosli-dev/terraform-provider-kosli → `["Terraform Provider"]`
+- kosli-dev/setup-cli-action → `["GitHub Action"]`
- kosli-dev/server → `["Platform"]`
The changelog is about changes to the product, not changes to the docs.
diff --git a/.mintlify/workflows/update-github-action-reference.md b/.mintlify/workflows/update-github-action-reference.md
new file mode 100644
index 0000000..5485a8f
--- /dev/null
+++ b/.mintlify/workflows/update-github-action-reference.md
@@ -0,0 +1,38 @@
+---
+name: "Update GitHub Action reference"
+on:
+ cron: "0 9 * * 1"
+context:
+ - repo: "kosli-dev/setup-cli-action"
+notify:
+ slack:
+ channels:
+ - docs
+---
+
+# Agent Instructions
+
+Check kosli-dev/setup-cli-action for **new tags/releases** published since the last time the GitHub Action reference page was updated. The action uses semver GitHub Releases (e.g., `v5.2.1`) with moving major/minor tags (`v5`, `v5.2`). Check the Releases list for new versions. Only consider changes that are part of a published release — do not include unreleased work on `main`.
+
+If no new releases exist since the last update, do not open a PR.
+
+Keep the GitHub Action reference in `github-action-reference/setup_cli_action.md` in sync with the action's [`README.md`](https://github.com/kosli-dev/setup-cli-action/blob/main/README.md) and [`action.yml`](https://github.com/kosli-dev/setup-cli-action/blob/main/action.yml), which are the source of truth. Update the page when any of the following change:
+
+1. **Inputs** — an input is added, removed, renamed, or its accepted values or default change (e.g., `version`, `github-token`).
+2. **Outputs** — an output is added, removed, or its meaning changes (e.g., `version`).
+3. **Version selection behavior** — how `latest`, full semver, and major/minor pins resolve.
+4. **Supported runners** — the set of runners the action supports (`ubuntu-latest`, `windows-latest`, `macos-latest`).
+5. **Usage** — the recommended major version in the `uses:` examples (e.g., `@v5`) or the example workflows.
+
+Most releases of this action are Dependabot or internal chore bumps. Those do not change the documented interface — if a release does not affect inputs, outputs, behavior, supported runners, or usage, do not open a PR for it.
+
+When updating the page:
+- Follow Mintlify formatting conventions. Review the existing `github-action-reference/setup_cli_action.md` and other reference pages for style reference.
+- Use root-relative links (e.g., `/integrations/ci_cd`, `/client_reference`).
+- Keep the page a terse reference: inputs/outputs tables, version-selection rules, and usage examples.
+
+Do not modify changelog/index.mdx — that is handled by the "Update changelog" workflow.
+
+Before opening a PR, review all written content against the style rules in `styles/Kosli/`. In particular, `AmericanSpelling.yml` maps British spellings to their American equivalents — use American spelling throughout (e.g., "behavior", "customize", "organize").
+
+PR titles and commit messages must follow the conventional commits format described in CLAUDE.md. Use `docs:` as the type.
diff --git a/config/navigation.json b/config/navigation.json
index 06a34ac..4b1d934 100644
--- a/config/navigation.json
+++ b/config/navigation.json
@@ -168,7 +168,7 @@
"group": "Integrations",
"icon": "puzzle-piece",
"pages": [
- "integrations/actions",
+ "integrations/kosli_actions",
"integrations/ci_cd",
"integrations/slack",
"integrations/launchdarkly",
@@ -455,45 +455,19 @@
}
]
},
- {
- "item": "Template Reference",
- "icon": "file-code",
- "groups": [
- {
- "group": "Templates",
- "pages": [
- "template-reference/flow_template"
- ]
- }
- ]
- },
- {
- "item": "Policy Reference",
- "icon": "scroll",
- "groups": [
- {
- "group": "Policies",
- "pages": [
- "policy-reference/environment_policy",
- "policy-reference/policy_builder",
- "policy-reference/rego_policy"
- ]
- }
- ]
- },
{
"item": "API Reference",
"icon": "code",
"openapi": "https://app.kosli.com/api/v2/openapi.json"
},
{
- "item": "Helm Reference",
- "icon": "layer-group",
+ "item": "GitHub Action Reference",
+ "icon": "github",
"groups": [
{
- "group": "Helm Charts",
+ "group": "GitHub Action",
"pages": [
- "helm/k8s_reporter"
+ "github-action-reference/setup_cli_action"
]
}
]
@@ -532,6 +506,44 @@
]
}
]
+ },
+ {
+ "item": "Helm Reference",
+ "icon": "layer-group",
+ "groups": [
+ {
+ "group": "Helm Charts",
+ "pages": [
+ "helm/k8s_reporter"
+ ]
+ }
+ ]
+ },
+ {
+ "item": "Template Reference",
+ "icon": "file-code",
+ "groups": [
+ {
+ "group": "Templates",
+ "pages": [
+ "template-reference/flow_template"
+ ]
+ }
+ ]
+ },
+ {
+ "item": "Policy Reference",
+ "icon": "scroll",
+ "groups": [
+ {
+ "group": "Policies",
+ "pages": [
+ "policy-reference/environment_policy",
+ "policy-reference/policy_builder",
+ "policy-reference/rego_policy"
+ ]
+ }
+ ]
}
]
},
diff --git a/config/redirects.json b/config/redirects.json
index a04693a..4518025 100644
--- a/config/redirects.json
+++ b/config/redirects.json
@@ -7,6 +7,10 @@
"source": "/tutorials/terraform_drift_detection",
"destination": "/tutorials/detecting_non_terraform_changes"
},
+ {
+ "source": "/integrations/actions",
+ "destination": "/integrations/kosli_actions"
+ },
{
"source": "/getting_started/approvals",
"destination": "/getting_started/attestations"
diff --git a/github-action-reference/setup_cli_action.md b/github-action-reference/setup_cli_action.md
new file mode 100644
index 0000000..ef36e65
--- /dev/null
+++ b/github-action-reference/setup_cli_action.md
@@ -0,0 +1,120 @@
+---
+title: GitHub Action
+description: Reference for the setup-kosli-cli GitHub Action that installs the Kosli CLI on GitHub Actions runners.
+icon: github
+---
+
+The [`kosli-dev/setup-cli-action`](https://github.com/kosli-dev/setup-cli-action) GitHub Action (`setup-kosli-cli`) installs the [Kosli CLI](/client_reference) on GitHub Actions runners. After the action runs, every CLI command is available in later steps of the job.
+
+The action runs on `ubuntu-latest`, `windows-latest`, and `macos-latest` runners.
+
+
+This page documents the action itself. For a broader guide to using Kosli in GitHub Actions, including the command flags that are defaulted from GitHub CI variables, see [CI/CD](/integrations/ci_cd).
+
+
+## Usage
+
+Install the latest release of the Kosli CLI:
+
+```yaml
+steps:
+ - uses: kosli-dev/setup-cli-action@v5
+```
+
+Install a specific version:
+
+```yaml
+steps:
+ - name: Setup Kosli CLI
+ uses: kosli-dev/setup-cli-action@v5
+ with:
+ version: 2.11.43
+```
+
+## Inputs
+
+| Input | Required | Default | Description |
+| :--- | :--- | :--- | :--- |
+| `version` | No | `latest` | Version of the Kosli CLI to install. See [Version selection](#version-selection). |
+| `github-token` | No | `${{ github.token }}` | Token used to authenticate the GitHub API calls that resolve `latest` or a major/minor pin. You normally do not need to set this. |
+
+## Outputs
+
+| Output | Description |
+| :--- | :--- |
+| `version` | The resolved Kosli CLI version that was installed. When `version` is `latest` or a major/minor pin, this is the concrete semver that was selected (e.g. `2.12.0`). |
+
+Reference the resolved version in later steps:
+
+```yaml
+steps:
+ - name: Setup Kosli CLI
+ id: setup
+ uses: kosli-dev/setup-cli-action@v5
+
+ - name: Print installed version
+ run: echo "Installed Kosli CLI ${{ steps.setup.outputs.version }}"
+```
+
+## Version selection
+
+The `version` input accepts:
+
+- **A full semver**, e.g. `2.11.43` — installed as-is.
+- **A major pin**, e.g. `"2"` — resolves to the newest stable `2.x` release, and never `3.0.0`.
+- **A major.minor pin**, e.g. `"2.11"` — resolves to the newest stable `2.11.z` patch.
+- **`latest`** — resolves to the newest stable release of [`kosli-dev/cli`](https://github.com/kosli-dev/cli). This is the default.
+
+Major and minor pins resolve at runtime and never select a pre-release or a higher major.
+
+
+Quote partial versions. In YAML, `version: 2.10` is parsed as the number `2.1`, which is not what you mean. Always quote a major or minor pin: `version: "2"`, `version: "2.10"`.
+
+
+Track a major version and pick up every update within it without ever jumping to the next (breaking) major:
+
+```yaml
+steps:
+ - name: Setup Kosli CLI
+ uses: kosli-dev/setup-cli-action@v5
+ with:
+ version: "2" # newest stable 2.x, never 3.x
+```
+
+## Example job
+
+Secrets in GitHub Actions are not automatically exported as environment variables, so set the API token explicitly. All CLI flags can be set as environment variables by adding the `KOSLI_` prefix and capitalizing them.
+
+```yaml
+jobs:
+ build-image:
+ runs-on: ubuntu-latest
+ env:
+ KOSLI_API_TOKEN: ${{ secrets.KOSLI_API_TOKEN }}
+ KOSLI_ORG: my-org
+ KOSLI_FLOW: my-flow
+ KOSLI_TRAIL: ${{ github.sha }}
+ IMAGE_NAME: my-registry/my-image:latest
+ steps:
+ - name: Build and push Docker image
+ id: build
+ uses: docker/build-push-action@v5
+ with:
+ push: true
+ tags: ${{ env.IMAGE_NAME }}
+
+ - name: Setup Kosli CLI
+ uses: kosli-dev/setup-cli-action@v5
+
+ - name: Attest image provenance
+ run: kosli attest artifact "${IMAGE_NAME}" --artifact-type=oci
+```
+
+For a complete example of a GitHub workflow using Kosli, see the Kosli CLI's [own workflow](https://github.com/kosli-dev/cli/blob/main/.github/workflows/docker.yml).
+
+## References
+
+- Action source: [`kosli-dev/setup-cli-action`](https://github.com/kosli-dev/setup-cli-action)
+- Marketplace listing: [setup-kosli-cli](https://github.com/marketplace/actions/setup-kosli-cli)
+- [CI/CD integration guide](/integrations/ci_cd)
+- [Kosli CLI reference](/client_reference)
diff --git a/integrations/ci_cd.md b/integrations/ci_cd.md
index 67e5a6f..adb1b44 100644
--- a/integrations/ci_cd.md
+++ b/integrations/ci_cd.md
@@ -34,8 +34,8 @@ description: Use Kosli in CI Systems like GitHub Actions, GitLab CI, and more.
## Use Kosli in Github Actions
- To use Kosli in [Github Actions](https://docs.github.com/en/actions) workflows, you can use the kosli [CLI setup action](https://github.com/marketplace/actions/setup-kosli-cli) to install the CLI on your Github Actions Runner.
- Then, you can use all the [CLI commands](/client_reference) in your workflows.
+ To use Kosli in [Github Actions](https://docs.github.com/en/actions) workflows, you can use the [`setup-kosli-cli` GitHub Action](/github-action-reference/setup_cli_action) to install the CLI on your Github Actions Runner.
+ Then, you can use all the [CLI commands](/client_reference) in your workflows. See the GitHub Action reference for its inputs, outputs, and version-pinning options.
### GitHub Secrets
diff --git a/integrations/actions.md b/integrations/kosli_actions.md
similarity index 98%
rename from integrations/actions.md
rename to integrations/kosli_actions.md
index 15cc211..04a18d7 100644
--- a/integrations/actions.md
+++ b/integrations/kosli_actions.md
@@ -31,7 +31,7 @@ To receive Kosli notifications in Slack, you have two options. You can either us
- Create a [Slack incoming webhook](https://api.slack.com/messaging/webhooks#create_a_webhook).
- - Use this webhook to [create a notification settings in the Kosli UI](/integrations/actions/#manage-actions-in-the-ui).
+ - Use this webhook to [create a notification settings in the Kosli UI](/integrations/kosli_actions/#manage-actions-in-the-ui).
## Custom Webhook Notifications
diff --git a/tutorials/attest_snyk.md b/tutorials/attest_snyk.md
index abd5f54..5ba2bed 100644
--- a/tutorials/attest_snyk.md
+++ b/tutorials/attest_snyk.md
@@ -112,4 +112,4 @@ You have run four types of Snyk scans and attested each result to a Kosli trail.
From here you can:
- Explore the trail in the [Kosli app](https://app.kosli.com)
- Attest scans to an artifact in a trail — see [`kosli attest snyk`](/client_reference/kosli_attest_snyk) for details
-- Add Snyk attestations to your CI pipeline using the [GitHub Actions integration](/integrations/actions)
+- Add Snyk attestations to your CI pipeline using the [GitHub Action](/github-action-reference/setup_cli_action)
diff --git a/tutorials/unauthorized_iac_changes.md b/tutorials/unauthorized_iac_changes.md
new file mode 100644
index 0000000..691f5d8
--- /dev/null
+++ b/tutorials/unauthorized_iac_changes.md
@@ -0,0 +1,158 @@
+---
+title: "Detecting unauthorized Terraform changes"
+description: "Learn how to use Kosli to detect unauthorized Terraform infrastructure changes — changes made outside your approved CI process."
+---
+
+By the end of this tutorial, you will have set up Kosli to track authorized Terraform changes and detect when an unauthorized change slips through.
+
+
+This tutorial focuses on detecting changes made by bypassing the approved Terraform process (e.g. a developer running `terraform apply` directly from their machine). Detecting infrastructure drift is a separate concern covered by [Terraform drift detection](https://developer.hashicorp.com/terraform/tutorials/state/resource-drift).
+
+
+## Prerequisites
+
+* [Install Terraform](https://developer.hashicorp.com/terraform/install).
+* [Install Snyk CLI](https://docs.snyk.io/snyk-cli/getting-started-with-the-snyk-cli#install-the-snyk-cli-and-authenticate-your-machine) (optional — needed for the security scan step).
+* [Install Kosli CLI](/getting_started/install).
+* [Get a Kosli API token](/getting_started/authenticating_to_kosli).
+
+## Setup
+
+```shell
+export KOSLI_ORG=
+export KOSLI_API_TOKEN=
+```
+
+Clone the tutorial repository:
+
+```shell
+git clone https://github.com/kosli-dev/iac-changes-tutorial.git
+cd iac-changes-tutorial
+```
+
+## Create a Kosli flow
+
+Create a Kosli flow to represent the approved process for Terraform changes. Using --use-empty-template keeps things simple for this tutorial:
+
+```shell
+kosli create flow tf-tutorial --use-empty-template
+```
+
+## Make and track an authorized change
+
+
+In production, an authorized change goes through CI. In this tutorial, you run those commands locally to simulate the process.
+
+
+Begin a trail to represent a single authorized change:
+
+```shell
+kosli begin trail authorized-1 --flow=tf-tutorial
+```
+
+Optionally, scan your Terraform config for security issues and attest the SARIF output to Kosli:
+
+```shell
+snyk iac test main.tf --sarif-file-output=sarif.json
+kosli attest snyk --name=security --flow=tf-tutorial --trail=authorized-1 --scan-results=sarif.json
+```
+
+Create a Terraform plan, save it to a file, and attest it to Kosli:
+
+```shell
+terraform init
+terraform plan -out=tf.plan
+kosli attest generic --name=tf-plan --flow=tf-tutorial --trail=authorized-1 --attachments=tf.plan
+```
+
+Apply the plan and attest the resulting state file as an artifact. Kosli calculates a fingerprint from the state file contents — this fingerprint is how it later detects unauthorized changes:
+
+
+This tutorial uses a local state file for simplicity. In production, the state file is typically stored in cloud storage (e.g. AWS S3) and you would download it after the authorized change. Note that `--build-url` and `--commit-url` are set to placeholder URLs here — in CI these are set automatically.
+
+
+```shell
+terraform apply -auto-approve tf.plan
+kosli attest artifact terraform.tfstate --name=state-file --artifact-type=file --flow=tf-tutorial --trail=authorized-1 \
+ --build-url=https://example.com --commit-url=https://example.com --commit=HEAD
+```
+
+## Monitor the state file
+
+To detect unauthorized changes, Kosli monitors the state file for changes by tracking it in an environment. Create a `server` environment:
+
+```shell
+kosli create env terraform-state --type=server
+```
+
+Report the current state file to the environment:
+
+
+In production, configure environment reporting to run periodically or on state file changes. See [reporting AWS environments](/tutorials/report_aws_envs) if you use S3 as your Terraform backend.
+
+
+```shell
+kosli snapshot path terraform-state --name=tf-state --path=terraform.tfstate
+```
+
+Check the latest snapshot:
+
+```shell
+kosli get snapshot terraform-state
+```
+
+You should see:
+
+```plaintext
+COMMIT ARTIFACT FLOW COMPLIANCE RUNNING_SINCE REPLICAS
+d881b2f Name: tf-state tf-tutorial COMPLIANT 28 minutes ago 1
+ Fingerprint: a57667a7b921b91d438631afa1a1fe35300b4da909a19d2b61196580f30f1d0c
+```
+
+The `FLOW` column shows `tf-tutorial` — Kosli has provenance for this change. In the Kosli UI under **Environments > terraform-state**, the artifact shows as compliant.
+
+
+
+## Introduce an unauthorized change
+
+Simulate an unauthorized change by modifying line 6 of `main.tf` — change `random_pet_result` to `random_pet_name` — then apply directly without going through the approved process:
+
+```shell
+terraform apply --auto-approve
+```
+
+Report the updated state file to Kosli:
+
+
+In production this step is not needed — environment reporting runs automatically on change or on a schedule.
+
+
+```shell
+kosli snapshot path terraform-state --name=tf-state --path=terraform.tfstate
+```
+
+Check the snapshot again:
+
+```shell
+kosli get snapshot terraform-state
+```
+
+You should see:
+
+```plaintext
+COMMIT ARTIFACT FLOW COMPLIANCE RUNNING_SINCE REPLICAS
+N/A Name: tf-state N/A NON-COMPLIANT 8 minutes ago 1
+ Fingerprint: edd93dcde27718ed493222ceb218275655555f3f3bfefa95628c599e678ac325
+```
+
+The `FLOW` is now `N/A` — Kosli has no provenance for this state file fingerprint. It was not attested through any known flow, which means the change was unauthorized. The environment page reflects this:
+
+
+
+## What you've accomplished
+
+You have used Kosli to track authorized Terraform changes and detect an unauthorized one. By fingerprinting the Terraform state file and comparing it against attested artifacts, Kosli can tell whether a running infrastructure state came from an approved process or not.
+
+From here you can:
+* Set up alerts and automated responses when unauthorized changes are detected using [Kosli Actions](/integrations/kosli_actions)
+* See how to report S3-backed state files automatically in the [Report AWS environments](/tutorials/report_aws_envs) tutorial