Skip to main content
Integrations

Slack alerts for design drift

DeveloperDesigner
Your Slack:

Design of dembrandt.com · Production · deployed 1 min ago · 9a04c7e

+ New accent color · #F59E0B

Footer social icons removed

~ Hero background · #0D0D0D#000000

Design of dembrandt.com · Production · deployed 5 min ago · 2c88e1a

+ New badge color · #22C55E

Design of dembrandt.com · Production · deployed 30 sec ago · 71bf4de

~ Body font size · 1rem0.9375rem

~ H1 font size · 2.25rem2.5rem

Overview

Design drift shows up where your team already looks: Slack. dembrandt --compare catches drift and fails the build, and this recipe posts the report as a normal message in your existing channel, no new tool to check. Works on a PR-preview drift gate or a plain production deploy workflow: you only need a committed baseline and a site URL to compare against it.

  1. 1

    Create a new Slack app

    api.slack.com/apps → Create New App → Blank app. Skip the templates. A one-way webhook needs no slash commands or listeners.

    Slack 'Create new app' dialog with template options
  2. 2

    Name it and pick a workspace

    Name it how you wish. Pick the workspace it should post into.

    Slack 'Create from scratch' dialog with app name and workspace fields
  3. 3

    Turn on Incoming Webhooks

    Open Incoming Webhooks in the app settings. Flip the toggle on, then use Add New Webhook below the curl example to continue.

    Slack Incoming Webhooks settings page with the feature toggled on and the Add New Webhook button
  4. 4

    Authorize it into a channel

    Pick the channel your team actually watches.

    Slack 'Allow app to access Slack' permission screen with channel picker
  5. 5

    Copy the webhook URL

    It's under Webhook URLs for Your Workspace. Store it as a CI secret, SLACK_DRIFT_WEBHOOK_URL.

    Slack Incoming Webhooks page listing the generated webhook URL and a sample curl request
6

Save this as .github/scripts/slack-drift-notify.sh in your repo (next to the drift-gate workflow from the prerequisite recipe) and make it executable: chmod +x .github/scripts/slack-drift-notify.sh

.github/scripts/slack-drift-notify.sh
set -euo pipefail
: "${SLACK_DRIFT_WEBHOOK_URL:?SLACK_DRIFT_WEBHOOK_URL is not set}"
# SITE is whatever URL your workflow just deployed and extracted a baseline
# for: $PREVIEW on a PR-preview gate, or the fixed production URL on a
# production-only workflow. baseline.json comes from the earlier compare step.
SITE="${SITE:?SITE is not set}"
ENVIRONMENT="${ENVIRONMENT:-preview}"
COMMIT="${COMMIT:-$(git rev-parse --short HEAD)}"
ICON_URL="${ICON_URL:-https://www.dembrandt.com/dembrandt-logo-just-symbol.png}"
for bin in jq npx curl; do
command -v "$bin" >/dev/null 2>&1 || { echo "$bin is required but not on PATH" >&2; exit 1; }
done
# dembrandt --compare exits 1 on drift by design (that's what makes it work
# as a CI gate elsewhere in the pipeline). Under set -e that would kill this
# script before STATUS is even read, so the exit code is captured and
# ignored here; only a genuinely unparseable report should still fail loud.
REPORT=$(npx dembrandt "$SITE" --compare baseline.json --json-only) || true
STATUS=$(echo "$REPORT" | jq -r '.drift.status')
if [ "$STATUS" = "drift" ]; then
DEPLOYED_AT=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
ATTACHMENTS=$(echo "$REPORT" | jq '[.drift.changes[] | {
color: (if .kind == "added" then "#22C55E" elif .kind == "removed" then "#EA580C" else "#8A8F98" end),
text: ((if .kind == "added" then "+" elif .kind == "removed" then "-" else "~" end) + " " + .label +
(if .before and .after then " · " + .before + " → " + .after elif .after then " · " + .after else "" end))
}]')
PAYLOAD=$(jq -n \
--argjson attachments "$ATTACHMENTS" \
--arg site "$SITE" \
--arg environment "$ENVIRONMENT" \
--arg commit "$COMMIT" \
--arg deployedAt "$DEPLOYED_AT" \
--arg iconUrl "$ICON_URL" \
'{
username: "Dembrandt",
icon_url: $iconUrl,
text: "*Design of \($site)* · \($environment) · \($deployedAt) · \($commit)",
attachments: $attachments
}')
# One retry after a short backoff. A single Slack hiccup shouldn't fail
# the whole CI run when the drift itself was real and worth reporting.
attempt=1
while true; do
RESPONSE=$(curl -s -w '\n%{http_code}' -X POST \
-H 'Content-type: application/json' --data "$PAYLOAD" "$SLACK_DRIFT_WEBHOOK_URL")
HTTP_STATUS=$(echo "$RESPONSE" | tail -n1)
BODY=$(echo "$RESPONSE" | sed '$d')
if [ "$HTTP_STATUS" = "200" ]; then break; fi
if [ "$attempt" -ge 2 ]; then
echo "Slack notification failed (HTTP $HTTP_STATUS): $BODY" >&2
exit 1
fi
echo "Slack notification attempt $attempt failed (HTTP $HTTP_STATUS), retrying..." >&2
attempt=$((attempt + 1))
sleep 2
done
fi
.github/workflows/drift.yml: add this step after the existing compare step
- run: npx dembrandt "$PREVIEW" --compare baseline.json --html report.html
# New step: posts to Slack only when the step above found drift.
# SITE is $PREVIEW on a PR-preview gate; use your fixed site URL instead
# on a production-only workflow.
- name: Notify Slack on drift
env:
SLACK_DRIFT_WEBHOOK_URL: ${{ secrets.SLACK_DRIFT_WEBHOOK_URL }}
SITE: ${{ env.PREVIEW }}
run: .github/scripts/slack-drift-notify.sh
Output

One Slack message per deploy when drift is detected. Silent when the site matches baseline.