- Pact Broker webhook ingestion: Accepts Pact Broker webhook
POSTpayloads (currentlycontract_requiring_verification_publishedandprovider_verification_published) and converts them into normalized event records. Webhook requests are expected to include?key=$SLACK_AGGR_KEY(otherwise the worker responds401). - Stateful aggregation via Durable Objects: Uses a Durable Object (
PactAggregator) to persist events and ensure serialized processing (no interleaving) per aggregator instance. - Retry-friendly publishing: Publishing uses a “peek then ack” flow, so if Slack posting fails the events are not deleted and will be retried on the next cron/trigger.
- Batching + bucketing:
- Stores events in minute buckets (
MINUTE_BUCKET_MS, default 60s). - Enforces a quiet period (
QUIET_PERIOD_MS, default 10s) so events arriving “right now” don’t get published prematurely. - Consolidates events across adjacent buckets so that events for the same pacticipant/version are published together.
- Uses
MAX_TIME_BEFORE_FLUSHING(default 5 minutes) as the consolidation window: recent events can be pulled forward into the current bucket for better grouping, but publishing still only includes completed buckets (everything except the current minute).
- Stores events in minute buckets (
- Slack publishing (main channel):
- Posts a summary message per pacticipant version (counts of publications + verification successes/failures).
- Posts a thread reply containing the detailed publication/verification lines (including links to Pact and GitHub).
- Provider-specific Slack channels + per-contract threads:
- On publication, posts a root summary to a provider channel derived from
PROVIDER_CHANNEL_PREFIX(default#pact-) + provider name. - On verification, posts results into the matching contract thread; verifications on the provider’s configured “master” branch (see
DEFAULT_MASTER_BRANCH/PACTICIPANT_MASTER_BRANCH_EXCEPTIONS) also update the root message with the latest status. - Supports thread rotation when a thread becomes too large (
MAX_MESSAGES_PER_PACT_IN_THREAD), closing the old thread and opening a new one. - Deprecated pact handling (new publications): when a new
contract_requiring_verification_publishedevent is received for the same provider + consumer + consumer branch + provider channel, older pact versions are marked as deprecated and stop receiving updates:- If
consumerVersionBranchmatches the consumer’s configured “master” branch, keep the 2 most recently updated pact versions for that provider/consumer/branch/channel; deprecate the rest. - If
consumerVersionBranchis any other non-empty value, keep only the most recently updated pact version for that provider/consumer/branch/channel; deprecate the rest. - If
consumerVersionBranchis empty/unknown, branch-based deprecation is skipped (nothing is auto-deprecated on publish). - Deprecation is communicated in Slack by replying
🧹 *Deprecated pact!*in the old thread and updating the root summary message to include the same notice.
- If
- On publication, posts a root summary to a provider channel derived from
- Scheduled flushing with working-hours gating:
- Designed to run from Cloudflare Cron every 2 minutes, but gated by local time (
TIMEZONE) so it publishes frequently during working hours and less often off-hours/weekends. - A daily cron runs maintenance (retention pruning for stored publication-thread metadata):
- Uses
RETENTION_MIN_PACT_VERSIONS(default 10) andRETENTION_RECENT_DAYS(default 90) to remove old publication-thread entries per provider/consumer/channel. - When an entry is pruned, Slack is notified by replying
🦕 *Old pact!*in that thread and updating the root summary message with the same notice so it’s clear the thread will no longer receive updates.
- Uses
- Designed to run from Cloudflare Cron every 2 minutes, but gated by local time (
- Operational endpoints (guarded by
SLACK_AGGR_KEY):GET /debug?key=...returns Durable Object state (event buckets, stats, stored publication threads).GET /debug?key=...&clear=trueclears all stored state.GET /debug?key=...&clearPublicationThreads=trueclears only publication-thread metadata.GET /trigger?key=...manually triggers a publish cycle (useful locally since cron doesn’t run inwrangler dev).GET /trigger-daily?key=...runs the daily maintenance job.
Copy the environment templates and configure them:
# Copy environment variables template
cp .env.example .env
# Copy local and production wrangler configuration template
cp wrangler.dev.template.jsonc wrangler.dev.jsonc
cp wrangler.prod.template.jsonc wrangler.prod.jsoncThen edit both .env, wrangler.dev.jsonc and wrangler.prod.jsonc with your specific values:
- SLACK_TOKEN: Your Slack bot token
- SLACK_AGGR_KEY: A secret key for accessing worker endpoints (debug/trigger and webhook ingestion)
- SLACK_CHANNEL: Target Slack channel (e.g.,
#ci) - DEFAULT_MASTER_BRANCH: Default “master” branch name used for branch-specific behavior (e.g.,
masterormain) - PACTICIPANT_MASTER_BRANCH_EXCEPTIONS: JSON map of pacticipant name -> master branch name for exceptions to the default
- GITHUB_BASE_URL: Your GitHub organization URL
- PACTICIPANT_TO_REPO_MAP: JSON mapping of Pact broker pacticipant names to Github repository names. For pacticipants with no entry, it is assumed that the repo name is found by converting PascalCase pacticipant names to dash-separated strings.
Visit https://api.slack.com/apps and click on your application or create a new one.
Go to OAuth & Permissions and make sure that you have the following bot token scopes (assuming your app is called "Pact Broker"):
channels:history: View messages and other content in public channels that "Pact Broker" has been added tochat:writeSend messages as @Pact Brokerchat:write.publicSend messages to channels @Pact Broker isn't a member of
Set up secrets for production deployment:
# Set authentication secrets
wrangler secret put SLACK_TOKEN
wrangler secret put SLACK_AGGR_KEYwrangler types -c wrangler.dev.jsoncgenerates Env type using .env and vars in wrangler.dev.jsonc
For local development (uses wrangler.dev.jsonc):
npm run dev
# or
wrangler dev --config wrangler.dev.jsoncFor production-like local testing (uses wrangler.jsonc):
npm run dev:prodCRON jobs do not work in dev. You have to trigger the job execution manually (DEV_PORT is the port reported):
curl http://localhost:$DEV_PORT/trigger\?key\=$SLACK_AGGR_KEYThe production deployment uses wrangler.prod.jsonc which contains environment-specific configuration values. This file is excluded from git (listed in .gitignore) to avoid committing sensitive company information while still allowing non-secret environment variables to be managed as configuration rather than secrets.
The deployment script automatically handles the production configuration:
npm run deployThis command performs the following steps:
- Copies
wrangler.prod.jsonctowrangler.jsonc(temporary file) - Generates TypeScript types with
wrangler types - Deploys the worker with
wrangler deploy - Cleans up by removing the temporary
wrangler.jsoncfile
If you need to deploy manually:
wrangler deployNote: Manual deployment requires that wrangler.jsonc exists in the project root. For production deployments, always use npm run deploy to ensure the correct configuration is used.
The slack workspace to which the messages are being sent.
This is the Bot User OAuth Token found at OAuth & Permissions at https://api.slack.com/apps
e.g. xoxb-......................
wrangler secret put SLACK_TOKEN --name pact-slack-aggregatorand to set the key for authorization:
wrangler secret put SLACK_AGGR_KEY --name pact-slack-aggregator
wrangler tail pact-slack-aggregatorcurl https://psa.workers.dev/debug\?key\=SLACK_AGGR_KEY
curl -X GET $PACT_URL/webhooksIn folder pact-broker-webhooks read the README file for instructions.
For a Pact broker webhook to communicate from inside Docker to the host localhost POST to http://host.docker.internal:8787 and
run wrangler at ip 0.0.0.0:
wrangler dev --port 8787 --ip 0.0.0.0 --config wrangler.dev.jsonc
Also add to the docker file the environment variables:
PACT_BROKER_WEBHOOK_SCHEME_WHITELIST: 'https http'
And map host.docker.internal so the container can reach services running on the host (eg. webhook target on host port 8787)
extra_hosts:
- "host.docker.internal:host-gateway"
Run tests:
npm test
With coverage:
npm run test -- --coverage=true