Skip to content

Tell a chat channel when a wave stops

llms.txtlists every page for an agent
Optional: hand this page to your coding agentThe steps work by hand too.
Show the whole prompt
Read https://intentius.io/terragucci/guides/notify-a-chat-channel/.
Add a `notify:` block to terragucci.yml naming the secret SLACK_WEBHOOK_URL (or TEAMS_WEBHOOK_URL), run
`npx terragucci config check` and `npx terragucci init`, and open a pull request. Tell me which secret I must create.
Do not create the webhook or the secret, and never write a webhook address into any file.
Never apply, approve (a pull request review or `terragucci approve`), override a policy denial (`terragucci override`), use `--mode apply`, or merge; never touch `.chant/allowed_signers` or `chant/lifecycle`.

A message in your channel each time an apply job stops at a wave or the drift job finds a change. The message approves nothing; a person approves with terragucci approve from a checkout, which records the approval on chant/lifecycle. Under approval: pr-review an approving review of the pull request counts too.

You want You set up
a notice in Slack or Teams, and under approval: pr-review a link straight to the review that approves the wave notify: and an incoming webhook for the channel. Nothing to install or host.
Approve and Decline buttons that record the approval on chant/lifecycle as the person who clicked terragucci relay, in your own cloud (Approve from Slack and Teams)
The wave Exit The message says
waits for an approval 3 the wave, its roots, its plan digest, the terragucci approve command for that digest, and the run; under approval: pr-review, first a link to review the pull request (below)
is refused: its plans changed after approval 4 the wave, its roots, the outcome line, npx terragucci approve wave-<k> --plan <digest> for the new digest, and the run
fails, or the policy denies it 1 the wave, its roots, the outcome line, that there is nothing to approve, and the run
You need Why
The pipeline init maps the secret into the apply jobs
An incoming webhook for the channel the apply job posts to it
A secret in your forge’s store holding the webhook’s address the address is a credential: anyone holding it can post
  1. Make a webhook for the channel.

    Turn on Incoming Webhooks in a Slack app and add a webhook to the channel. Copy its https://hooks.slack.com/services/... address.

  2. Store the address as a secret.

    A repository secret under Settings, Secrets and variables, Actions.

  3. Name the secret in terragucci.yml. Use either key, or both:

    notify:
    slack: SLACK_WEBHOOK_URL
    teams: TEAMS_WEBHOOK_URL

    The value is the secret’s name. config check refuses an address.

  4. Write the pipeline again and merge it.

    Terminal window
    npx terragucci init

    The apply jobs get the secret as TERRAGUCCI_SLACK_WEBHOOK or TERRAGUCCI_TEAMS_WEBHOOK, and each runs terragucci notify when its wave stops. With a drift schedule, so does the drift job; no plan job gets it.

  5. Read the next message.

    terragucci: wave 2 of github.com/acme/infra waits for an approval
    Roots: envs/prod/app, envs/prod/db
    Digest: jcs1-sha256:9f2c...
    Approve: terragucci approve wave-2 --plan jcs1-sha256:9f2c...
    Run: https://github.com/acme/infra/actions/runs/42
If the webhook The job
answers 2xx logs terragucci notify: posted to Slack (or Teams)
answers an error, or not within 10 seconds logs the status and goes on; the wave’s own exit code stands

A comment that applies (/terragucci apply) posts the same messages from its job.

With a drift schedule, the drift job posts to Slack and Teams when its refresh-only plans find drift, or a root cannot be refreshed. A run that finds nothing posts nothing.

terragucci: drift in github.com/acme/infra: 1 root changed outside Terraform
Drifted: envs/prod/app
Run: https://github.com/acme/infra/actions/runs/57

The Re-plan button opens the page where you run the check again under your own login; it holds no token and starts nothing itself.

Forge Re-plan opens
GitHub the workflow’s page, where Run workflow is
Forgejo the workflow’s runs, where Run workflow is
GitLab the pipeline schedules, where each schedule has a run button

The generic webhook gets no drift event: its events are a wave’s.

With approval: pr-review, a waiting wave’s message opens with a link to the pull request’s review page when an approving review of its head would approve the wave. The approval is still a person’s own review on the forge, and it binds the plans the pull request’s note recorded. The chat app holds no key and approves nothing.

terragucci: wave 2 of github.com/acme/infra waits for an approval
Review and approve: pull request 12, then run the wave again
Roots: envs/prod/app, envs/prod/db
Digest: jcs1-sha256:9f2c...
Or approve: terragucci approve wave-2 --plan jcs1-sha256:9f2c...
Run: https://github.com/acme/infra/actions/runs/42

In Teams, the card’s first button (Review and approve) opens the same page.

Forge The link opens Given when
GitHub the pull request’s Files changed tab, where Review changes > Approve is the wave’s pull request has no approving review of its head, or a reviewer asked for changes, and its plan note has a digest for the wave
Forgejo the pull request’s Files changed tab, where Review > Approve is the same
GitLab the merge request, where Approve is the same, and the merge request is still open (applied before it merges): GitLab takes no approval of a merged one

A direct push gets no link, and neither does a refused wave or one the pull request’s note has no digest for: no review of that head can approve those plans. The message then offers the approve command alone.

After the review, run the wave again: comment /terragucci apply on the pull request, or re-run the job (Run the stage again). The wave finds the approving review of the head and records it on chant/lifecycle with via: pr-review, then applies.

notify can also post one JSON event to an HTTPS endpoint of yours and sign it with a key you hold. A chat bot, an incident tool or a relay that approves from chat can read it; the chat setup above needs none of this.

  1. Store two secrets in your forge’s store, as in step 2 above: the endpoint’s address, and a random key of 32 bytes or more, for example from openssl rand -hex 32. Give the receiver the same key.

  2. Name both in terragucci.yml. They can sit beside slack and teams.

    notify:
    webhook: TERRAGUCCI_HOOK_URL
    webhook_key: TERRAGUCCI_HOOK_KEY

    config check refuses one without the other: an event is never sent unsigned.

  3. Run npx terragucci init and merge. The apply jobs get the secrets as TERRAGUCCI_WEBHOOK and TERRAGUCCI_WEBHOOK_KEY.

  4. Verify each request before reading it. The body is a terragucci.notify/v1 event.

    Header Holds
    X-Terragucci-Signature sha256= and the hex HMAC-SHA256 of the raw body with the key
    X-Terragucci-Event waiting, refused or failed
    X-Terragucci-Delivery the event’s id, the same for each post of one wave’s event about one digest; drop a repeat
    import { createHmac, timingSafeEqual } from "node:crypto";
    function verified(rawBody, header, key) {
    const want = Buffer.from("sha256=" + createHmac("sha256", key).update(rawBody).digest("hex"));
    const got = Buffer.from(header ?? "");
    return got.length === want.length && timingSafeEqual(got, want);
    }

The event carries the wave’s outcome as the stage wrote it (its fields). It approves nothing, and the receiver’s answer changes nothing: a webhook that fails or is slow is logged like the chat ones.

To approve from a chat click, terragucci relay does it without this event: the buttons carry the wave and its digest, and the relay checks who clicked.

terragucci

These docs count page views and clicks with PostHog. They set no cookies, store nothing in your browser, and send nothing when your browser asks not to be tracked.