- Nx 22.7 monorepo (pnpm 11.1, TypeScript 5.9, Node 24) - apps/api: NestJS 11 (CJS conforme CODING-RULES.md PGD-DB-004) - apps/web: React 19 + Vite 8 (ESM) - libs/shared/api-interface: Zod contract base - Docker Compose dev: Postgres 18, Valkey 8, MinIO, Mailpit - WDS artifacts: - design-artifacts/A-Product-Brief/ (5 docs canônicos + 16 dialogs) - design-artifacts/B-Trigger-Map/ (hub + 4 personas + feature impact) - Stack canon: STACK.md v2.2 + CODING-RULES.md v2.0 + brand.md - AGENTS.md + README.md como entrada para devs/agentes Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
13 KiB
Pact Broker Webhooks (PactFlow → GitHub)
Principle
Configure PactFlow webhooks to trigger provider verification in GitHub Actions via a dedicated GitHub machine user, a long-lived classic Personal Access Token (PAT), and a PactFlow-stored secret. Monitor for silent webhook failures so an expired/revoked token does not quietly block deployments for days.
Rationale
Why webhooks matter
- PactFlow's
contract_requiring_verification_publishedwebhook is the mechanism that notifies a provider repo (viarepository_dispatch) that a consumer has published a contract needing verification. - Without a working webhook,
can-i-deployin the consumer CI times out (900s) and eventually fails withThere is no verified pact between <consumer-version> and the version of <provider> currently in <env>— even though nothing is wrong in either codebase. - Webhook failures are silent by default: PactFlow keeps emitting requests, GitHub keeps returning
401 Unauthorized, but nothing alerts the team until a PR is blocked.
Why a dedicated GitHub machine user (not a personal PAT)
- Personal PATs die when the person leaves the company, rotates laptops, or revokes credentials during a security review. The contract test pipeline then breaks for reasons unrelated to any code change.
- A dedicated machine user (e.g.,
pactflow-<org>) is owned by the org, has only the repos it needs, and the PAT lifecycle is controlled by the security/platform team. - GitHub billing does not count machine users added as outside collaborators to the specific repos they need — confirm with the org owner before assuming it's free.
Why classic PAT with repo scope and no expiration
- PactFlow's webhook calls the GitHub REST API's
repository_dispatchendpoint. This endpoint requires thereposcope on a classic PAT (fine-grained PATs work for many flows but have edge cases withrepository_dispatchthat are not universally supported at time of writing — verify with current GitHub docs). - Classic PATs support "No expiration" — required to avoid the silent-failure trap every 90 days. GitHub warns against this for human users; for a locked-down machine-user PAT stored in PactFlow's secret vault, the security trade-off is documented and accepted.
- The alternative — rotating a PAT every 30/60/90 days — requires tooling and coordination most teams don't yet have. Long-lived + monitored + machine-user-owned is the pragmatic default.
Pattern Examples
Example 1: Webhook URL, Headers, and Body
{
"description": "Notify <provider-repo> when a consumer contract requires verification",
"events": [{ "name": "contract_requiring_verification_published" }],
"provider": { "name": "<provider-pacticipant-name>" },
"request": {
"method": "POST",
"url": "https://api.github.com/repos/<org>/<provider-repo>/dispatches",
"headers": {
"Accept": "application/vnd.github+json",
"Authorization": "Bearer ${user.githubToken}",
"Content-Type": "application/json",
"User-Agent": "PactFlow",
"X-GitHub-Api-Version": "2022-11-28"
},
"body": {
"event_type": "contract_requiring_verification_published",
"client_payload": {
"pact_url": "${pactbroker.pactUrl}",
"sha": "${pactbroker.providerVersionNumber}",
"branch": "${pactbroker.providerVersionBranch}",
"consumer_name": "${pactbroker.consumerName}",
"consumer_version_number": "${pactbroker.consumerVersionNumber}",
"consumer_version_tags": "${pactbroker.consumerVersionTags}",
"consumer_version_branch": "${pactbroker.consumerVersionBranch}"
}
}
}
}
Key Points:
${user.githubToken}references a PactFlow secret stored inSettings → Secrets(web UI:/settings/secrets). The secret holds the classic PAT — never inline the token in the webhook body.${pactbroker.*}are PactFlow-injected template variables; the provider workflow reads them fromgithub.event.client_payload.- Use the
contract_requiring_verification_publishedevent (notcontract_published) — the former fires only when a new pact content change needs verification; the latter fires on every publish, including no-op republishes.
Example 2: Provider GitHub Actions Workflow (Triggered by Webhook)
# .github/workflows/contract-test-provider.yml
name: contract-test-provider
on:
repository_dispatch:
types: [contract_requiring_verification_published]
push:
branches: [main]
jobs:
verify:
runs-on: ubuntu-latest
env:
PACT_BROKER_BASE_URL: ${{ secrets.PACT_BROKER_BASE_URL }}
PACT_BROKER_TOKEN: ${{ secrets.PACT_BROKER_TOKEN }}
# Pulled from webhook client_payload when triggered by PactFlow:
PACT_PAYLOAD_URL: ${{ github.event.client_payload.pact_url }}
GITHUB_SHA: ${{ github.event.client_payload.sha || github.sha }}
GITHUB_BRANCH: ${{ github.event.client_payload.branch || github.head_ref || github.ref_name }}
steps:
- uses: actions/checkout@v4
with:
# Check out the provider version known to the broker — this is the provider SHA PactFlow wants verified.
ref: ${{ github.event.client_payload.sha || github.sha }}
- uses: actions/setup-node@v4
with:
node-version: 20
- run: npm ci
- name: Run provider verification
run: npm run test:pact:provider
- name: Can I deploy provider?
if: github.event_name == 'push'
run: npm run can:i:deploy:provider
Key Points:
repository_dispatchis the event type emitted by GitHub when the webhook's REST call hits/repos/<org>/<repo>/dispatches.- The
typesfilter must match the webhook'sevent_type(contract_requiring_verification_publishedhere). - Checking out the provider version known to the broker (
providerVersionNumber) ensures verification runs against the exact provider commit PactFlow registered — not whatever is on main. PACT_PAYLOAD_URLmakesbuildVerifierOptionsverify only the triggering pact (seepactjs-utils-provider-verifier.mdExample 1).
Example 3: Secret Rotation Runbook
Trigger: can-i-deploy in a consumer repo times out with There is no verified pact between <consumer-version> and the version of <provider> currently in <env> — AND the provider's contract-test-provider workflow shows no recent repository_dispatch runs.
Diagnosis:
- In PactFlow UI:
Settings → Webhooks → <webhook-id> → Test. A401 Unauthorizedfrom GitHub confirms the token is dead. - In PactFlow UI: the webhook's "Last executed at" is hours/days stale while consumer pacts are actively being published.
Rotation:
- Log in to GitHub as the dedicated machine user (e.g.,
pactflow-<org>). Do not use a personal account — the whole point of the machine user is that the token outlives any individual. Settings → Developer settings → Personal access tokens → Tokens (classic) → Generate new token (classic).- Configure the token:
- Name:
pactflow-webhook-<yyyy-mm-dd> - Expiration: No expiration (accepted trade-off for a locked-down machine-user token stored in PactFlow's secret vault)
- Scopes:
repo(full repo scope is required byrepository_dispatch;public_repoalone is insufficient for private repos)
- Name:
- Copy the new token value (shown only once).
- In PactFlow UI:
Settings → Secrets → <secret-name>(e.g.,githubToken). Paste the new token into the value field and save. The webhook does not need to be edited — it references the secret by name via${user.<secret-name>}. - Re-test the webhook:
Settings → Webhooks → <webhook-id> → Test. ExpectHTTP/1.1 204 No Content(GitHub's success response forrepository_dispatch). - In the provider repo: watch
Actions → contract-test-providerfor the newly dispatched run. Re-run the original consumer CI to confirmcan-i-deploynow passes. - Revoke the old token: in the machine user's GitHub settings, delete the previous
pactflow-webhook-*token so a leaked copy can't be reused.
Why no expiration: A token with a 90-day expiry rotates 4× per year. Each rotation is a silent-failure window if the runbook isn't executed proactively. With monitoring (Example 4) + a locked-down machine-user-owned PAT that is only stored in PactFlow, long-lived is safer than short-lived-but-forgotten.
Example 4: Staleness Monitoring (Detect Silent Webhook Failures)
Goal: Alert the team if verification results haven't been published for a pacticipant pair in the last N hours, so an expired PAT or network issue doesn't silently block can-i-deploy for days.
Pick one of these (in increasing order of investment):
Option A — Daily sanity CI job (cheapest):
# .github/workflows/pact-staleness-check.yml
name: pact-staleness-check
on:
schedule:
- cron: '0 9 * * 1-5' # weekdays 09:00 UTC
workflow_dispatch:
jobs:
check:
runs-on: ubuntu-latest
steps:
- name: Fail if latest verification for <pair> is older than 24h
env:
PACT_BROKER_BASE_URL: ${{ secrets.PACT_BROKER_BASE_URL }}
PACT_BROKER_TOKEN: ${{ secrets.PACT_BROKER_TOKEN }}
run: |
# Query broker matrix for newest verification timestamp for consumer/provider pair.
# Exit 1 if > 24h old; team gets an email on the failed scheduled run.
./scripts/assert-recent-verification.sh <consumer> <provider> 86400
Option B — PactFlow metrics endpoint: Use the SmartBear MCP get_metrics / get_team_metrics tool (see pact-mcp.md) to surface verification freshness in a dashboard or Slack digest.
Option C — Webhook delivery log: PactFlow logs every webhook execution. Ship those logs to your SIEM / observability stack and alert on sustained 4xx responses from api.github.com.
Key Points:
- The point is not "which option you pick" — it's that you pick at least one. Without monitoring, the first time you learn the webhook is dead is when a release is blocked.
- Alert threshold should match your consumer-publish cadence: if consumers publish daily, alert after 24–48h of silence; if hourly, after 3–6h.
- Keep the alert noise-free: page only on sustained staleness, not a single missed run.
Key Points
- Dedicated machine user owns the PAT — never a personal PAT. Name it
pactflow-<org>or similar; give it outside-collaborator access only to the specific provider repos. - Classic PAT,
reposcope, no expiration — required forrepository_dispatch. The "no expiration" trade-off is accepted in exchange for machine-user ownership + PactFlow-secret storage + staleness monitoring. - Store the PAT as a PactFlow secret at
/settings/secrets, reference it from the webhook via${user.<secret-name>}. Never inline the token. - Monitor for silence — at minimum, a daily scheduled CI job that asserts a recent verification timestamp exists for each critical consumer/provider pair.
- Rotation is a runbook, not an emergency — document it (see Example 3), keep it in the repo, and do a practice rotation once a year so it stays fresh.
- Symptom to remember: "consumer
can-i-deploytimeout after 900s withThere is no verified pact..." + "provider'scontract-test-providerworkflow has no recent runs" = expired/revoked PAT. Start with Example 3.
Related Fragments
pactjs-utils-provider-verifier.md— howPACT_PAYLOAD_URLfrom the webhook'sclient_payload.pact_urlis consumed bybuildVerifierOptionspact-consumer-framework-setup.md— consumer CI flow that issuescan-i-deployand silently times out when the webhook is deadpact-mcp.md— SmartBear MCP tools (Matrix,Metrics - All) useful for staleness monitoring dashboardscontract-testing.md— foundational CDC patterns and resilience coverage
Anti-Patterns
Wrong: Using a human's personal PAT
# ❌ PactFlow secret githubToken stores the lead engineer's personal classic PAT
# When they leave / rotate / revoke → all provider verifications stop silently
Right: Dedicated machine user owns the PAT
# ✅ Machine user `pactflow-<org>` generates the PAT; secret is owned by the org
# PAT lifecycle is decoupled from any individual's employment or laptop state
Wrong: No staleness monitoring
# ❌ No scheduled check for verification recency
# First signal that the webhook is dead: a blocked release PR, several days later
Right: Daily scheduled sanity check
# ✅ Scheduled workflow fails if latest verification > 24h old
# Team gets email alert on failed scheduled run → rotate PAT before anyone is blocked
Wrong: Short-expiration PAT with no rotation tooling
# ❌ 90-day expiry PAT, no calendar reminder, no runbook
# Breaks every 90 days for a day or two until someone notices
Right: No-expiration PAT on machine user + monitoring + documented runbook
# ✅ Long-lived PAT, scoped narrowly, stored in PactFlow, monitored for staleness
# Rotation is intentional (security review, suspected leak) not calendar-driven
Source: PactFlow webhook documentation, GitHub repository_dispatch REST API, seon-mcp-server / seon-admin-panel production incident April 2026