TIER docs

Webhook setup

TIER records PR outcomes from a GitHub webhook. Without it, tierd serve still captures cost, but it computes no full TIER scores because it has no outcomes.

Configure the webhook in GitHub

Repository (or organization) Settings → Webhooks → Add webhook:

Configure the secret on the tierd side

Provide the same secret to tierd serve, ideally without putting it on the command line:

export TIER_WEBHOOK_SECRET=@$HOME/.tier/webhook-secret   # read from a file
# or:  ./bin/tierd serve --webhook-secret @$HOME/.tier/webhook-secret ...

Fail-closed: if no secret is set, the POST /webhook/github route is not mounted at all — an unauthenticated webhook would let anyone who can reach the listener forge merged-PR outcomes or fake revert pushes. You will see this at startup:

WARN TIER_WEBHOOK_SECRET is not set — POST /webhook/github is disabled (fail closed, #60)

Every delivery is verified with an HMAC-SHA256 signature over the raw body, compared against the X-Hub-Signature-256 header. A mismatch returns 403.

PR size labels and outcome weight

When a merged PR carries a size label, TIER weighs the outcome from that label (weight_source='label') instead of the diff-size heuristic. Out of the box it recognizes GitHub's size/xs..size/xl labels and the bare xs..xl forms, mapped onto the fixed outcome scale:

Label (case-insensitive) Weight
size/xs, xs 0.5
size/s, s 1
size/m, m 3
size/l, l 5
size/xl, xl 8

A PR whose labels don't match any entry falls through to the git-diff heuristic (weight_source='git-heuristic') — nothing is lost, but a label is the more trustworthy signal.

Remapping the label names (outcomes.size_labels)

If your org uses a different naming convention — size: M with a space, size-l, or an S-prefixed scheme common to pull-request-size bots — remap the names in the YAML config (tierd serve --config):

outcomes:
  size_labels:
    "xs": 0.5
    "s": 1
    "m": 3
    "l": 5
    "xl": 8
    "xxl": 8   # extra names are fine — they just map onto the same fixed scale

Rules:

Work-type labels (#187) are a deliberately fixed taxonomy and are not configurable; this applies to size labels only.

Delivery semantics

These local bounds do not establish complete delivery. GitHub documents a separate 25 MB provider payload cap: events exceeding it produce no webhook payload. Push payloads include at most 2,048 commits. TIER cannot observe omitted payloads or commits through this endpoint; retrying a local 413 does not recover provider omissions. See GitHub's webhook payload limits and push payload.

Redelivering a failed delivery

GitHub can redeliver only deliveries from the past 3 days, so act promptly. A redelivery carries the same X-GitHub-Delivery GUID, and tierd reprocesses it because a failed delivery is never recorded as seen.

In the GitHub UI: open the repository (or organization) Settings → Webhooks, click the TIER webhook's URL, open the Recent Deliveries tab, click a failed delivery (red icon), and click Redeliver.

With the REST API (for example with the gh CLI, signed in as a repository admin; for an organization webhook, see below):

# 1. The hook id: the number at the end of the webhook's settings page URL
#    (.../settings/hooks/<hook_id>), or from this list.
gh api repos/<owner>/<repo>/hooks --jq '.[] | {id, url: .config.url}'

# 2. The failed delivery ids, including timeouts and missing responses.
#    Keep ids as bare scalars: object output can round ids above 2^53,
#    producing an id that the redelivery endpoint rejects.
gh api --paginate 'repos/<owner>/<repo>/hooks/<hook_id>/deliveries?per_page=100' \
  --jq '.[] | select(.status != "OK") | .id'

# 3. Redeliver one of them.
gh api -X POST repos/<owner>/<repo>/hooks/<hook_id>/deliveries/<id>/attempts

For an organization webhook, replace repos/<owner>/<repo> with orgs/<org>. This needs an organization owner and the admin:org_hook token scope, which a default gh auth login does not grant: add it with gh auth refresh -s admin:org_hook.

Verifying it works

You can compute a signature by hand and post a synthetic merged-PR payload using a private header file, keeping the HMAC out of process arguments (the key file must be the same file given to tierd serve --webhook-secret):

This writes an outcome for the fake login __tier_webhook_test__ and issue issue-999999999 into the receiving server's store. The row persists there and can affect reports; this is not a dry run. Use a test server if you do not want synthetic data in your live store.

(
set -eu
umask 077
H_WEBHOOK=$(mktemp)
trap 'rm -f "$H_WEBHOOK"' EXIT
PAYLOAD='{"action":"closed","pull_request":{"number":999999999,"merged":true,"merge_commit_sha":"tier-webhook-test-not-a-real-merge","user":{"login":"__tier_webhook_test__"},"body":"closes #999999999","labels":[{"name":"size/m"}],"additions":10,"deletions":2,"changed_files":3}}'
SIG="sha256=$(printf '%s' "$PAYLOAD" | perl -MDigest::SHA=hmac_sha256_hex -e '
  local $/;
  open my $fh, "<", "$ENV{HOME}/.tier/webhook-secret" or die $!;
  my $key = <$fh>;
  $key =~ s/[\r\n]+\z//;
  die "empty webhook secret\n" unless length $key;
  print hmac_sha256_hex(scalar <STDIN>, $key);
')"
[ "$SIG" != "sha256=" ] || { echo "could not compute a signature: check ~/.tier/webhook-secret" >&2; exit 1; }
printf 'X-Hub-Signature-256: %s\n' "$SIG" > "$H_WEBHOOK"
curl -i -X POST https://<your-server>/webhook/github \
  -H "X-GitHub-Event: pull_request" \
  -H "X-GitHub-Delivery: $(uuidgen)" \
  -H @"$H_WEBHOOK" \
  -H "Content-Type: application/json" \
  -d "$PAYLOAD"
)
# expect HTTP 204

GitHub's "Redeliver" button on any recorded delivery does the same against a real payload.

Troubleshooting