From f3ecf6bf06d4107367b663d6db3bdcb3a1808a21 Mon Sep 17 00:00:00 2001 From: Lewis Christie Date: Wed, 17 Jun 2026 16:57:03 -0600 Subject: [PATCH 1/7] Add hooks documentation --- .../pipelines/guides/hooks/authentication.md | 110 ++++++++++++++ .../pipelines/guides/hooks/configuring.md | 95 ++++++++++++ .../docs/pipelines/guides/hooks/overview.md | 34 +++++ docs/2.0/docs/pipelines/guides/hooks/setup.md | 64 ++++++++ .../guides/hooks/slack-deploy-notification.md | 114 ++++++++++++++ .../pipelines/guides/hooks/writing-a-hook.md | 141 ++++++++++++++++++ .../pipelines/configurations-as-code/api.mdx | 100 ++++++++++++- docs/2.0/reference/pipelines/hooks-api.md | 100 +++++++++++++ sidebars/docs.js | 41 +++++ sidebars/reference.js | 5 + .../guides/affected-units-comment.png | Bin 0 -> 71288 bytes 11 files changed, 803 insertions(+), 1 deletion(-) create mode 100644 docs/2.0/docs/pipelines/guides/hooks/authentication.md create mode 100644 docs/2.0/docs/pipelines/guides/hooks/configuring.md create mode 100644 docs/2.0/docs/pipelines/guides/hooks/overview.md create mode 100644 docs/2.0/docs/pipelines/guides/hooks/setup.md create mode 100644 docs/2.0/docs/pipelines/guides/hooks/slack-deploy-notification.md create mode 100644 docs/2.0/docs/pipelines/guides/hooks/writing-a-hook.md create mode 100644 docs/2.0/reference/pipelines/hooks-api.md create mode 100644 static/img/pipelines/guides/affected-units-comment.png diff --git a/docs/2.0/docs/pipelines/guides/hooks/authentication.md b/docs/2.0/docs/pipelines/guides/hooks/authentication.md new file mode 100644 index 0000000000..03a9778230 --- /dev/null +++ b/docs/2.0/docs/pipelines/guides/hooks/authentication.md @@ -0,0 +1,110 @@ +# Authentication & Secrets + +import Tabs from '@theme/Tabs'; +import TabItem from '@theme/TabItem'; + +A hook often needs to call a cloud API or reach an external service: to inspect live resources, fetch a secret, or post a notification. The [`authentication`](/2.0/reference/pipelines/configurations-as-code/api#authentication-block) block on an [`after_hook`](/2.0/reference/pipelines/configurations-as-code/api#after_hook-block) gives the hook's `execute` command a cloud identity to do this. When it is present, Pipelines authenticates and makes the resulting credentials available to the hook before running its command. When it is omitted, the hook runs with no cloud credentials. + +## Cloud credentials + +The `authentication` block authenticates the hook against a cloud provider. It supports AWS, Azure, and GCP through their OIDC blocks (`aws_oidc`, `azure_oidc`, `gcp_oidc`), as well as a `custom` block that runs your own command to obtain credentials. Each provider takes a separate identity for plan and for apply: Pipelines authenticates with the plan identity when the hook runs after a `plan`, and the apply identity when it runs after an `apply`. The two can be the same or different. + +Once the hook is authenticated, the provider's CLIs and SDKs work inside it with no further configuration. Configure the block for your provider: + + + + +```hcl +repository { + after_hook "inspect_resources" { + commands = ["plan"] + execute = [".gruntwork/hooks/inspect-resources.sh"] + + authentication { + aws_oidc { + account_id = "123456789012" + plan_iam_role_arn = "arn:aws:iam::123456789012:role/pipelines-plan" + apply_iam_role_arn = "arn:aws:iam::123456789012:role/pipelines-apply" + } + } + } +} +``` + + + + +```hcl +repository { + after_hook "inspect_resources" { + commands = ["plan"] + execute = [".gruntwork/hooks/inspect-resources.sh"] + + authentication { + azure_oidc { + tenant_id = "a-tenant-id" + subscription_id = "a-subscription-id" + plan_client_id = "plan-client-id" + apply_client_id = "apply-client-id" + } + } + } +} +``` + + + + +```hcl +repository { + after_hook "inspect_resources" { + commands = ["plan"] + execute = [".gruntwork/hooks/inspect-resources.sh"] + + authentication { + gcp_oidc { + workload_identity_provider_id = "projects/123456789012/locations/global/workloadIdentityPools/pipelines-pool/providers/pipelines-provider" + plan_service_account_email = "pipelines-plan@my-gcp-project.iam.gserviceaccount.com" + apply_service_account_email = "pipelines-apply@my-gcp-project.iam.gserviceaccount.com" + } + } + } +} +``` + + + + +```hcl +repository { + after_hook "inspect_resources" { + commands = ["plan"] + execute = [".gruntwork/hooks/inspect-resources.sh"] + + authentication { + custom { + auth_provider_cmd = "./scripts/auth-provider.sh" + } + } + } +} +``` + + + + +For setting up each provider and the full set of fields, see [Authenticating to the Cloud](/2.0/docs/pipelines/concepts/cloud-auth/index) and the [`authentication` block reference](/2.0/reference/pipelines/configurations-as-code/api#authentication-block). + +## Secrets + +Pipelines does not load secrets into a hook for you. It is up to the hook author to decide how a secret is stored and retrieved. What the `authentication` block provides is the context, a cloud identity, that lets the hook retrieve the secret itself at runtime. + +The pattern is the same whatever your provider: store the secret in a secret store, grant the hook's identity permission to read it, and have the hook fetch it at runtime using the credentials the `authentication` block already provides. The secret never appears in your configuration or the hook script. + +For a worked example using AWS and SSM Parameter Store, see [Example: Slack Deploy Notification](/2.0/docs/pipelines/guides/hooks/slack-deploy-notification). For other ways to manage and supply secrets across Pipelines, see [Managing Secrets in your Pipelines](/2.0/docs/pipelines/guides/managing-secrets). + +## Related documentation + +- [Writing a Hook](/2.0/docs/pipelines/guides/hooks/writing-a-hook) +- [`authentication` block reference](/2.0/reference/pipelines/configurations-as-code/api#authentication-block) +- [Authenticating to the Cloud](/2.0/docs/pipelines/concepts/cloud-auth/index) diff --git a/docs/2.0/docs/pipelines/guides/hooks/configuring.md b/docs/2.0/docs/pipelines/guides/hooks/configuring.md new file mode 100644 index 0000000000..3ecd5df698 --- /dev/null +++ b/docs/2.0/docs/pipelines/guides/hooks/configuring.md @@ -0,0 +1,95 @@ +# Configuring Hooks + +Hooks are configured in your Pipelines HCL configuration. + +## After hooks + +After hooks run after Pipelines completes a `plan` or `apply`. They are configured with [`after_hook`](/2.0/reference/pipelines/configurations-as-code/api#after_hook-block) blocks nested inside the `repository` block. Each block declares which commands it runs after and the command to run. You can define multiple after hooks, and they run in the order they are defined. + +```hcl +repository { + after_hook "hello_world" { + commands = ["plan"] + execute = ["echo", "Hello, World!"] + } +} +``` + +### Required fields + +- **`commands`**: the commands this hook runs after. One or both of `plan` and `apply`. +- **`execute`**: the command to run, given as a list of the program followed by its arguments. + +The block label (`hello_world` in the example above) is also required and must be unique within the `repository` block. + +### Optional fields + +- **`name`**: a human-readable display name for the hook. +- **`env`**: environment variables to set for the `execute` command. +- **`run_on_error`**: whether the hook runs when a preceding command or hook failed. Defaults to `false`. +- **`timeout_seconds`**: how long the hook may run before it is terminated. Defaults to `300`. +- **`authentication`**: cloud credentials and secrets for the hook. See [Authentication & Secrets](/2.0/docs/pipelines/guides/hooks/authentication). + +See the [`after_hook` block attributes](/2.0/reference/pipelines/configurations-as-code/api#after_hook-block-attributes) reference for full details. + +## How hooks execute + +### Hooks only run when units are affected + +Hooks only run when the Pipelines run affected at least one unit. A unit is affected when the run actually planned or applied it; units excluded from the run do not count. + +If a change produces no work, hooks are skipped and the run still succeeds. This covers two cases: + +- The change touches no unit, so Pipelines schedules no jobs to run. +- Jobs run, but no units are affected (for example an edit to a file that does not belong to any unit). + +In both cases there is nothing for a hook to act on, so no hooks run. + +### Command filtering + +A run executes a single command, either `plan` or `apply`, and only hooks whose `commands` include that command run. A hook scoped to `apply` does not run on a pull/merge request plan, and a hook scoped to `plan` does not run on an apply. + +A destroy is treated as an `apply` for this purpose, so a hook configured with `commands = ["apply"]` also runs after a destroy. + +### Exit codes + +A hook's exit code is how it tells Pipelines whether it succeeded: + +- **Exit `0`** means the hook succeeded. Pipelines reads back its output files (result, summary, and comment). +- **Any non-zero exit** means the hook failed. **A failed hook fails the entire pipeline run**, exactly as a failed `plan` or `apply` does, and Pipelines ignores the hook's output files. + +Only the exit code decides success or failure. The result a hook writes (`pass`, `warn`, or `deny`) is advisory: it surfaces in the comment but never fails the run on its own, so a hook that exits `0` succeeds even when it reports `deny`. See [Hooks API](/2.0/reference/pipelines/hooks-api) for the result values. + +### Skipping after a failure + +By default, a hook is skipped if anything earlier in the run failed. This includes: + +- the `plan` or `apply` the hook runs after failing, or +- an earlier hook in the list exiting non-zero. + +A skipped hook does not run, and is reported as skipped on the pull/merge request. + +Set `run_on_error = true` to run the hook regardless of an earlier failure. This is useful for hooks that should always run, such as sending a notification whether the run succeeded or failed. A `run_on_error` hook still runs even when a preceding hook failed. + +### Timeout and cancellation + +Each hook has a `timeout_seconds` limit (default `300`). The limit covers the whole hook, including acquiring any credentials from its [`authentication`](/2.0/docs/pipelines/guides/hooks/authentication) block. A hook that runs longer than its limit is cancelled. + +When a hook is cancelled, Pipelines signals the hook's process group to terminate, gives it a brief grace period to exit cleanly, and then forcibly kills it. Because the whole process group is signalled, any child processes the hook started are terminated too. + +A cancelled hook counts as a failure: it fails the run, and like any failure it causes later hooks without `run_on_error = true` to be skipped. + +### Isolated working directory + +Each hook runs in its own temporary copy of the repository, with that copy as its working directory. This is why an `execute` path like `.gruntwork/hooks/affected-units.sh` resolves relative to the repository root. + +Any changes a hook makes to files are not persisted. The copy is discarded once the hook finishes, so edits are never committed, pushed, or seen by the rest of the run. Because each hook gets its own fresh copy, hooks also do not see file changes made by other hooks. + +### Inputs and outputs + +Pipelines passes information to a hook through environment variables, and a hook returns information by writing to files whose paths Pipelines provides. See [Hooks API](/2.0/reference/pipelines/hooks-api) for the full contract. + +## Next steps + +- [Hooks API](/2.0/reference/pipelines/hooks-api) +- [Writing a Hook](/2.0/docs/pipelines/guides/hooks/writing-a-hook) diff --git a/docs/2.0/docs/pipelines/guides/hooks/overview.md b/docs/2.0/docs/pipelines/guides/hooks/overview.md new file mode 100644 index 0000000000..0f3474c93e --- /dev/null +++ b/docs/2.0/docs/pipelines/guides/hooks/overview.md @@ -0,0 +1,34 @@ +# Hooks + +:::info + +Hooks are an Enterprise-only feature. + +::: + +Hooks are how you extend a Pipelines run with your own tooling. If you have used Terragrunt's before and after hooks, the model will feel familiar: you declare a hook and Pipelines runs your command at a defined point in the run. + +This unblocks the kinds of integrations teams reach for most when running infrastructure changes at scale, such as cost estimation, security scanning, policy enforcement, auditing, and notifications. + +Hooks are configured in your Pipelines HCL configuration. Each hook declares whether it runs after `plan` and/or `apply`, and the command to execute. + +Pipelines passes each hook context about the run through environment variables (for example the actor, repository, and action) and gives it the plan output to inspect. In turn, a hook can write outputs that Pipelines reflects back in the pull/merge request comment, so its results show up right alongside the plan or apply. See [Hooks API](/2.0/reference/pipelines/hooks-api) for the full contract. + +:::note + +Hooks are under active development, and new capabilities will continue to roll out over time. Expect this documentation to expand alongside them. + +::: + +## In this section + +- [Setup & Prerequisites](/2.0/docs/pipelines/guides/hooks/setup) - what you need before configuring a hook. +- [Configuring Hooks](/2.0/docs/pipelines/guides/hooks/configuring) - how to declare a hook and how hooks execute. +- [Hooks API](/2.0/reference/pipelines/hooks-api) - the environment variables and files exchanged with a hook. +- [Writing a Hook](/2.0/docs/pipelines/guides/hooks/writing-a-hook) - a step-by-step guide to authoring your own hook. +- [Authentication & Secrets](/2.0/docs/pipelines/guides/hooks/authentication) - giving a hook cloud credentials and secrets when it runs. +- [Example: Slack Deploy Notification](/2.0/docs/pipelines/guides/hooks/slack-deploy-notification) - a worked example. + +## Related documentation + +- [`after_hook` block reference](/2.0/reference/pipelines/configurations-as-code/api#after_hook-block) - the full list of configurable fields. diff --git a/docs/2.0/docs/pipelines/guides/hooks/setup.md b/docs/2.0/docs/pipelines/guides/hooks/setup.md new file mode 100644 index 0000000000..85443c6036 --- /dev/null +++ b/docs/2.0/docs/pipelines/guides/hooks/setup.md @@ -0,0 +1,64 @@ +# Setup & Prerequisites + +import Tabs from '@theme/Tabs'; +import TabItem from '@theme/TabItem'; + +Before you can configure a hook, your repository needs to meet a couple of prerequisites. + +## Enterprise license + +:::info + +Hooks are an Enterprise-only feature. + +::: + +## Plan encryption key + +When any hooks are configured, the `PIPELINES_PLAN_ENCRYPTION_KEY` secret must be set. + +Pipelines adds the OpenTofu/Terraform plan output to the job's artifacts so that hooks can read it. Because plan output can contain sensitive information, Pipelines encrypts it before storing it as an artifact, and the `PIPELINES_PLAN_ENCRYPTION_KEY` secret is the key used to do so. + +If a hook is declared and this secret is missing, Pipelines fails its preflight checks before running. + +### Generating a key + +The secret can be any non-empty value. Use a long, randomly generated value rather than a memorable passphrase. For example: + +```bash +openssl rand -base64 32 +``` + +Store the generated value somewhere safe (such as a password manager) and treat it like any other sensitive credential. If you rotate the key, plan artifacts encrypted with the previous value can no longer be decrypted. + +### Configuring the secret + +Make the generated value available to your Pipelines workflows as a secret named `PIPELINES_PLAN_ENCRYPTION_KEY`. + + + + +Add a repository or organization secret named `PIPELINES_PLAN_ENCRYPTION_KEY` under **Settings > Secrets and variables > Actions**. See [GitHub's documentation on encrypted secrets](https://docs.github.com/en/actions/security-guides/using-secrets-in-github-actions) for details. + +Then pass it through to the Pipelines workflow in your `.github/workflows/pipelines.yml` by adding it to the `secrets` block: + +```yml +jobs: + GruntworkPipelines: + uses: gruntwork-io/pipelines-workflows/.github/workflows/pipelines.yml@v4 + secrets: + # ... other secrets ... + PIPELINES_PLAN_ENCRYPTION_KEY: ${{ secrets.PIPELINES_PLAN_ENCRYPTION_KEY }} +``` + + + + +Add a project or group CI/CD variable named `PIPELINES_PLAN_ENCRYPTION_KEY` under **Settings > CI/CD > Variables**. Mark it **Masked** so the value is not exposed in job logs, and leave both **Protect variable** and **Expand variable reference** unchecked. The variable must not be protected so that it is available on the feature branch pipelines where Pipelines runs `plan`. See [GitLab's documentation on CI/CD variables](https://docs.gitlab.com/ee/ci/variables/) for details. + + + + +## Next steps + +- [Configuring Hooks](/2.0/docs/pipelines/guides/hooks/configuring) diff --git a/docs/2.0/docs/pipelines/guides/hooks/slack-deploy-notification.md b/docs/2.0/docs/pipelines/guides/hooks/slack-deploy-notification.md new file mode 100644 index 0000000000..8dae1676fb --- /dev/null +++ b/docs/2.0/docs/pipelines/guides/hooks/slack-deploy-notification.md @@ -0,0 +1,114 @@ +# Example: Slack Deploy Notification + +This example posts a message to Slack after a deploy (`apply`), so your team is notified when infrastructure changes are rolled out. It uses `run_on_error` so a notification is sent whether the apply succeeds or fails, and it fetches the Slack webhook URL from AWS SSM Parameter Store using the credentials from the hook's [`authentication`](/2.0/docs/pipelines/guides/hooks/authentication) block. + +Before you start, make sure hooks are set up for your repository (see [Setup & Prerequisites](/2.0/docs/pipelines/guides/hooks/setup)). + +## 1. Decide where to store the secret + +Pipelines does not store secrets for you (see [Authentication & Secrets](/2.0/docs/pipelines/guides/hooks/authentication)). As the hook author, you decide where the webhook URL lives and how the hook retrieves it. This example keeps it in AWS SSM Parameter Store and gives the hook an IAM role that can read it. The same approach works with any secret store the hook's identity can reach, such as AWS Secrets Manager, Azure Key Vault, or GCP Secret Manager. + +## 2. Create a Slack incoming webhook + +In Slack, create an app with [incoming webhooks](https://api.slack.com/messaging/webhooks) enabled and add a webhook to the channel you want to post to. Slack gives you a webhook URL that looks like `https://hooks.slack.com/services/FOO/BAR/BAZ`. Posting a JSON payload to that URL delivers a message to the channel. + +## 3. Store the webhook URL in SSM Parameter Store + +The webhook URL is a secret, so keep it out of your configuration. Store it in SSM Parameter Store as an encrypted `SecureString`: + +```bash +aws ssm put-parameter \ + --name "/$$SLACK_WEBHOOK_PARAM$$" \ + --type "SecureString" \ + --value "$$SLACK_WEBHOOK_URL$$" +``` + +## 4. Create a role the hook can assume + +The hook reads the secret at runtime under an IAM role. Create (or reuse) a role the hook can assume, and allow it to read the parameter. At minimum the role needs `ssm:GetParameter` on the parameter, plus `kms:Decrypt` on the key if you encrypted it with a customer-managed key: + +```json +{ + "Version": "2012-10-17", + "Statement": [ + { + "Effect": "Allow", + "Action": "ssm:GetParameter", + "Resource": "arn:aws:ssm:*:$$AWS_ACCOUNT_ID$$:parameter/$$SLACK_WEBHOOK_PARAM$$" + } + ] +} +``` + +The role also needs a trust policy that allows Pipelines to assume it. See [Authenticating to the Cloud](/2.0/docs/pipelines/concepts/cloud-auth/aws) for how Pipelines assumes roles. + +## 5. Configure the hook + +Declare an [`after_hook`](/2.0/reference/pipelines/configurations-as-code/api#after_hook-block) on `apply`. Set `run_on_error = true` so the notification is sent even when the apply fails, and reference the role from the previous step in the `authentication` block as both the plan and apply role: + +```hcl +repository { + after_hook "notify_slack" { + name = "Notify Slack" + commands = ["apply"] + execute = ["bash", ".gruntwork/hooks/slack-notify.sh"] + run_on_error = true + + authentication { + aws_oidc { + account_id = "$$AWS_ACCOUNT_ID$$" + plan_iam_role_arn = "arn:aws:iam::$$AWS_ACCOUNT_ID$$:role/$$HOOK_ROLE$$" + apply_iam_role_arn = "arn:aws:iam::$$AWS_ACCOUNT_ID$$:role/$$HOOK_ROLE$$" + } + } + } +} +``` + +## 6. Fetch the secret and post to Slack + +Create `.gruntwork/hooks/slack-notify.sh`. It fetches the webhook URL, reads the run context from the [Hooks API](/2.0/reference/pipelines/hooks-api) environment variables to build a message, and posts it to Slack: + +```bash +#!/usr/bin/env bash +set -euo pipefail + +# The authentication block has already authenticated the hook to AWS, +# so the AWS CLI reads the webhook URL directly. +webhook_url=$(aws ssm get-parameter \ + --name "/$$SLACK_WEBHOOK_PARAM$$" \ + --with-decryption \ + --query "Parameter.Value" \ + --output text) + +# Read run context provided by Pipelines. +repository="$PIPELINES_HOOKS_CTX_REPOSITORY" +actor="$PIPELINES_HOOKS_CTX_ACTOR" +status="$PIPELINES_HOOKS_CTX_ACTION_STATUS" + +if [ "$status" = "succeeded" ]; then + text="✅ Deploy of $repository by $actor succeeded." +else + text="❌ Deploy of $repository by $actor failed." +fi + +# Build the JSON payload safely and post it to the Slack webhook. +payload=$(jq -n --arg text "$text" '{text: $text}') +curl --fail --silent --show-error \ + -X POST \ + -H 'Content-type: application/json' \ + --data "$payload" \ + "$webhook_url" +``` + +The hook writes nothing to its output files, so Pipelines reports it as a `pass`. See [Writing a Hook](/2.0/docs/pipelines/guides/hooks/writing-a-hook) if you also want it to leave a comment on the pull/merge request. + +## What you'll see + +After your next merge to a deploy branch, Pipelines runs the apply and then this hook. Your team receives a Slack message reporting who deployed and whether it succeeded or failed. + +## Related documentation + +- [Authentication & Secrets](/2.0/docs/pipelines/guides/hooks/authentication) - how the `authentication` block gives the hook access to the secret. +- [Writing a Hook](/2.0/docs/pipelines/guides/hooks/writing-a-hook) - authoring hooks from scratch. +- [Hooks API](/2.0/reference/pipelines/hooks-api) - the environment variables the script reads. diff --git a/docs/2.0/docs/pipelines/guides/hooks/writing-a-hook.md b/docs/2.0/docs/pipelines/guides/hooks/writing-a-hook.md new file mode 100644 index 0000000000..3a92dc6ffd --- /dev/null +++ b/docs/2.0/docs/pipelines/guides/hooks/writing-a-hook.md @@ -0,0 +1,141 @@ +# Writing a Hook + +This guide builds a hook from scratch: a small bash script that reads context about the run, inspects the units that were planned, and posts a comment back on the pull/merge request. Along the way it covers the full loop a hook goes through, reading inputs from the environment and writing outputs to files. + +Before you start, make sure hooks are set up for your repository. See [Setup & Prerequisites](/2.0/docs/pipelines/guides/hooks/setup). + +## 1. Create the hook script + +A hook is any executable command. Here we use a bash script committed to the repository. Create a file at `.gruntwork/hooks/affected-units.sh` with a shebang and strict mode: + +```bash +#!/usr/bin/env bash +set -euo pipefail +``` + +Hooks run from the root of a copy of your repository, so this path resolves relative to the repository root when the hook is configured. See [Isolated working directory](/2.0/docs/pipelines/guides/hooks/configuring#isolated-working-directory) for details. + +## 2. Read the run context + +Pipelines passes information to the hook through environment variables. Context values about the run are in the `PIPELINES_HOOKS_CTX_*` namespace, and paths to input files are in the `PIPELINES_HOOKS_IN_*` namespace. + +Read the actor and action from the context, and the path to the units file from the inputs: + +```bash +actor="$PIPELINES_HOOKS_CTX_ACTOR" +action="$PIPELINES_HOOKS_CTX_ACTION" +units_file="$PIPELINES_HOOKS_IN_UNITS_JSON_FILE" +``` + +`PIPELINES_HOOKS_IN_UNITS_JSON_FILE` points at a JSON array of the units in the run, each with its path and (when one exists) the path to its plan JSON. For example, list the affected unit paths with `jq`: + +```bash +jq -r '.[].path' "$units_file" +``` + +For the complete list of context variables and input files, see the [Hooks API](/2.0/reference/pipelines/hooks-api). + +## 3. Write the comment + +A hook returns information by writing to the files named in the `PIPELINES_HOOKS_OUT_*` namespace. Build a comment listing the affected units and write it to the comment file: + +```bash +{ + echo "$action triggered by @$actor affected:" + echo + jq -r '.[] | "- \(.path)"' "$units_file" +} > "$PIPELINES_HOOKS_OUT_COMMENT_FILE" +``` + +Writing outputs is optional. This hook only posts a comment, so it does not write a result file: when a hook writes nothing to `PIPELINES_HOOKS_OUT_RESULT_FILE` and exits `0`, Pipelines defaults its result to `pass`. To flag a problem instead, write `warn` or `deny` to that file. The result is advisory and surfaces in the comment, but does not by itself fail the run. [How results and comments appear](#how-results-and-comments-appear) below covers how each one renders on the request. + +The complete script: + +```bash +#!/usr/bin/env bash +set -euo pipefail + +actor="$PIPELINES_HOOKS_CTX_ACTOR" +action="$PIPELINES_HOOKS_CTX_ACTION" +units_file="$PIPELINES_HOOKS_IN_UNITS_JSON_FILE" + +{ + echo "$action triggered by @$actor affected:" + echo + jq -r '.[] | "- \(.path)"' "$units_file" +} > "$PIPELINES_HOOKS_OUT_COMMENT_FILE" +``` + +## 4. Make the script executable + +Pipelines runs the hook as a program, so the script needs the executable bit set. Set it and commit the change so the bit is preserved in git: + +```bash +chmod +x .gruntwork/hooks/affected-units.sh +git add .gruntwork/hooks/affected-units.sh +``` + +## 5. Configure the hook + +Declare an [`after_hook`](/2.0/reference/pipelines/configurations-as-code/api#after_hook-block) block in your `repository` configuration. Set `commands` to the commands it runs after and `execute` to the script's repository-root-relative path: + +```hcl +repository { + after_hook "affected_units" { + name = "Affected Units" + commands = ["plan"] + execute = [".gruntwork/hooks/affected-units.sh"] + } +} +``` + +See [Configuring Hooks](/2.0/docs/pipelines/guides/hooks/configuring) for every field and how hooks execute. + +## 6. Run the hook + +Commit the script and configuration, then open a pull/merge request that changes at least one unit. Pipelines runs the hook after the `plan`, and the comment your hook wrote appears on the request alongside the plan output. + +![Hook comment on a pull request](/img/pipelines/guides/affected-units-comment.png) + +The result and comment your hook produced are shown in the Pipelines status comment. The next section covers exactly how they render. + +## How results and comments appear + +Pipelines includes each hook in its status comment on the pull/merge request, rendered as a collapsible section. The output files the hook writes control how that section looks. + +### Title and icon + +The section is titled by the hook's `name`, or its block label when `name` is unset. An icon prefixes the title to reflect the outcome: + +| Outcome | Icon | +|---|---| +| `pass` result | ✅ | +| `warn` result | ⚠️ | +| `deny` result | ❌ | +| Failed (non-zero exit) | ❌ | +| Timed out | ❌ | +| Skipped | ⏭️ | + +The overall comment reflects the most severe hook outcome, so a `warn` or `deny` is visible at the top without expanding each section. + +### Summary and comment + +The two text outputs serve different purposes: + +- **Summary** (`PIPELINES_HOOKS_OUT_SUMMARY_FILE`) appears inline next to the title, after a colon, for example `⚠️ Affected Units: 3 units changed`. Use it for a short, at-a-glance headline. +- **Comment** (`PIPELINES_HOOKS_OUT_COMMENT_FILE`) is the body of the collapsible section, rendered as HTML. Use it for detailed output such as a table, a list, or links. + +If the hook writes no comment, Pipelines shows a fallback line in the body, such as `Hook exited with code 0`. The hook from this guide writes a `pass` result and a comment, so it appears with a ✅ icon and its unit list in the body. + +### Results do not fail the run + +A `deny` shows a ❌ and raises the severity shown in the comment, but it does not fail the run. Only a non-zero exit fails the run; see [Exit codes](/2.0/docs/pipelines/guides/hooks/configuring#exit-codes). + +### Skipped hooks + +A hook skipped because of an earlier failure (see [Skipping after a failure](/2.0/docs/pipelines/guides/hooks/configuring#skipping-after-a-failure)) appears with a ⏭️ icon and the note "Hook skipped due to previous failure", rather than as a pass or a failure. + +## Next steps + +- [Authentication & Secrets](/2.0/docs/pipelines/guides/hooks/authentication) - give a hook cloud credentials and secrets. +- [Hooks API](/2.0/reference/pipelines/hooks-api) - the full environment variable and file contract. diff --git a/docs/2.0/reference/pipelines/configurations-as-code/api.mdx b/docs/2.0/reference/pipelines/configurations-as-code/api.mdx index 1088cf00bb..1d50f5cac0 100644 --- a/docs/2.0/reference/pipelines/configurations-as-code/api.mdx +++ b/docs/2.0/reference/pipelines/configurations-as-code/api.mdx @@ -106,12 +106,50 @@ repository { +### `after_hook` block + + + + +After hook blocks are nested inside the [repository](#repository-block) block and define a command that Pipelines runs after a `plan` or `apply`. +
+The label applied to an after_hook block is a user-defined identifier for the hook and must be unique within the repository block. +
+See more [below](#after_hook-block-attributes). + +:::info + +Hooks are an Enterprise-only feature. + +::: + +:::caution + +When any `after_hook` block is configured, the `PIPELINES_PLAN_ENCRYPTION_KEY` secret must be set. Pipelines will fail preflight checks if hooks are declared and this secret is missing. + +::: + +
+ + +```hcl +repository { + after_hook "affected_units" { + name = "Affected Units" + commands = ["plan"] + execute = ["bash", ".gruntwork/hooks/affected-units.sh"] + } +} +``` + +
+ ### `env` block -Env blocks are configuration components used by [repository](#repository-block) blocks to specify environment variables that will be set when executing Terragrunt commands. The block contains a map of environment variable names and their values. +Env blocks are configuration components used by [repository](#repository-block) blocks to specify environment variables that will be set when executing Terragrunt commands. The block contains a map of environment variable names and their values. Env blocks are also valid inside [after_hook](#after_hook-block) blocks to set environment variables for the hook's `execute` command. @@ -351,6 +389,66 @@ A comma separate list of ignore filters to exclude from pipelines runs. See the +### `after_hook` block attributes + + + +A human-readable display name for the hook, shown in Pipelines output and status comments. + + + + + +The Terragrunt commands after which this hook runs. Valid values are `plan` and `apply`. Values must be unique. + + + + + + +The command Pipelines runs for the hook, written as a list where the first element is the program and the rest are its arguments. The program is executed directly rather than through a shell, so shell features such as variable expansion, pipes, and redirection are only available if you invoke a shell yourself (for example `bash -c`). + +Run an inline shell snippet (note the `bash -c` so the variable is expanded by the shell): + +```hcl +execute = ["bash", "-c", "echo \"Triggered by $PIPELINES_HOOKS_CTX_ACTOR\""] +``` + +Or invoke an executable script directly by its path: + +```hcl +execute = [".gruntwork/hooks/affected-units.sh"] +``` + + + + + + +An [env](#env-block) block of key/value environment variables made available to the `execute` command. Values may be strings, numbers, or booleans and are converted to strings. + + + + + +Whether the hook runs even when the preceding `plan` or `apply` command fails, or when an earlier hook in the run failed. When `false`, the hook is skipped if anything before it failed. + + + + + + +The maximum number of seconds the hook is allowed to run before Pipelines terminates it. Must be greater than `0`. + + + + + + +An [authentication](#authentication-block) block defining the credentials for this hook. When present, Pipelines authenticates with the given context and makes those credentials available to the hook before running its `execute` command. When omitted, the hook runs without any cloud credentials. See more [below](#authentication-block-attributes). + + + ### `filter` block attributes diff --git a/docs/2.0/reference/pipelines/hooks-api.md b/docs/2.0/reference/pipelines/hooks-api.md new file mode 100644 index 0000000000..12f72ef9a7 --- /dev/null +++ b/docs/2.0/reference/pipelines/hooks-api.md @@ -0,0 +1,100 @@ +# Hooks API + +Pipelines communicates with a hook entirely through environment variables. There are three namespaces: + +- **`PIPELINES_HOOKS_CTX_*`**: context values about the run, set directly as the variable's value. +- **`PIPELINES_HOOKS_IN_*`**: paths to input files the hook reads. +- **`PIPELINES_HOOKS_OUT_*`**: paths to output files the hook writes. + +The hook's `execute` command reads from the first two namespaces and writes to the third. + +## Context inputs (`PIPELINES_HOOKS_CTX_*`) + +Scalar facts about the run, set directly as the variable's value. + +A context variable that does not apply to the run is left unset, rather than set to an empty string. A hook can therefore tell "not applicable" apart from "set but blank", for example with `[ -z "${PIPELINES_HOOKS_CTX_ACTION+x}" ]` in bash. + +### Always set + +| Variable | Description | +|---|---| +| `PIPELINES_HOOKS_CTX_CI_PLATFORM` | The CI platform running the hook: `github` or `gitlab`. | +| `PIPELINES_HOOKS_CTX_ORGANIZATION` | The organization (GitHub) or group (GitLab) that owns the repository. | +| `PIPELINES_HOOKS_CTX_REPOSITORY` | The repository name. | +| `PIPELINES_HOOKS_CTX_ACTOR` | The user that triggered the run. | +| `PIPELINES_HOOKS_CTX_GIT_REF` | The git ref that triggered the run. | +| `PIPELINES_HOOKS_CTX_GIT_HASH` | The commit SHA the run is operating on. | + +### Set when applicable + +| Variable | Description | +|---|---| +| `PIPELINES_HOOKS_CTX_ACTION` | The command the hook ran after: `plan` or `apply`. A destroy is reported as `apply`. | +| `PIPELINES_HOOKS_CTX_ACTION_STATUS` | The outcome of that command: `succeeded` or `failed`. | +| `PIPELINES_HOOKS_CTX_CHANGE_REQUEST_NUMBER` | The pull/merge request number. | +| `PIPELINES_HOOKS_CTX_CHANGE_REQUEST_URL` | The pull/merge request URL. | +| `PIPELINES_HOOKS_CTX_CHANGE_REQUEST_BRANCH` | The source branch of the pull/merge request. | + +The three `CHANGE_REQUEST` variables are set or absent together: they are set when the run is associated with a pull/merge request, and absent for a push to a deploy branch. + +## File inputs (`PIPELINES_HOOKS_IN_*`) + +Paths to files the hook reads. Both are always set. + +| Variable | Description | +|---|---| +| `PIPELINES_HOOKS_IN_PLANS_JSON_DIR` | Directory containing the decrypted plan JSON for the run's units. | +| `PIPELINES_HOOKS_IN_UNITS_JSON_FILE` | Path to a JSON file describing the units in the run. | + +### Plans JSON directory + +Within `PIPELINES_HOOKS_IN_PLANS_JSON_DIR`, each unit's plan is stored at `/tfplan.json`, where `` is the unit's path relative to the repository root. Only units that produced a plan have a file, so the directory may not contain an entry for every unit in the run. + +The file is the JSON form of the OpenTofu/Terraform plan (`terraform show -json`). + +### Units JSON file + +`PIPELINES_HOOKS_IN_UNITS_JSON_FILE` points to a JSON array describing each unit the hook applies to: + +```json +[ + { + "path": "dev/us-east-1/vpc", + "plan_json_file": "/abs/path/to/dev/us-east-1/vpc/tfplan.json" + }, + { + "path": "dev/us-east-1/no-changes" + } +] +``` + +| Field | Description | +|---|---| +| `path` | The unit's path relative to the repository root. | +| `plan_json_file` | Absolute path to the unit's decrypted plan JSON, the same file addressed under the plans directory above. Omitted when the unit produced no plan. | + +## Outputs (`PIPELINES_HOOKS_OUT_*`) + +Paths to files the hook may write. All are always set. Pipelines reads them back only when the hook process exits `0`; if the hook exits non-zero the output files are ignored. + +| Variable | Description | +|---|---| +| `PIPELINES_HOOKS_OUT_RESULT_FILE` | Write the hook's result: `pass`, `warn`, or `deny`. | +| `PIPELINES_HOOKS_OUT_SUMMARY_FILE` | Write a short summary of the hook's outcome. | +| `PIPELINES_HOOKS_OUT_COMMENT_FILE` | Write a comment body to surface on the pull/merge request. | + +Writing to these files is optional. A hook that writes nothing reports a `pass` with no summary or comment. + +### Results + +The result written to `PIPELINES_HOOKS_OUT_RESULT_FILE` is one of: + +| Result | Meaning | +|---|---| +| `pass` | The hook succeeded. Produces no comment. | +| `warn` | Advisory warning. | +| `deny` | Advisory rejection. | + +The result is an advisory severity surfaced in the pull/merge request comment; it does not by itself change whether the run succeeds. In particular, `deny` does not fail the run. An empty or unrecognized value is treated as `pass`. + +For how the result, summary, and comment appear on the pull/merge request, see [How results and comments appear](/2.0/docs/pipelines/guides/hooks/writing-a-hook#how-results-and-comments-appear). diff --git a/sidebars/docs.js b/sidebars/docs.js index d92a19d838..157339dac5 100644 --- a/sidebars/docs.js +++ b/sidebars/docs.js @@ -378,6 +378,47 @@ const sidebar = [ type: "doc", id: "2.0/docs/pipelines/guides/extending-pipelines", }, + { + label: "Hooks", + type: "category", + collapsed: true, + link: { + type: "doc", + id: "2.0/docs/pipelines/guides/hooks/overview", + }, + items: [ + { + label: "Overview", + type: "doc", + id: "2.0/docs/pipelines/guides/hooks/overview", + }, + { + label: "Setup & Prerequisites", + type: "doc", + id: "2.0/docs/pipelines/guides/hooks/setup", + }, + { + label: "Configuring Hooks", + type: "doc", + id: "2.0/docs/pipelines/guides/hooks/configuring", + }, + { + label: "Writing a Hook", + type: "doc", + id: "2.0/docs/pipelines/guides/hooks/writing-a-hook", + }, + { + label: "Authentication & Secrets", + type: "doc", + id: "2.0/docs/pipelines/guides/hooks/authentication", + }, + { + label: "Example: Slack Deploy Notification", + type: "doc", + id: "2.0/docs/pipelines/guides/hooks/slack-deploy-notification", + }, + ], + }, { label: "Installing Drift Detection", type: "doc", diff --git a/sidebars/reference.js b/sidebars/reference.js index ba42922093..80b100d4f9 100644 --- a/sidebars/reference.js +++ b/sidebars/reference.js @@ -53,6 +53,11 @@ const sidebar = [ }, ], }, + { + label: "Hooks API", + type: "doc", + id: "2.0/reference/pipelines/hooks-api", + }, { label: "Ignore List", type: "doc", diff --git a/static/img/pipelines/guides/affected-units-comment.png b/static/img/pipelines/guides/affected-units-comment.png new file mode 100644 index 0000000000000000000000000000000000000000..0725511bf42d471161ea10676fa89466e7199cac GIT binary patch literal 71288 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

XKMu;Hh%u-9ISn{XGbar=R|u< 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 E0FDY^8~^|S literal 0 HcmV?d00001 From 481f8d175028aa278d84e7fa0731655f7f376e44 Mon Sep 17 00:00:00 2001 From: Lewis Christie Date: Wed, 17 Jun 2026 17:14:07 -0600 Subject: [PATCH 2/7] Fix broken link --- docs/2.0/docs/pipelines/guides/hooks/authentication.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/2.0/docs/pipelines/guides/hooks/authentication.md b/docs/2.0/docs/pipelines/guides/hooks/authentication.md index 03a9778230..c90b024da7 100644 --- a/docs/2.0/docs/pipelines/guides/hooks/authentication.md +++ b/docs/2.0/docs/pipelines/guides/hooks/authentication.md @@ -93,7 +93,7 @@ repository { -For setting up each provider and the full set of fields, see [Authenticating to the Cloud](/2.0/docs/pipelines/concepts/cloud-auth/index) and the [`authentication` block reference](/2.0/reference/pipelines/configurations-as-code/api#authentication-block). +For setting up each provider and the full set of fields, see [Authenticating to the Cloud](/2.0/docs/pipelines/concepts/cloud-auth/index.md) and the [`authentication` block reference](/2.0/reference/pipelines/configurations-as-code/api#authentication-block). ## Secrets @@ -107,4 +107,4 @@ For a worked example using AWS and SSM Parameter Store, see [Example: Slack Depl - [Writing a Hook](/2.0/docs/pipelines/guides/hooks/writing-a-hook) - [`authentication` block reference](/2.0/reference/pipelines/configurations-as-code/api#authentication-block) -- [Authenticating to the Cloud](/2.0/docs/pipelines/concepts/cloud-auth/index) +- [Authenticating to the Cloud](/2.0/docs/pipelines/concepts/cloud-auth/index.md) From 0583a3b4e565c16f766fecd94b1b0a193a5d9b8b Mon Sep 17 00:00:00 2001 From: Lewis Christie Date: Wed, 17 Jun 2026 17:21:11 -0600 Subject: [PATCH 3/7] Tidy --- .../docs/pipelines/guides/hooks/slack-deploy-notification.md | 2 +- docs/2.0/reference/pipelines/hooks-api.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/2.0/docs/pipelines/guides/hooks/slack-deploy-notification.md b/docs/2.0/docs/pipelines/guides/hooks/slack-deploy-notification.md index 8dae1676fb..637d5661cd 100644 --- a/docs/2.0/docs/pipelines/guides/hooks/slack-deploy-notification.md +++ b/docs/2.0/docs/pipelines/guides/hooks/slack-deploy-notification.md @@ -101,7 +101,7 @@ curl --fail --silent --show-error \ "$webhook_url" ``` -The hook writes nothing to its output files, so Pipelines reports it as a `pass`. See [Writing a Hook](/2.0/docs/pipelines/guides/hooks/writing-a-hook) if you also want it to leave a comment on the pull/merge request. +This hook writes no output files, so Pipelines reports it as a `pass`. (Heads up: with `set -euo pipefail` and curl's `--fail`, a failed secret lookup or Slack post exits non-zero and fails the run; handle those cases in your script if you want it to pass regardless.) To also leave a comment on the pull/merge request, see [Writing a Hook](/2.0/docs/pipelines/guides/hooks/writing-a-hook). ## What you'll see diff --git a/docs/2.0/reference/pipelines/hooks-api.md b/docs/2.0/reference/pipelines/hooks-api.md index 12f72ef9a7..63e476dfcd 100644 --- a/docs/2.0/reference/pipelines/hooks-api.md +++ b/docs/2.0/reference/pipelines/hooks-api.md @@ -91,7 +91,7 @@ The result written to `PIPELINES_HOOKS_OUT_RESULT_FILE` is one of: | Result | Meaning | |---|---| -| `pass` | The hook succeeded. Produces no comment. | +| `pass` | The default result. | | `warn` | Advisory warning. | | `deny` | Advisory rejection. | From 046488f37c52ff088efdad6ea847143954e92898 Mon Sep 17 00:00:00 2001 From: Lewis Christie Date: Fri, 19 Jun 2026 08:44:42 -0600 Subject: [PATCH 4/7] Apply suggestions from code review Co-authored-by: Oreoluwa Agunbiade <21035422+oredavids@users.noreply.github.com> Co-authored-by: Eben Eliason --- docs/2.0/docs/pipelines/guides/hooks/authentication.md | 2 +- docs/2.0/docs/pipelines/guides/hooks/configuring.md | 8 ++++---- docs/2.0/docs/pipelines/guides/hooks/overview.md | 2 +- docs/2.0/docs/pipelines/guides/hooks/setup.md | 2 +- .../pipelines/guides/hooks/slack-deploy-notification.md | 2 +- .../reference/pipelines/configurations-as-code/api.mdx | 2 +- 6 files changed, 9 insertions(+), 9 deletions(-) diff --git a/docs/2.0/docs/pipelines/guides/hooks/authentication.md b/docs/2.0/docs/pipelines/guides/hooks/authentication.md index c90b024da7..c9371563da 100644 --- a/docs/2.0/docs/pipelines/guides/hooks/authentication.md +++ b/docs/2.0/docs/pipelines/guides/hooks/authentication.md @@ -101,7 +101,7 @@ Pipelines does not load secrets into a hook for you. It is up to the hook author The pattern is the same whatever your provider: store the secret in a secret store, grant the hook's identity permission to read it, and have the hook fetch it at runtime using the credentials the `authentication` block already provides. The secret never appears in your configuration or the hook script. -For a worked example using AWS and SSM Parameter Store, see [Example: Slack Deploy Notification](/2.0/docs/pipelines/guides/hooks/slack-deploy-notification). For other ways to manage and supply secrets across Pipelines, see [Managing Secrets in your Pipelines](/2.0/docs/pipelines/guides/managing-secrets). +For a working example using AWS and SSM Parameter Store, see [Example: Slack Deploy Notification](/2.0/docs/pipelines/guides/hooks/slack-deploy-notification). For other ways to manage and supply secrets across Pipelines, see [Managing Secrets in your Pipelines](/2.0/docs/pipelines/guides/managing-secrets). ## Related documentation diff --git a/docs/2.0/docs/pipelines/guides/hooks/configuring.md b/docs/2.0/docs/pipelines/guides/hooks/configuring.md index 3ecd5df698..2551c6bfcf 100644 --- a/docs/2.0/docs/pipelines/guides/hooks/configuring.md +++ b/docs/2.0/docs/pipelines/guides/hooks/configuring.md @@ -17,7 +17,7 @@ repository { ### Required fields -- **`commands`**: the commands this hook runs after. One or both of `plan` and `apply`. +- **`commands`**: the Pipelines commands this hook runs after. One or both of `plan` and `apply`. - **`execute`**: the command to run, given as a list of the program followed by its arguments. The block label (`hello_world` in the example above) is also required and must be unique within the `repository` block. @@ -64,8 +64,8 @@ Only the exit code decides success or failure. The result a hook writes (`pass`, By default, a hook is skipped if anything earlier in the run failed. This includes: -- the `plan` or `apply` the hook runs after failing, or -- an earlier hook in the list exiting non-zero. +- the `plan` or `apply` the hook runs after, or +- an earlier hook in the list that exited non-zero. A skipped hook does not run, and is reported as skipped on the pull/merge request. @@ -77,7 +77,7 @@ Each hook has a `timeout_seconds` limit (default `300`). The limit covers the wh When a hook is cancelled, Pipelines signals the hook's process group to terminate, gives it a brief grace period to exit cleanly, and then forcibly kills it. Because the whole process group is signalled, any child processes the hook started are terminated too. -A cancelled hook counts as a failure: it fails the run, and like any failure it causes later hooks without `run_on_error = true` to be skipped. +A cancelled hook counts as a failure: it fails the run and, like any failure, causes later hooks without `run_on_error = true` to be skipped. ### Isolated working directory diff --git a/docs/2.0/docs/pipelines/guides/hooks/overview.md b/docs/2.0/docs/pipelines/guides/hooks/overview.md index 0f3474c93e..1ab658295d 100644 --- a/docs/2.0/docs/pipelines/guides/hooks/overview.md +++ b/docs/2.0/docs/pipelines/guides/hooks/overview.md @@ -12,7 +12,7 @@ This unblocks the kinds of integrations teams reach for most when running infras Hooks are configured in your Pipelines HCL configuration. Each hook declares whether it runs after `plan` and/or `apply`, and the command to execute. -Pipelines passes each hook context about the run through environment variables (for example the actor, repository, and action) and gives it the plan output to inspect. In turn, a hook can write outputs that Pipelines reflects back in the pull/merge request comment, so its results show up right alongside the plan or apply. See [Hooks API](/2.0/reference/pipelines/hooks-api) for the full contract. +Pipelines passes each hook context about the run through environment variables (for example the actor, repository, and action). *After hooks* additionally receive the run's OpenTofu/Terraform plan. In turn, a hook can write outputs that Pipelines reflects back in the pull/merge request comment, so its results show up alongside the plan or apply summary. See [Hooks API](/2.0/reference/pipelines/hooks-api) for the full contract. :::note diff --git a/docs/2.0/docs/pipelines/guides/hooks/setup.md b/docs/2.0/docs/pipelines/guides/hooks/setup.md index 85443c6036..4e96c2a393 100644 --- a/docs/2.0/docs/pipelines/guides/hooks/setup.md +++ b/docs/2.0/docs/pipelines/guides/hooks/setup.md @@ -17,7 +17,7 @@ Hooks are an Enterprise-only feature. When any hooks are configured, the `PIPELINES_PLAN_ENCRYPTION_KEY` secret must be set. -Pipelines adds the OpenTofu/Terraform plan output to the job's artifacts so that hooks can read it. Because plan output can contain sensitive information, Pipelines encrypts it before storing it as an artifact, and the `PIPELINES_PLAN_ENCRYPTION_KEY` secret is the key used to do so. +Pipelines adds the OpenTofu/Terraform plan output to the job's artifacts so that after-hooks can read it. Because plan output can contain sensitive information, Pipelines encrypts it before storing it as an artifact, and the `PIPELINES_PLAN_ENCRYPTION_KEY` secret is the key used to do so. If a hook is declared and this secret is missing, Pipelines fails its preflight checks before running. diff --git a/docs/2.0/docs/pipelines/guides/hooks/slack-deploy-notification.md b/docs/2.0/docs/pipelines/guides/hooks/slack-deploy-notification.md index 637d5661cd..f1837c778b 100644 --- a/docs/2.0/docs/pipelines/guides/hooks/slack-deploy-notification.md +++ b/docs/2.0/docs/pipelines/guides/hooks/slack-deploy-notification.md @@ -6,7 +6,7 @@ Before you start, make sure hooks are set up for your repository (see [Setup & P ## 1. Decide where to store the secret -Pipelines does not store secrets for you (see [Authentication & Secrets](/2.0/docs/pipelines/guides/hooks/authentication)). As the hook author, you decide where the webhook URL lives and how the hook retrieves it. This example keeps it in AWS SSM Parameter Store and gives the hook an IAM role that can read it. The same approach works with any secret store the hook's identity can reach, such as AWS Secrets Manager, Azure Key Vault, or GCP Secret Manager. +Pipelines does not store secrets for you (see [Authentication & Secrets](/2.0/docs/pipelines/guides/hooks/authentication)). As the hook author, you decide where the webhook URL is stored and how the hook retrieves it. This example stores it in AWS SSM Parameter Store and gives the hook an IAM role that can read it. The same approach works with any secret store that the hook's identity can reach, such as AWS Secrets Manager, Azure Key Vault, or GCP Secret Manager. ## 2. Create a Slack incoming webhook diff --git a/docs/2.0/reference/pipelines/configurations-as-code/api.mdx b/docs/2.0/reference/pipelines/configurations-as-code/api.mdx index 1d50f5cac0..abd1511acd 100644 --- a/docs/2.0/reference/pipelines/configurations-as-code/api.mdx +++ b/docs/2.0/reference/pipelines/configurations-as-code/api.mdx @@ -431,7 +431,7 @@ An [env](#env-block) block of key/value environment variables made available to -Whether the hook runs even when the preceding `plan` or `apply` command fails, or when an earlier hook in the run failed. When `false`, the hook is skipped if anything before it failed. +Whether the hook runs even when the preceding Pipelines `plan` or `apply` command fails, or when an earlier hook in the run failed. When `false`, the hook is skipped if anything before it failed. From 36333cd5924d9f7c52209d351f66f9bfd22c23ab Mon Sep 17 00:00:00 2001 From: Lewis Christie Date: Fri, 19 Jun 2026 09:17:53 -0600 Subject: [PATCH 5/7] Rename hook env vars --- .../guides/hooks/slack-deploy-notification.md | 6 +-- .../pipelines/guides/hooks/writing-a-hook.md | 28 +++++----- .../pipelines/configurations-as-code/api.mdx | 2 +- docs/2.0/reference/pipelines/hooks-api.md | 52 +++++++++---------- 4 files changed, 44 insertions(+), 44 deletions(-) diff --git a/docs/2.0/docs/pipelines/guides/hooks/slack-deploy-notification.md b/docs/2.0/docs/pipelines/guides/hooks/slack-deploy-notification.md index f1837c778b..ff079bc744 100644 --- a/docs/2.0/docs/pipelines/guides/hooks/slack-deploy-notification.md +++ b/docs/2.0/docs/pipelines/guides/hooks/slack-deploy-notification.md @@ -82,9 +82,9 @@ webhook_url=$(aws ssm get-parameter \ --output text) # Read run context provided by Pipelines. -repository="$PIPELINES_HOOKS_CTX_REPOSITORY" -actor="$PIPELINES_HOOKS_CTX_ACTOR" -status="$PIPELINES_HOOKS_CTX_ACTION_STATUS" +repository="$PIPELINES_HOOK_CTX_REPOSITORY" +actor="$PIPELINES_HOOK_CTX_ACTOR" +status="$PIPELINES_HOOK_CTX_ACTION_STATUS" if [ "$status" = "succeeded" ]; then text="✅ Deploy of $repository by $actor succeeded." diff --git a/docs/2.0/docs/pipelines/guides/hooks/writing-a-hook.md b/docs/2.0/docs/pipelines/guides/hooks/writing-a-hook.md index 3a92dc6ffd..d808d2898b 100644 --- a/docs/2.0/docs/pipelines/guides/hooks/writing-a-hook.md +++ b/docs/2.0/docs/pipelines/guides/hooks/writing-a-hook.md @@ -17,17 +17,17 @@ Hooks run from the root of a copy of your repository, so this path resolves rela ## 2. Read the run context -Pipelines passes information to the hook through environment variables. Context values about the run are in the `PIPELINES_HOOKS_CTX_*` namespace, and paths to input files are in the `PIPELINES_HOOKS_IN_*` namespace. +Pipelines passes information to the hook through environment variables. Context values about the run are in the `PIPELINES_HOOK_CTX_*` namespace, and paths to input files are in the `PIPELINES_HOOK_IN_*` namespace. Read the actor and action from the context, and the path to the units file from the inputs: ```bash -actor="$PIPELINES_HOOKS_CTX_ACTOR" -action="$PIPELINES_HOOKS_CTX_ACTION" -units_file="$PIPELINES_HOOKS_IN_UNITS_JSON_FILE" +actor="$PIPELINES_HOOK_CTX_ACTOR" +action="$PIPELINES_HOOK_CTX_ACTION" +units_file="$PIPELINES_HOOK_IN_UNITS_JSON_FILE" ``` -`PIPELINES_HOOKS_IN_UNITS_JSON_FILE` points at a JSON array of the units in the run, each with its path and (when one exists) the path to its plan JSON. For example, list the affected unit paths with `jq`: +`PIPELINES_HOOK_IN_UNITS_JSON_FILE` points at a JSON array of the units in the run, each with its path and (when one exists) the path to its plan JSON. For example, list the affected unit paths with `jq`: ```bash jq -r '.[].path' "$units_file" @@ -37,17 +37,17 @@ For the complete list of context variables and input files, see the [Hooks API]( ## 3. Write the comment -A hook returns information by writing to the files named in the `PIPELINES_HOOKS_OUT_*` namespace. Build a comment listing the affected units and write it to the comment file: +A hook returns information by writing to the files named in the `PIPELINES_HOOK_OUT_*` namespace. Build a comment listing the affected units and write it to the comment file: ```bash { echo "$action triggered by @$actor affected:" echo jq -r '.[] | "- \(.path)"' "$units_file" -} > "$PIPELINES_HOOKS_OUT_COMMENT_FILE" +} > "$PIPELINES_HOOK_OUT_COMMENT_FILE" ``` -Writing outputs is optional. This hook only posts a comment, so it does not write a result file: when a hook writes nothing to `PIPELINES_HOOKS_OUT_RESULT_FILE` and exits `0`, Pipelines defaults its result to `pass`. To flag a problem instead, write `warn` or `deny` to that file. The result is advisory and surfaces in the comment, but does not by itself fail the run. [How results and comments appear](#how-results-and-comments-appear) below covers how each one renders on the request. +Writing outputs is optional. This hook only posts a comment, so it does not write a result file: when a hook writes nothing to `PIPELINES_HOOK_OUT_RESULT_FILE` and exits `0`, Pipelines defaults its result to `pass`. To flag a problem instead, write `warn` or `deny` to that file. The result is advisory and surfaces in the comment, but does not by itself fail the run. [How results and comments appear](#how-results-and-comments-appear) below covers how each one renders on the request. The complete script: @@ -55,15 +55,15 @@ The complete script: #!/usr/bin/env bash set -euo pipefail -actor="$PIPELINES_HOOKS_CTX_ACTOR" -action="$PIPELINES_HOOKS_CTX_ACTION" -units_file="$PIPELINES_HOOKS_IN_UNITS_JSON_FILE" +actor="$PIPELINES_HOOK_CTX_ACTOR" +action="$PIPELINES_HOOK_CTX_ACTION" +units_file="$PIPELINES_HOOK_IN_UNITS_JSON_FILE" { echo "$action triggered by @$actor affected:" echo jq -r '.[] | "- \(.path)"' "$units_file" -} > "$PIPELINES_HOOKS_OUT_COMMENT_FILE" +} > "$PIPELINES_HOOK_OUT_COMMENT_FILE" ``` ## 4. Make the script executable @@ -122,8 +122,8 @@ The overall comment reflects the most severe hook outcome, so a `warn` or `deny` The two text outputs serve different purposes: -- **Summary** (`PIPELINES_HOOKS_OUT_SUMMARY_FILE`) appears inline next to the title, after a colon, for example `⚠️ Affected Units: 3 units changed`. Use it for a short, at-a-glance headline. -- **Comment** (`PIPELINES_HOOKS_OUT_COMMENT_FILE`) is the body of the collapsible section, rendered as HTML. Use it for detailed output such as a table, a list, or links. +- **Summary** (`PIPELINES_HOOK_OUT_SUMMARY_FILE`) appears inline next to the title, after a colon, for example `⚠️ Affected Units: 3 units changed`. Use it for a short, at-a-glance headline. +- **Comment** (`PIPELINES_HOOK_OUT_COMMENT_FILE`) is the body of the collapsible section, rendered as HTML. Use it for detailed output such as a table, a list, or links. If the hook writes no comment, Pipelines shows a fallback line in the body, such as `Hook exited with code 0`. The hook from this guide writes a `pass` result and a comment, so it appears with a ✅ icon and its unit list in the body. diff --git a/docs/2.0/reference/pipelines/configurations-as-code/api.mdx b/docs/2.0/reference/pipelines/configurations-as-code/api.mdx index abd1511acd..f7ce46416f 100644 --- a/docs/2.0/reference/pipelines/configurations-as-code/api.mdx +++ b/docs/2.0/reference/pipelines/configurations-as-code/api.mdx @@ -411,7 +411,7 @@ The command Pipelines runs for the hook, written as a list where the first eleme Run an inline shell snippet (note the `bash -c` so the variable is expanded by the shell): ```hcl -execute = ["bash", "-c", "echo \"Triggered by $PIPELINES_HOOKS_CTX_ACTOR\""] +execute = ["bash", "-c", "echo \"Triggered by $PIPELINES_HOOK_CTX_ACTOR\""] ``` Or invoke an executable script directly by its path: diff --git a/docs/2.0/reference/pipelines/hooks-api.md b/docs/2.0/reference/pipelines/hooks-api.md index 63e476dfcd..a62ebdff21 100644 --- a/docs/2.0/reference/pipelines/hooks-api.md +++ b/docs/2.0/reference/pipelines/hooks-api.md @@ -2,59 +2,59 @@ Pipelines communicates with a hook entirely through environment variables. There are three namespaces: -- **`PIPELINES_HOOKS_CTX_*`**: context values about the run, set directly as the variable's value. -- **`PIPELINES_HOOKS_IN_*`**: paths to input files the hook reads. -- **`PIPELINES_HOOKS_OUT_*`**: paths to output files the hook writes. +- **`PIPELINES_HOOK_CTX_*`**: context values about the run, set directly as the variable's value. +- **`PIPELINES_HOOK_IN_*`**: paths to input files the hook reads. +- **`PIPELINES_HOOK_OUT_*`**: paths to output files the hook writes. The hook's `execute` command reads from the first two namespaces and writes to the third. -## Context inputs (`PIPELINES_HOOKS_CTX_*`) +## Context inputs (`PIPELINES_HOOK_CTX_*`) Scalar facts about the run, set directly as the variable's value. -A context variable that does not apply to the run is left unset, rather than set to an empty string. A hook can therefore tell "not applicable" apart from "set but blank", for example with `[ -z "${PIPELINES_HOOKS_CTX_ACTION+x}" ]` in bash. +A context variable that does not apply to the run is left unset, rather than set to an empty string. A hook can therefore tell "not applicable" apart from "set but blank", for example with `[ -z "${PIPELINES_HOOK_CTX_ACTION+x}" ]` in bash. ### Always set | Variable | Description | |---|---| -| `PIPELINES_HOOKS_CTX_CI_PLATFORM` | The CI platform running the hook: `github` or `gitlab`. | -| `PIPELINES_HOOKS_CTX_ORGANIZATION` | The organization (GitHub) or group (GitLab) that owns the repository. | -| `PIPELINES_HOOKS_CTX_REPOSITORY` | The repository name. | -| `PIPELINES_HOOKS_CTX_ACTOR` | The user that triggered the run. | -| `PIPELINES_HOOKS_CTX_GIT_REF` | The git ref that triggered the run. | -| `PIPELINES_HOOKS_CTX_GIT_HASH` | The commit SHA the run is operating on. | +| `PIPELINES_HOOK_CTX_CI_PLATFORM` | The CI platform running the hook: `github` or `gitlab`. | +| `PIPELINES_HOOK_CTX_ORGANIZATION` | The organization (GitHub) or group (GitLab) that owns the repository. | +| `PIPELINES_HOOK_CTX_REPOSITORY` | The repository name. | +| `PIPELINES_HOOK_CTX_ACTOR` | The user that triggered the run. | +| `PIPELINES_HOOK_CTX_GIT_REF` | The git ref that triggered the run. | +| `PIPELINES_HOOK_CTX_GIT_HASH` | The commit SHA the run is operating on. | ### Set when applicable | Variable | Description | |---|---| -| `PIPELINES_HOOKS_CTX_ACTION` | The command the hook ran after: `plan` or `apply`. A destroy is reported as `apply`. | -| `PIPELINES_HOOKS_CTX_ACTION_STATUS` | The outcome of that command: `succeeded` or `failed`. | -| `PIPELINES_HOOKS_CTX_CHANGE_REQUEST_NUMBER` | The pull/merge request number. | -| `PIPELINES_HOOKS_CTX_CHANGE_REQUEST_URL` | The pull/merge request URL. | -| `PIPELINES_HOOKS_CTX_CHANGE_REQUEST_BRANCH` | The source branch of the pull/merge request. | +| `PIPELINES_HOOK_CTX_ACTION` | The command the hook ran after: `plan` or `apply`. A destroy is reported as `apply`. | +| `PIPELINES_HOOK_CTX_ACTION_STATUS` | The outcome of that command: `succeeded` or `failed`. | +| `PIPELINES_HOOK_CTX_CHANGE_REQUEST_NUMBER` | The pull/merge request number. | +| `PIPELINES_HOOK_CTX_CHANGE_REQUEST_URL` | The pull/merge request URL. | +| `PIPELINES_HOOK_CTX_CHANGE_REQUEST_BRANCH` | The source branch of the pull/merge request. | The three `CHANGE_REQUEST` variables are set or absent together: they are set when the run is associated with a pull/merge request, and absent for a push to a deploy branch. -## File inputs (`PIPELINES_HOOKS_IN_*`) +## File inputs (`PIPELINES_HOOK_IN_*`) Paths to files the hook reads. Both are always set. | Variable | Description | |---|---| -| `PIPELINES_HOOKS_IN_PLANS_JSON_DIR` | Directory containing the decrypted plan JSON for the run's units. | -| `PIPELINES_HOOKS_IN_UNITS_JSON_FILE` | Path to a JSON file describing the units in the run. | +| `PIPELINES_HOOK_IN_PLAN_JSON_DIR` | Directory containing the decrypted plan JSON for the run's units. | +| `PIPELINES_HOOK_IN_UNITS_JSON_FILE` | Path to a JSON file describing the units in the run. | ### Plans JSON directory -Within `PIPELINES_HOOKS_IN_PLANS_JSON_DIR`, each unit's plan is stored at `/tfplan.json`, where `` is the unit's path relative to the repository root. Only units that produced a plan have a file, so the directory may not contain an entry for every unit in the run. +Within `PIPELINES_HOOK_IN_PLAN_JSON_DIR`, each unit's plan is stored at `/tfplan.json`, where `` is the unit's path relative to the repository root. Only units that produced a plan have a file, so the directory may not contain an entry for every unit in the run. The file is the JSON form of the OpenTofu/Terraform plan (`terraform show -json`). ### Units JSON file -`PIPELINES_HOOKS_IN_UNITS_JSON_FILE` points to a JSON array describing each unit the hook applies to: +`PIPELINES_HOOK_IN_UNITS_JSON_FILE` points to a JSON array describing each unit the hook applies to: ```json [ @@ -73,21 +73,21 @@ The file is the JSON form of the OpenTofu/Terraform plan (`terraform show -json` | `path` | The unit's path relative to the repository root. | | `plan_json_file` | Absolute path to the unit's decrypted plan JSON, the same file addressed under the plans directory above. Omitted when the unit produced no plan. | -## Outputs (`PIPELINES_HOOKS_OUT_*`) +## Outputs (`PIPELINES_HOOK_OUT_*`) Paths to files the hook may write. All are always set. Pipelines reads them back only when the hook process exits `0`; if the hook exits non-zero the output files are ignored. | Variable | Description | |---|---| -| `PIPELINES_HOOKS_OUT_RESULT_FILE` | Write the hook's result: `pass`, `warn`, or `deny`. | -| `PIPELINES_HOOKS_OUT_SUMMARY_FILE` | Write a short summary of the hook's outcome. | -| `PIPELINES_HOOKS_OUT_COMMENT_FILE` | Write a comment body to surface on the pull/merge request. | +| `PIPELINES_HOOK_OUT_RESULT_FILE` | Write the hook's result: `pass`, `warn`, or `deny`. | +| `PIPELINES_HOOK_OUT_SUMMARY_FILE` | Write a short summary of the hook's outcome. | +| `PIPELINES_HOOK_OUT_COMMENT_FILE` | Write a comment body to surface on the pull/merge request. | Writing to these files is optional. A hook that writes nothing reports a `pass` with no summary or comment. ### Results -The result written to `PIPELINES_HOOKS_OUT_RESULT_FILE` is one of: +The result written to `PIPELINES_HOOK_OUT_RESULT_FILE` is one of: | Result | Meaning | |---|---| From e7daabfae62f4fd0f0023cdfa2590ad2f9cc189c Mon Sep 17 00:00:00 2001 From: Lewis Christie Date: Fri, 19 Jun 2026 11:40:46 -0600 Subject: [PATCH 6/7] Apply review suggestions --- docs/2.0/docs/pipelines/guides/hooks/configuring.md | 12 ++++++------ .../pipelines/configurations-as-code/api.mdx | 2 +- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/docs/2.0/docs/pipelines/guides/hooks/configuring.md b/docs/2.0/docs/pipelines/guides/hooks/configuring.md index 2551c6bfcf..c8bc4e3ac4 100644 --- a/docs/2.0/docs/pipelines/guides/hooks/configuring.md +++ b/docs/2.0/docs/pipelines/guides/hooks/configuring.md @@ -51,6 +51,12 @@ A run executes a single command, either `plan` or `apply`, and only hooks whose A destroy is treated as an `apply` for this purpose, so a hook configured with `commands = ["apply"]` also runs after a destroy. +### Isolated working directory + +Each hook runs in its own temporary copy of the repository, with that copy as its working directory. This is why an `execute` path like `.gruntwork/hooks/affected-units.sh` resolves relative to the repository root. + +Any changes a hook makes to files are not persisted. The copy is discarded once the hook finishes, so edits are never committed, pushed, or seen by the rest of the run. Because each hook gets its own fresh copy, hooks also do not see file changes made by other hooks. + ### Exit codes A hook's exit code is how it tells Pipelines whether it succeeded: @@ -79,12 +85,6 @@ When a hook is cancelled, Pipelines signals the hook's process group to terminat A cancelled hook counts as a failure: it fails the run and, like any failure, causes later hooks without `run_on_error = true` to be skipped. -### Isolated working directory - -Each hook runs in its own temporary copy of the repository, with that copy as its working directory. This is why an `execute` path like `.gruntwork/hooks/affected-units.sh` resolves relative to the repository root. - -Any changes a hook makes to files are not persisted. The copy is discarded once the hook finishes, so edits are never committed, pushed, or seen by the rest of the run. Because each hook gets its own fresh copy, hooks also do not see file changes made by other hooks. - ### Inputs and outputs Pipelines passes information to a hook through environment variables, and a hook returns information by writing to files whose paths Pipelines provides. See [Hooks API](/2.0/reference/pipelines/hooks-api) for the full contract. diff --git a/docs/2.0/reference/pipelines/configurations-as-code/api.mdx b/docs/2.0/reference/pipelines/configurations-as-code/api.mdx index f7ce46416f..881a39b0b8 100644 --- a/docs/2.0/reference/pipelines/configurations-as-code/api.mdx +++ b/docs/2.0/reference/pipelines/configurations-as-code/api.mdx @@ -125,7 +125,7 @@ Hooks are an Enterprise-only feature. :::caution -When any `after_hook` block is configured, the `PIPELINES_PLAN_ENCRYPTION_KEY` secret must be set. Pipelines will fail preflight checks if hooks are declared and this secret is missing. +When any `after_hook` block is configured, the `PIPELINES_PLAN_ENCRYPTION_KEY` secret must be set. Pipelines will fail preflight checks if hooks are declared and this secret is missing. See [Plan encryption key](/2.0/docs/pipelines/guides/hooks/setup#plan-encryption-key) for how to generate and configure it. ::: From 587e74135fd020a28b532df53fbbe21ba28f67b9 Mon Sep 17 00:00:00 2001 From: Lewis Christie Date: Fri, 19 Jun 2026 12:47:17 -0600 Subject: [PATCH 7/7] Document deny blocking pipeline --- .../2.0/docs/pipelines/guides/hooks/configuring.md | 2 +- .../guides/hooks/slack-deploy-notification.md | 2 +- .../docs/pipelines/guides/hooks/writing-a-hook.md | 14 +++++++------- docs/2.0/reference/pipelines/hooks-api.md | 4 ++-- 4 files changed, 11 insertions(+), 11 deletions(-) diff --git a/docs/2.0/docs/pipelines/guides/hooks/configuring.md b/docs/2.0/docs/pipelines/guides/hooks/configuring.md index c8bc4e3ac4..afc28bdc03 100644 --- a/docs/2.0/docs/pipelines/guides/hooks/configuring.md +++ b/docs/2.0/docs/pipelines/guides/hooks/configuring.md @@ -64,7 +64,7 @@ A hook's exit code is how it tells Pipelines whether it succeeded: - **Exit `0`** means the hook succeeded. Pipelines reads back its output files (result, summary, and comment). - **Any non-zero exit** means the hook failed. **A failed hook fails the entire pipeline run**, exactly as a failed `plan` or `apply` does, and Pipelines ignores the hook's output files. -Only the exit code decides success or failure. The result a hook writes (`pass`, `warn`, or `deny`) is advisory: it surfaces in the comment but never fails the run on its own, so a hook that exits `0` succeeds even when it reports `deny`. See [Hooks API](/2.0/reference/pipelines/hooks-api) for the result values. +The exit code is not the only thing that can fail the run. When a hook exits `0`, Pipelines reads the result it wrote (`pass`, `warn`, or `deny`) and surfaces it in the comment. A `deny` result fails the pipeline run and blocks the pull/merge request from merging. `warn` is advisory and does not affect the run, and `pass` (or an empty or unrecognized value) has no effect. See [Hooks API](/2.0/reference/pipelines/hooks-api) for the result values. ### Skipping after a failure diff --git a/docs/2.0/docs/pipelines/guides/hooks/slack-deploy-notification.md b/docs/2.0/docs/pipelines/guides/hooks/slack-deploy-notification.md index ff079bc744..a661621ce7 100644 --- a/docs/2.0/docs/pipelines/guides/hooks/slack-deploy-notification.md +++ b/docs/2.0/docs/pipelines/guides/hooks/slack-deploy-notification.md @@ -101,7 +101,7 @@ curl --fail --silent --show-error \ "$webhook_url" ``` -This hook writes no output files, so Pipelines reports it as a `pass`. (Heads up: with `set -euo pipefail` and curl's `--fail`, a failed secret lookup or Slack post exits non-zero and fails the run; handle those cases in your script if you want it to pass regardless.) To also leave a comment on the pull/merge request, see [Writing a Hook](/2.0/docs/pipelines/guides/hooks/writing-a-hook). +This hook writes no output files, so Pipelines reports it as a `pass`. (Heads up: with `set -euo pipefail` and curl's `--fail`, a failed secret lookup or Slack post exits non-zero and fails the run; handle those cases in your script if you want it to pass regardless.) To also add content to the Pipelines comment on the pull/merge request, see [Writing a Hook](/2.0/docs/pipelines/guides/hooks/writing-a-hook). ## What you'll see diff --git a/docs/2.0/docs/pipelines/guides/hooks/writing-a-hook.md b/docs/2.0/docs/pipelines/guides/hooks/writing-a-hook.md index d808d2898b..7208ef2ad8 100644 --- a/docs/2.0/docs/pipelines/guides/hooks/writing-a-hook.md +++ b/docs/2.0/docs/pipelines/guides/hooks/writing-a-hook.md @@ -1,6 +1,6 @@ # Writing a Hook -This guide builds a hook from scratch: a small bash script that reads context about the run, inspects the units that were planned, and posts a comment back on the pull/merge request. Along the way it covers the full loop a hook goes through, reading inputs from the environment and writing outputs to files. +This guide builds a hook from scratch: a small bash script that reads context about the run, inspects the units that were planned, and adds a list of them to the Pipelines comment on the pull/merge request. Along the way it covers the full loop a hook goes through, reading inputs from the environment and writing outputs to files. Before you start, make sure hooks are set up for your repository. See [Setup & Prerequisites](/2.0/docs/pipelines/guides/hooks/setup). @@ -37,7 +37,7 @@ For the complete list of context variables and input files, see the [Hooks API]( ## 3. Write the comment -A hook returns information by writing to the files named in the `PIPELINES_HOOK_OUT_*` namespace. Build a comment listing the affected units and write it to the comment file: +A hook returns information by writing to the files named in the `PIPELINES_HOOK_OUT_*` namespace. The comment file holds HTML that Pipelines adds to the comment on the pull/merge request, in this hook's section. Build the list of affected units and write it to the comment file: ```bash { @@ -47,7 +47,7 @@ A hook returns information by writing to the files named in the `PIPELINES_HOOK_ } > "$PIPELINES_HOOK_OUT_COMMENT_FILE" ``` -Writing outputs is optional. This hook only posts a comment, so it does not write a result file: when a hook writes nothing to `PIPELINES_HOOK_OUT_RESULT_FILE` and exits `0`, Pipelines defaults its result to `pass`. To flag a problem instead, write `warn` or `deny` to that file. The result is advisory and surfaces in the comment, but does not by itself fail the run. [How results and comments appear](#how-results-and-comments-appear) below covers how each one renders on the request. +Writing outputs is optional. This hook only writes comment content, so it does not write a result file: when a hook writes nothing to `PIPELINES_HOOK_OUT_RESULT_FILE` and exits `0`, Pipelines defaults its result to `pass`. To flag a problem instead, write `warn` or `deny` to that file. Both surface in the comment; `warn` is advisory, while `deny` fails the run. [How results and comments appear](#how-results-and-comments-appear) below covers how each one renders on the request. The complete script: @@ -93,7 +93,7 @@ See [Configuring Hooks](/2.0/docs/pipelines/guides/hooks/configuring) for every ## 6. Run the hook -Commit the script and configuration, then open a pull/merge request that changes at least one unit. Pipelines runs the hook after the `plan`, and the comment your hook wrote appears on the request alongside the plan output. +Commit the script and configuration, then open a pull/merge request that changes at least one unit. Pipelines runs the hook after the `plan`, and the content your hook wrote appears in the Pipelines comment on the request alongside the plan output. ![Hook comment on a pull request](/img/pipelines/guides/affected-units-comment.png) @@ -111,7 +111,7 @@ The section is titled by the hook's `name`, or its block label when `name` is un |---|---| | `pass` result | ✅ | | `warn` result | ⚠️ | -| `deny` result | ❌ | +| `deny` result | ⛔️ | | Failed (non-zero exit) | ❌ | | Timed out | ❌ | | Skipped | ⏭️ | @@ -127,9 +127,9 @@ The two text outputs serve different purposes: If the hook writes no comment, Pipelines shows a fallback line in the body, such as `Hook exited with code 0`. The hook from this guide writes a `pass` result and a comment, so it appears with a ✅ icon and its unit list in the body. -### Results do not fail the run +### How results affect the run -A `deny` shows a ❌ and raises the severity shown in the comment, but it does not fail the run. Only a non-zero exit fails the run; see [Exit codes](/2.0/docs/pipelines/guides/hooks/configuring#exit-codes). +A `deny` result fails the pipeline run and blocks the pull/merge request from merging, and shows a ⛔️ in the comment. A `warn` shows a ⚠️ and raises the severity in the comment but does not fail the run. Results are only read when the hook exits `0`; a non-zero exit is always a failure, see [Exit codes](/2.0/docs/pipelines/guides/hooks/configuring#exit-codes). ### Skipped hooks diff --git a/docs/2.0/reference/pipelines/hooks-api.md b/docs/2.0/reference/pipelines/hooks-api.md index a62ebdff21..c15cee7c69 100644 --- a/docs/2.0/reference/pipelines/hooks-api.md +++ b/docs/2.0/reference/pipelines/hooks-api.md @@ -93,8 +93,8 @@ The result written to `PIPELINES_HOOK_OUT_RESULT_FILE` is one of: |---|---| | `pass` | The default result. | | `warn` | Advisory warning. | -| `deny` | Advisory rejection. | +| `deny` | Rejection. Fails the pipeline run. | -The result is an advisory severity surfaced in the pull/merge request comment; it does not by itself change whether the run succeeds. In particular, `deny` does not fail the run. An empty or unrecognized value is treated as `pass`. +The result is a severity surfaced in the pull/merge request comment. `deny` fails the pipeline run and blocks the pull/merge request from merging; `warn` is advisory and does not affect the run; `pass` produces no failure. An empty or unrecognized value is treated as `pass`. For how the result, summary, and comment appear on the pull/merge request, see [How results and comments appear](/2.0/docs/pipelines/guides/hooks/writing-a-hook#how-results-and-comments-appear).