# Git code-review webhook — plan for Zettai Process Tracker

Status: plan only. Nothing in this document is built yet.

Today, Gate C blocks **Code Review & QA → Done** until `task.codeReviewApproved` is true (`server/src/services/workflow.ts`, `validateGateC`). A Project Admin, Super Admin, or Developer can tick that box on the Workflow tab. The tick is not tied to a pull request, a reviewer, or a passing build.

This plan replaces that tick with a signed webhook from the Git host, using the same pattern large engineering organisations use with Jira, Azure Boards, and GitHub/GitLab.

---

## 1. How large companies do this

They split three jobs that ZPT currently mixes into one checkbox.

| Job | Who owns it | What “done” means |
| --- | --- | --- |
| Code review | Git host (GitHub, GitLab, Bitbucket, Azure DevOps) | A pull request has the required approvals from people who did not write the change, and branch protection is on |
| Quality sign-off | The tracker (ZPT Gate C: QA Passed, evidence, tester approval, no blocking bugs) | A tester recorded a result |
| Release | CI/CD (Actions, GitLab CI, Jenkins) | The approved commit is merged and deployed |

The tracker does not decide that a review happened. The Git host does. The tracker only records the result.

The standard shape is:

1. **Branch protection on the integration branch** (`main` or `develop`). Direct push is blocked. Merge needs at least one approving review, a green required check, and usually “dismiss stale approvals” so a new commit clears an old approval.
2. **CODEOWNERS** (or GitLab approval rules) so the reviewer is a named owner, not whoever clicked Approve.
3. **A task key in the branch and the pull request.** Example: branch `feature/ZPT-101-invoice-export`, PR title `ZPT-101 Invoice export`. The host already knows the task id; nobody copies a UUID into Git.
4. **A webhook, not a poll, as the fast path.** The Git host POSTs `pull_request` and `pull_request_review` to the tracker. The tracker verifies a shared secret, stores the delivery, and updates the task. A slow reconcile job re-reads open pull requests in case a delivery was missed.
5. **The tracker sets a flag. It does not move the stage by itself.** Approval makes `codeReviewApproved` true. Moving to Done still runs the rest of Gate C (QA, evidence, bugs, acceptance, tester approval). Jira automations that jump an issue to Done on merge are the exception, and most teams turn that off once QA is a separate step.
6. **Identity is an account link, not a display name.** GitHub login `jane.dev` is stored on the employee. A review counts only when that login maps to an active employee who is allowed to review that project, and who is not the pull-request author.

That is the whole enterprise pattern. The webhook is the pipe. Branch protection is the policy. The tracker flag is the copy of the policy result.

---

## 2. What stays true in ZPT

Gate C in `validateGateC` keeps this check:

- Project policy `requireCodeReview` (default on).
- Task field `codeReviewApproved` must be true before Done.

QA Passed, QA evidence, blocking bugs, required subtasks, acceptance, and tester approval stay separate. A merged pull request does not mean the task is Done.

When Git review is enabled on a project:

- The Workflow tab shows the flag as read-only, with the pull-request link, reviewer, head SHA, and time.
- The task PATCH that sets `codeReviewApproved` from the browser is rejected for that project.
- Super Admin can record a manual override with a reason. The override is an audit row, not a silent checkbox. Use it when the change has no repository (a document-only task) or the Git host is down.

When Git review is off, today’s checkbox behaviour stays, so existing projects do not change until someone turns the integration on.

Reopen from Done already clears completion and QA flags. It must also clear `codeReviewApproved` unless a still-valid merged pull request covers the task. A new commit after approval clears the flag, matching “dismiss stale approvals”.

---

## 3. Linking a pull request to a task

ZPT task codes are unique (`ZPT-101`). That code is the join key.

A pull request is linked when any of these contain the code, case-sensitive, as its own token:

- source branch name (`feature/ZPT-101-…`, `bugfix/ZPT-101-…`)
- pull request title
- pull request body

One pull request may name several tasks. Each named task gets its own link row. A task may have several pull requests over its life (review failed, new branch). The flag uses the **latest link on the project’s integration branch**, not an older merged pull request that a later commit replaced.

Commits that mention the code are stored for the chatter feed. They do not approve the review by themselves. Approval is a review event on a pull request.

---

## 4. When the flag turns on

`codeReviewApproved` becomes **true** only when all of these hold for the latest linked pull request:

