-
Notifications
You must be signed in to change notification settings - Fork 41
Add hooks documentation #3180
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
Resonance1584
wants to merge
7
commits into
main
Choose a base branch
from
2026-06-17_after-hooks
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Add hooks documentation #3180
Changes from 5 commits
Commits
Show all changes
7 commits
Select commit
Hold shift + click to select a range
f3ecf6b
Add hooks documentation
Resonance1584 481f8d1
Fix broken link
Resonance1584 0583a3b
Tidy
Resonance1584 046488f
Apply suggestions from code review
Resonance1584 36333cd
Rename hook env vars
Resonance1584 e7daabf
Apply review suggestions
Resonance1584 587e741
Document deny blocking pipeline
Resonance1584 File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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: | ||
|
|
||
| <Tabs groupId="cloud-provider"> | ||
| <TabItem value="aws" label="AWS" default> | ||
|
|
||
| ```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" | ||
| } | ||
| } | ||
| } | ||
| } | ||
| ``` | ||
|
|
||
| </TabItem> | ||
| <TabItem value="azure" label="Azure"> | ||
|
|
||
| ```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" | ||
| } | ||
| } | ||
| } | ||
| } | ||
| ``` | ||
|
|
||
| </TabItem> | ||
| <TabItem value="gcp" label="GCP"> | ||
|
|
||
| ```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" | ||
| } | ||
| } | ||
| } | ||
| } | ||
| ``` | ||
|
|
||
| </TabItem> | ||
| <TabItem value="custom" label="Custom"> | ||
|
|
||
| ```hcl | ||
| repository { | ||
| after_hook "inspect_resources" { | ||
| commands = ["plan"] | ||
| execute = [".gruntwork/hooks/inspect-resources.sh"] | ||
|
|
||
| authentication { | ||
| custom { | ||
| auth_provider_cmd = "./scripts/auth-provider.sh" | ||
| } | ||
| } | ||
| } | ||
| } | ||
| ``` | ||
|
|
||
| </TabItem> | ||
| </Tabs> | ||
|
|
||
| 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 | ||
|
|
||
| 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 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 | ||
|
|
||
| - [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.md) |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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 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. | ||
|
|
||
| ### 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. | ||
|
Resonance1584 marked this conversation as resolved.
Outdated
|
||
|
|
||
| ### 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, 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. | ||
|
|
||
| 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, 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. | ||
|
Resonance1584 marked this conversation as resolved.
Outdated
|
||
|
|
||
| ### 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) | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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). *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 | ||
|
|
||
| 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. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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 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. | ||
|
|
||
| ### 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`. | ||
|
|
||
| <Tabs groupId="platform"> | ||
| <TabItem value="github" label="GitHub" default> | ||
|
|
||
| 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 }} | ||
| ``` | ||
|
|
||
| </TabItem> | ||
| <TabItem value="gitlab" label="GitLab"> | ||
|
|
||
| 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. | ||
|
|
||
| </TabItem> | ||
| </Tabs> | ||
|
|
||
| ## Next steps | ||
|
|
||
| - [Configuring Hooks](/2.0/docs/pipelines/guides/hooks/configuring) |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It may be best to leave out the Required and optional sections here since we link to the full spec below and can have a single source of truth for the documentation.
We should still have the text explaining the label, commands and execute meaning.