diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml new file mode 100644 index 0000000..0e44741 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -0,0 +1,5 @@ +blank_issues_enabled: true +contact_links: + - name: PRD policy — read this first + url: https://github.com/databrainhq/.github/blob/main/docs/PRD-POLICY.md + about: Why every feature and hotfix needs a PRD, and which template to pick. diff --git a/.github/ISSUE_TEMPLATE/prd-lite.yml b/.github/ISSUE_TEMPLATE/prd-lite.yml new file mode 100644 index 0000000..69e971b --- /dev/null +++ b/.github/ISSUE_TEMPLATE/prd-lite.yml @@ -0,0 +1,66 @@ +name: "PRD (lite)" +description: "Bug fix, hotfix RCA, or a small self-contained feature. One page." +title: "PRD: " +labels: ["PRD", "prd:lite", "prd:draft"] +body: + - type: markdown + attributes: + value: | + One-page PRD. Use this for fixes, hotfix RCAs, and small self-contained + features inside one repo. + + If the change crosses a repo boundary, changes a contract, or needs a + migration, use **PRD (full)** instead. + + A PR cannot merge until this issue carries the `prd-approved` label. + + - type: input + id: owner + attributes: + label: Owner + validations: { required: true } + + - type: dropdown + id: repo + attributes: + label: Repo + options: [frontend-mono, backend, plugin-in-app, mcp-server, hasura, docs, website, other] + validations: { required: true } + + - type: dropdown + id: kind + attributes: + label: Kind + options: ["feature (small)", "bug fix", "hotfix / RCA", "chore with user-visible effect"] + validations: { required: true } + + - type: textarea + id: shipping + attributes: + label: What are we shipping? + description: > + For a hotfix, this is the RCA. What broke, why it broke, and what stops it + happening again. + validations: { required: true } + + - type: textarea + id: success + attributes: + label: How do we know it worked? + description: What you check after the merge. + validations: { required: true } + + - type: textarea + id: notdoing + attributes: + label: What are we explicitly NOT doing? + description: Keeps the fix from growing in review. One or two lines is fine. + validations: { required: true } + + - type: checkboxes + id: ack + attributes: + label: Before you request review + options: + - label: "This really is small enough for a lite PRD." + required: true diff --git a/.github/ISSUE_TEMPLATE/prd.yml b/.github/ISSUE_TEMPLATE/prd.yml new file mode 100644 index 0000000..08fc011 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/prd.yml @@ -0,0 +1,112 @@ +name: "PRD (full)" +description: "Feature or change that crosses a repo boundary, changes a contract, or needs a migration." +title: "PRD: " +labels: ["PRD", "prd:draft"] +body: + - type: markdown + attributes: + value: | + Full PRD. Use this when the change crosses a repo boundary, changes an API or + storage contract, needs a migration, or a reviewer could reasonably disagree + about the approach. + + Smaller than that? Close this and open a **PRD (lite)** instead. + + A PR cannot merge until this issue carries the `prd-approved` label. + + - type: input + id: owner + attributes: + label: Owner + description: One name. The person who answers review comments. + validations: { required: true } + + - type: dropdown + id: repos + attributes: + label: Repos affected + multiple: true + options: [frontend-mono, backend, plugin-in-app, mcp-server, hasura, docs, website, other] + validations: { required: true } + + - type: textarea + id: links + attributes: + label: Links + description: Specs, ADRs, superseded docs, related PRDs. One per line. + placeholder: | + Schema: specs/semantic-layer-v2.md + Decision: docs/adr/0001-....md + Supersedes: #1234 + + - type: textarea + id: shipping + attributes: + label: What are we shipping? + description: The thing itself, not the motivation. One paragraph. + validations: { required: true } + + - type: textarea + id: success + attributes: + label: How do we know it worked? + description: Outcomes you can check after the merge. Not a feeling. + validations: { required: true } + + - type: textarea + id: hard + attributes: + label: What's the hard part? + description: The real constraint. If there is no hard part, use PRD (lite) instead. + validations: { required: true } + + - type: textarea + id: notdoing + attributes: + label: What are we explicitly NOT doing? + description: > + The most important section. List what is cut. Mark each as cut or deferred and + mean it. Anything revived later needs its own PRD. + validations: { required: true } + + - type: textarea + id: scope + attributes: + label: Scope + description: What ships together, and what is out. + value: | + | | In | Out | + | ------------------- | --- | --- | + | Ships together | | | + | Nice later, no date | | | + validations: { required: true } + + - type: input + id: contract + attributes: + label: One contract + description: The single sentence a reviewer should be able to repeat back. + validations: { required: true } + + - type: textarea + id: rollout + attributes: + label: Rollout + description: Flags, migrations, merge order across repos, and the back-out. + validations: { required: true } + + - type: textarea + id: risks + attributes: + label: Risks + description: What breaks if we are wrong, and who notices first. + + - type: checkboxes + id: ack + attributes: + label: Before you request review + options: + - label: "The NOT doing section is filled in, not left as a placeholder." + required: true + - label: "I have read docs/PRD-POLICY.md." + required: true diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md new file mode 100644 index 0000000..45f0a45 --- /dev/null +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -0,0 +1,23 @@ + + +**PRD:** # + +## What changed + + + +## Deviations from the PRD + + + +None. + +## How this was verified + + diff --git a/.github/workflows/prd-gate.yml b/.github/workflows/prd-gate.yml new file mode 100644 index 0000000..455223b --- /dev/null +++ b/.github/workflows/prd-gate.yml @@ -0,0 +1,181 @@ +# Reusable PRD gate. Called by each repo that enforces the policy. +# +# A PR may merge only if it references an issue that carries BOTH the `PRD` +# and `prd-approved` labels — or if the PR itself carries `prd-exempt`. +# +# Caller usage (see setup/caller-workflow.yml): +# jobs: +# prd: +# uses: databrainhq/.github/.github/workflows/prd-gate.yml@main +# secrets: +# cross_repo_token: ${{ secrets.PRD_GATE_TOKEN }} + +name: PRD gate + +on: + workflow_call: + inputs: + exempt_label: + description: Label on the PR that skips the gate. + type: string + default: prd-exempt + approved_label: + description: Label the PRD issue must carry. + type: string + default: prd-approved + secrets: + cross_repo_token: + description: > + Token that can read issues in other databrainhq repos. Optional — without + it, only same-repo PRD references can be verified. + required: false + +permissions: + contents: read + pull-requests: read + issues: read + +jobs: + check: + runs-on: ubuntu-latest + steps: + - uses: actions/github-script@v7 + env: + EXEMPT_LABEL: ${{ inputs.exempt_label }} + APPROVED_LABEL: ${{ inputs.approved_label }} + CROSS_REPO_TOKEN: ${{ secrets.cross_repo_token }} + with: + script: | + const pr = context.payload.pull_request; + if (!pr) { core.setFailed('PRD gate must run on pull_request events.'); return; } + + const EXEMPT = process.env.EXEMPT_LABEL; + const APPROVED = process.env.APPROVED_LABEL; + const POLICY = 'https://github.com/databrainhq/.github/blob/main/docs/PRD-POLICY.md'; + + // --- escape hatch ------------------------------------------------ + const prLabels = (pr.labels || []).map(l => l.name.toLowerCase()); + if (prLabels.includes(EXEMPT.toLowerCase())) { + core.notice(`PR carries "${EXEMPT}" — gate skipped. A human with write access applied it.`); + return; + } + + // --- collect candidate PRD references ---------------------------- + // Sources: PR body, PR title, and the branch name. + const haystack = [pr.body || '', pr.title || '', pr.head?.ref || ''].join('\n'); + const owner = context.repo.owner; + const refs = new Map(); // key -> {owner, repo, number} + + const add = (o, r, n) => { + const key = `${o}/${r}#${n}`; + if (!refs.has(key)) refs.set(key, { owner: o, repo: r, number: Number(n) }); + }; + + // https://github.com/OWNER/REPO/issues/123 + for (const m of haystack.matchAll(/https?:\/\/github\.com\/([\w.-]+)\/([\w.-]+)\/issues\/(\d+)/gi)) { + add(m[1], m[2], m[3]); + } + // OWNER/REPO#123 + for (const m of haystack.matchAll(/\b([\w.-]+)\/([\w.-]+)#(\d+)\b/g)) { + add(m[1], m[2], m[3]); + } + // bare #123 -> this repo + for (const m of haystack.matchAll(/(?:^|[^\w/])#(\d+)\b/g)) { + add(owner, context.repo.repo, m[1]); + } + + if (refs.size === 0) { + core.setFailed( + 'No PRD linked.\n\n' + + 'Add a line to the PR description that links the PRD issue, for example:\n' + + ' PRD: #1234\n' + + ' PRD: databrainhq/backend#3379\n\n' + + `The issue must carry the "PRD" and "${APPROVED}" labels.\n` + + `Policy: ${POLICY}` + ); + return; + } + + // --- resolve each reference -------------------------------------- + const crossToken = process.env.CROSS_REPO_TOKEN; + + // Cross-repo reads go over plain fetch so we never depend on a second + // Octokit instance being require-able inside github-script. + async function readIssue(ref, sameRepo) { + if (sameRepo) { + const r = await github.rest.issues.get({ + owner: ref.owner, repo: ref.repo, issue_number: ref.number, + }); + return r.data; + } + if (!crossToken) { + const e = new Error('no PRD_GATE_TOKEN secret set'); + e.noToken = true; + throw e; + } + const res = await fetch( + `https://api.github.com/repos/${ref.owner}/${ref.repo}/issues/${ref.number}`, + { headers: { + Authorization: `Bearer ${crossToken}`, + Accept: 'application/vnd.github+json', + 'User-Agent': 'databrainhq-prd-gate', + } }, + ); + if (!res.ok) { + const e = new Error(`HTTP ${res.status}`); + e.status = res.status; + throw e; + } + return res.json(); + } + + const report = []; + let approvedRef = null; + + for (const ref of refs.values()) { + const sameRepo = ref.owner === owner && ref.repo === context.repo.repo; + let issue; + try { + issue = await readIssue(ref, sameRepo); + } catch (e) { + report.push( + `- ${ref.owner}/${ref.repo}#${ref.number} — could not read (${e.status || e.message})` + + (e.noToken ? '. Set the PRD_GATE_TOKEN secret to allow cross-repo checks.' : '') + ); + continue; + } + + // A PR is not a PRD, even though the issues API returns both. + if (issue.pull_request) { + report.push(`- ${ref.owner}/${ref.repo}#${ref.number} — is a pull request, not a PRD issue`); + continue; + } + + const labels = (issue.labels || []) + .map(l => (typeof l === 'string' ? l : l.name).toLowerCase()); + const isPrd = labels.includes('prd'); + const isApproved = labels.includes(APPROVED.toLowerCase()); + + if (isPrd && isApproved) { approvedRef = { ref, issue }; break; } + if (isPrd) report.push(`- ${ref.owner}/${ref.repo}#${ref.number} "${issue.title}" — PRD found, but not yet ${APPROVED}`); + else report.push(`- ${ref.owner}/${ref.repo}#${ref.number} "${issue.title}" — not labelled PRD`); + } + + if (approvedRef) { + const { ref, issue } = approvedRef; + core.notice(`Approved PRD: ${ref.owner}/${ref.repo}#${ref.number} — ${issue.title}`); + await core.summary + .addHeading('PRD gate passed', 3) + .addRaw(`Approved PRD: [${ref.owner}/${ref.repo}#${ref.number}](${issue.html_url}) — ${issue.title}`) + .write(); + return; + } + + core.setFailed( + `No approved PRD found for this PR.\n\n${report.join('\n')}\n\n` + + `Fix one of these:\n` + + ` 1. Link the right PRD issue in the PR description ("PRD: #1234").\n` + + ` 2. Get the PRD approved — a reviewer adds the "${APPROVED}" label.\n` + + ` 3. If this genuinely needs no PRD, a maintainer adds "${EXEMPT}" to this PR.\n\n` + + `Policy: ${POLICY}` + ); diff --git a/docs/PRD-POLICY.md b/docs/PRD-POLICY.md new file mode 100644 index 0000000..cbe470f --- /dev/null +++ b/docs/PRD-POLICY.md @@ -0,0 +1,98 @@ +# PRD policy + +Every feature and every hotfix gets a PRD, approved before the PR is reviewed. + +Agreed by Rahul and Koushik, August 2026. This document is the rule; the CI gate +is the enforcement. + +--- + +## The rule + +1. **Write the PRD first.** Open an issue with the **PRD (full)** or **PRD (lite)** + template. It lands with the `PRD` and `prd:draft` labels. +2. **Get comments.** Post it in the team channel. Everyone reviews the approach, + not the code, because there is no code yet. +3. **Get approval.** Sapna signs off on architecture and codebase fit. The approver + removes `prd:draft` and adds `prd-approved`. +4. **Then raise the PR.** Link the PRD in the PR description: `PRD: #1234`. +5. **CI checks it.** A PR whose PRD is missing or unapproved cannot merge. + +We are not reviewing PRs without an approved PRD. That is the whole point: an audit +trail of what we built and why, written while the decision was still open. + +## Which template + +| Situation | Template | +| --------------------------------------------------------------- | ---------- | +| Crosses a repo boundary | full | +| Changes an API, storage, or embed contract | full | +| Needs a migration or a flag | full | +| A reviewer could reasonably disagree about the approach | full | +| Small self-contained feature in one repo | lite | +| Bug fix | lite | +| Hotfix — **the PRD is the RCA** | lite | +| Dependency bump, revert, generated file, typo | none: use `prd-exempt` | + +If a full PRD gets too long to read in one sitting, that is a signal to cut scope, +not to split it into linked documents. + +## The section that matters + +**What are we explicitly NOT doing.** + +It is the section reviewers should read first and the one most often left empty. +Mark each item **cut** or **deferred**, and mean it. Anything revived later needs +its own PRD. A PRD without this section is not approved, no matter how good the +rest is. + +## Escape hatch + +A maintainer may add `prd-exempt` to a PR to skip the gate. Say why in the PR +description. Anyone with write access can apply it, and every use is visible in +the PR timeline. It exists so the gate never blocks a real outage fix — use it for +that, then write the RCA as a lite PRD afterwards. + +## Where PRDs live + +**In a GitHub issue, in the repo the work lands in.** Not Google Docs, not a Slack +thread, not a loose `.md` on a branch. Those all still exist from before this +policy and are fine as links, but a new PRD is an issue, so the gate can read it +and so the audit trail sits next to the code. + +Cross-repo work: open the PRD in the repo where most of the change lands and link +it from the others as `databrainhq/backend#3379`. + +## Labels + +| Label | Meaning | +| -------------- | -------------------------------------------------------------- | +| `PRD` | This issue is a PRD. | +| `prd:draft` | Being written. Not ready for review. | +| `prd:lite` | One-pager. | +| `prd-approved` | Approved. PRs referencing it may merge. | +| `prd-exempt` | On a **PR**, not an issue. Skips the gate. | + +Only `prd-approved` is load-bearing for CI. The others are for humans. + +## Setup + +See [`setup/install-prd-gate.sh`](../setup/install-prd-gate.sh). Three steps cannot +be scripted safely and need an org admin: + +1. `databrainhq/.github` → Settings → Actions → General → Access → allow org repos. +2. Add the org secret `PRD_GATE_TOKEN` (read access to Issues) for cross-repo links. +3. Make `PRD gate / prd` a required status check on each repo's default branch. + +Until step 3 is done the gate reports but does not block. + +## Writing a PRD with Edith + +`/edith-prd ` drafts a PRD into an existing issue: it reads the +repo, reads the three most recently approved PRDs as style examples, and posts a +draft as a comment for you to edit. + +Edith never approves a PRD and never applies `prd-approved`. A person does that. + +Once approved, `/edith-build ` hands the PRD to a Cursor cloud agent, +which branches, implements, and opens a PR that links back to the PRD. diff --git a/docs/PRD-TEMPLATE.md b/docs/PRD-TEMPLATE.md new file mode 100644 index 0000000..17a8a32 --- /dev/null +++ b/docs/PRD-TEMPLATE.md @@ -0,0 +1,53 @@ +# PRD: + +| | | +| -------- | ---------------------------------------------------- | +| Status | draft \| ready-for-review \| ready-for-impl \| shipped | +| Owner | | +| Date | | +| Repo | | +| Links | | + +Fill this before writing code. Keep answers short. Link the filled doc from the PR. + +--- + +## Problem + +**What are we shipping?** + +One paragraph. The thing itself, not the motivation. + +**How do we know it worked?** + +Observable outcomes. Something you can check after the merge, not a feeling. + +**What's the hard part?** + +The real constraint. If there is no hard part, say so and use PRD-lite instead. + +**What are we explicitly NOT doing?** + +The most important section. List what is cut. Say **cut** or **deferred** for each, +and mean it. Anything revived later needs its own PRD. + +--- + +## Scope + +| | In | Out | +| ------------------- | --- | --- | +| Ships together | | | +| Nice later, no date | | | + +**One contract:** the single sentence a reviewer should be able to repeat back. + +--- + +## Rollout + +How it lands. Flags, migrations, order of merge across repos, and the back-out. + +## Risks + +What breaks if we are wrong, and who notices first. diff --git a/setup/caller-workflow.yml b/setup/caller-workflow.yml new file mode 100644 index 0000000..b5696b1 --- /dev/null +++ b/setup/caller-workflow.yml @@ -0,0 +1,24 @@ +# Copy this file to .github/workflows/prd-gate.yml in each repo that enforces the +# PRD policy (frontend-mono, backend, plugin-in-app). +# +# One-time prerequisites: +# 1. databrainhq/.github must allow its workflows to be reused by org repos: +# Settings -> Actions -> General -> Access -> "Accessible from repositories +# in the databrainhq organization". +# 2. Add the org secret PRD_GATE_TOKEN (a fine-grained PAT or GitHub App token +# with read access to Issues on databrainhq repos). Without it the gate can +# still verify same-repo PRD links, but not cross-repo ones. +# 3. Make "PRD gate / check" a required status check on the default branch: +# Settings -> Branches -> branch protection -> Require status checks. + +name: PRD gate + +on: + pull_request: + types: [opened, edited, reopened, synchronize, labeled, unlabeled] + +jobs: + prd: + uses: databrainhq/.github/.github/workflows/prd-gate.yml@main + secrets: + cross_repo_token: ${{ secrets.PRD_GATE_TOKEN }} diff --git a/setup/install-prd-gate.sh b/setup/install-prd-gate.sh new file mode 100755 index 0000000..53c9388 --- /dev/null +++ b/setup/install-prd-gate.sh @@ -0,0 +1,68 @@ +#!/usr/bin/env bash +# Creates the PRD labels and installs the gate caller workflow in the target repos. +# +# ./setup/install-prd-gate.sh --dry-run # show what would happen +# ./setup/install-prd-gate.sh # create labels +# ./setup/install-prd-gate.sh --with-workflow # labels + open a PR adding the caller +# +# Needs: gh CLI, authenticated with repo admin on databrainhq. +# This script never force-pushes and never deletes anything. + +set -euo pipefail + +REPOS=(frontend-mono backend plugin-in-app) +ORG=databrainhq +DRY=0 +WITH_WORKFLOW=0 + +for a in "$@"; do + case "$a" in + --dry-run) DRY=1 ;; + --with-workflow) WITH_WORKFLOW=1 ;; + *) echo "unknown flag: $a" >&2; exit 2 ;; + esac +done + +run() { if [ "$DRY" = 1 ]; then echo "DRY: $*"; else "$@"; fi; } + +# name|color|description +LABELS=( + "PRD|0E8A16|This issue is a PRD. Required before a PR can merge." + "prd:draft|FBCA04|PRD is being written. Not ready for review." + "prd:lite|C5DEF5|One-page PRD. Fix, hotfix RCA, or small feature." + "prd-approved|0052CC|PRD approved. PRs may now reference this issue and merge." + "prd-exempt|5319E7|This PR skips the PRD gate. Applied by a maintainer, with a reason in the PR." +) + +for repo in "${REPOS[@]}"; do + echo "== $ORG/$repo ==" + for spec in "${LABELS[@]}"; do + IFS='|' read -r name color desc <<< "$spec" + # --force updates colour/description if the label already exists; it never deletes. + run gh label create "$name" --repo "$ORG/$repo" --color "$color" --description "$desc" --force + done + + if [ "$WITH_WORKFLOW" = 1 ]; then + tmp="$(mktemp -d)" + run gh repo clone "$ORG/$repo" "$tmp/$repo" -- --depth=1 + if [ "$DRY" = 0 ]; then + mkdir -p "$tmp/$repo/.github/workflows" + cp "$(dirname "$0")/caller-workflow.yml" "$tmp/$repo/.github/workflows/prd-gate.yml" + git -C "$tmp/$repo" checkout -b chore/prd-gate + git -C "$tmp/$repo" add .github/workflows/prd-gate.yml + git -C "$tmp/$repo" commit -m "ci: require an approved PRD before a PR can merge" + git -C "$tmp/$repo" push -u origin chore/prd-gate + gh pr create --repo "$ORG/$repo" --base main --head chore/prd-gate \ + --title "ci: PRD gate" \ + --body "Adds the shared PRD gate. See databrainhq/.github/docs/PRD-POLICY.md. + +Not enforcing until someone makes \`PRD gate / prd\` a required status check on \`main\`." + fi + fi +done + +echo +echo "Labels done. Remaining manual steps (cannot be scripted safely):" +echo " 1. databrainhq/.github -> Settings -> Actions -> General -> Access -> allow org repos." +echo " 2. Add org secret PRD_GATE_TOKEN (read access to Issues)." +echo " 3. Make 'PRD gate / prd' a required status check on each repo's default branch."