1. The pull request targets the project’s configured base branch (default `main`).
2. It is **merged** (stricter, recommended) or, if the project chooses the looser rule, **open and approved**. Default for ZPT is **merged**. Done should not be reachable while the reviewed code is still sitting on a branch.
3. Approving reviews meet the project’s required count (default 1).
4. Each counted approval is from a Git login mapped to an active employee.
5. The author does not count, even if they are a Project Admin.
6. Required status checks on the head SHA are success. If the project lists none, this check is skipped.
7. The head SHA on the approval is the SHA that was merged. A review of an older SHA does not count.

The flag becomes **false** when:

- a new commit is pushed after the approval and the pull request is not re-approved
- the pull request is closed without merge
- the approving review is dismissed
- the task is reopened from Done
- an admin revokes the manual override

The webhook handler writes the flag and a chatter note (`stage_change` is not used; this is not a stage move). `transition_blocked` stays as it is when someone tries Done without the flag.

---

## 5. Request path

```
Developer                         Git host                         ZPT server
    |                                |                                  |
    |  push feature/ZPT-101-…        |                                  |
    |  open PR, title "ZPT-101 …"    |                                  |
    |------------------------------->|                                  |
    |                                |  POST /api/integrations/git/webhook
    |                                |  event: pull_request opened      |
    |                                |--------------------------------->|
    |                                |                                  | verify HMAC
    |                                |                                  | store delivery id
    |                                |                                  | link PR to ZPT-101
    |                                |                                  | chatter: PR opened
    |                                |  204                              |
    |                                |<---------------------------------|
    |  reviewer submits APPROVE      |                                  |
    |------------------------------->|                                  |
    |                                |  event: pull_request_review      |
    |                                |--------------------------------->|
    |                                |                                  | record review
    |                                |                                  | flag still false
    |                                |                                  | (not merged yet)
    |  merge to main                 |                                  |
    |------------------------------->|                                  |
    |                                |  event: pull_request closed      |
    |                                |  merged=true                      |
    |                                |--------------------------------->|
    |                                |                                  | checks still green?
    |                                |                                  | codeReviewApproved=true
    |                                |                                  | chatter: review recorded
    |  Tester later moves to Done    |                                  |
    |  Gate C reads the flag         |                                  |
```

The HTTP handler does four things and returns: verify the signature, reject a duplicate delivery id, enqueue the payload, respond `204`. A worker applies the rules. Webhook timeouts are why large teams never run the business rules inside the request.

Provider scope for the first build: **GitHub** (cloud or Enterprise), because the event names and HMAC header (`X-Hub-Signature-256`) are the common reference. GitLab (`X-Gitlab-Token` plus a project hook secret) is the same tables and a second parser. Do not start with both.

---

## 6. Data to add

New tables, names illustrative:

**`git_installations`** — one row per connected organisation or repo group.

- provider (`github` | `gitlab`)
- account login
- webhook secret (stored hashed or encrypted; never returned to the client)
- active

**`project_git_repos`** — which repo feeds which ZPT project.

- project id
- installation id
- `owner/name`
- base branch (default `main`)
- required approving reviews (default 1)
- require merge before approval flag (default true)
- required check names (text array, optional)
- enabled

**`employee_git_identities`**

- employee id
- provider
- login (unique per provider)
- verified at (set when the employee signs in with that provider, or an admin confirms the email match)

**`task_pull_requests`**

- task id
- repo, number, url
- state (`open` | `merged` | `closed`)
- head SHA, base branch
- author login
- merged at
- counts as approval (boolean, computed)

**`pull_request_reviews`**

- pull request row id
- reviewer login
- state (`approved` | `changes_requested` | `commented` | `dismissed`)
- commit SHA reviewed
- submitted at
- employee id if the login maps

**`git_webhook_deliveries`**

- provider delivery id (unique)
- event name
- received at
- processed at
- status (`pending` | `ok` | `ignored` | `error`)
- error text

**`code_review_overrides`**

- task id
- granted by employee id
- reason
- active

`tasks.codeReviewApproved` remains the field Gate C reads. The webhook and the override are the only writers once a project has a repo connected.

---

## 7. Security

- HTTPS only. Reject unsigned bodies.
- Compare the HMAC with a constant-time compare. GitHub signs the raw body with the webhook secret; use the raw bytes, not a re-serialised JSON object.
- Per-installation secret, rotatable. Rotating creates a new secret and accepts both for a short overlap.
- Store `X-GitHub-Delivery` and ignore a repeat.
- Optional: reject deliveries older than five minutes (`X-GitHub-Delivery` plus the event timestamp) to limit replay.
- The route is unauthenticated by user session and authenticated by signature. It has no other admin powers.
- Do not trust `sender` as a ZPT user until the login is in `employee_git_identities`.
- Log the delivery id, event, repo, and PR number. Do not log the secret or the full diff.
- Rate-limit the webhook path.

---

## 8. What the user configures

**Once per ZPT server (Super Admin)**

1. Create a GitHub App, or a single organisation webhook if a GitHub App is more than the first release needs.
2. Subscribe to `pull_request` and `pull_request_review`.
3. Payload URL: `https://<zpt-host>/api/integrations/git/webhook`.
4. Secret: paste into ZPT’s installation screen. ZPT stores it; GitHub stores it. They must match.
5. Permissions: read pull requests and read commit statuses. No write permission in the first release. ZPT does not comment on or merge pull requests yet.

**Once per project (Project Admin)**

1. Connect `owner/repo` and the base branch.
2. Leave “require merge” on.
3. Optionally name the required checks (`ci / test`).

**Once per employee**

Link Git login. Until that exists, their Approve click is stored and shown, and it does not satisfy the flag. That mismatch should be visible on the task (“review by `jane` is not linked to an employee”).

**Developer habit**

Branch and PR title include the task code. ZPT can show a copyable branch name on the task (`feature/ZPT-101-short-slug`) so people do not invent a second convention.

---

## 9. Build phases

### Phase 1 — Record, do not enforce

- Tables above, GitHub webhook, signature check, delivery dedupe.
- Parse task codes, upsert `task_pull_requests` and reviews.
- Chatter note on open, approve, and merge.
- Workflow checkbox unchanged.
- Goal: prove the pipe against a test repository before any gate depends on it.

### Phase 2 — Flag follows Git

- For projects with a connected repo, compute `codeReviewApproved` from the rules in section 4.
- Workflow control becomes read-only and shows PR, reviewer, SHA.
- Block client writes to the flag.
- Manual override with reason for Super Admin.
- Clear the flag on reopen and on stale SHA.
- Tests next to `workflow.gates.test.ts` for: merged + mapped reviewer passes Gate C’s review line; author self-approval fails; unmapped login fails; new SHA clears the flag; QA and bugs still block Done.

### Phase 3 — Catch missed webhooks

- Scheduled job: for open and recently merged pull requests in connected repos, read the GitHub API and reconcile.
- Alert when a delivery has been `error` or `pending` for more than a few minutes.

### Later, only if needed

- GitLab parser on the same tables.
- Post a ZPT comment back onto the pull request (“ZPT-101 review recorded”).
- Move internal status to a “In review” pill when a PR opens. That is a status hint, not a stage change. Stage moves stay on the chevron and the gates.

---

## 10. Acceptance for phase 2

- A project with no repo connected still uses the checkbox. Gate C text stays “Code review must be approved.”
- A project with a repo connected rejects a browser attempt to set `codeReviewApproved`.
- Merging a PR to `main` titled `ZPT-101 …`, with one approval from a linked employee who is not the author, sets the flag and writes a chatter note.
- The same merge with only the author’s approval leaves the flag false and says why on the Workflow tab.
- Pushing another commit after approval, before merge, leaves the flag false.
- Moving that task to Done still fails if QA has not passed or a blocking bug is open.
- Replaying the same `X-GitHub-Delivery` does not double-apply.
- A body with a bad signature is `401` and changes nothing.

---

## 11. Decisions already made in this plan

| Topic | Choice |
| --- | --- |
| First Git host | GitHub |
| What the webhook is allowed to change | `codeReviewApproved` and chatter, not the stage |
| When the flag turns on | PR merged to the project base branch, with enough non-author approvals from linked employees |
| Who may still tick the box | Nobody, once the project repo is connected. Super Admin override is a separate audited action |
| Task join key | Existing task `code` (`ZPT-101`) in branch or PR title/body |

Open only if the team disagrees with the default: some teams want the flag on **approval while the PR is still open**, so QA can start before merge. That is a project setting (`require merge` off). The default stays merge, because Gate C is the door to Done, and Done should describe code that landed on the base branch.
