-
Notifications
You must be signed in to change notification settings - Fork 1
PRD policy: templates, gate, and org defaults #1
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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. |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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 |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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 |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,23 @@ | ||
| <!-- | ||
| This PR cannot merge until the PRD it references carries the `prd-approved` label. | ||
| Policy: https://github.com/databrainhq/.github/blob/main/docs/PRD-POLICY.md | ||
| --> | ||
|
|
||
| **PRD:** # | ||
|
|
||
| ## What changed | ||
|
|
||
| <!-- One paragraph. What a reviewer will see in the diff. --> | ||
|
|
||
| ## Deviations from the PRD | ||
|
|
||
| <!-- | ||
| The section reviewers should read hardest. If the build differs from the approved | ||
| PRD, say so here and say why. "None" is a valid answer, but only if it is true. | ||
| --> | ||
|
|
||
| None. | ||
|
|
||
| ## How this was verified | ||
|
|
||
| <!-- Tests, manual steps, screenshots. "It compiles" is not verification. --> |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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', | ||
| } }, | ||
| ); | ||
|
Comment on lines
+116
to
+123
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
If the shared token can read an issue that is hidden from readers of the caller repository, a PR author can submit that unrestricted owner/repository reference and the gate publishes the issue title and URL in the caller's workflow output. Restrict lookups to repositories with a compatible visibility boundary or avoid emitting metadata obtained with the shared credential. How this was verified: PR-controlled repository coordinates flow without an allowlist into the token-authenticated fetch, and the returned metadata is written to notices, summaries, and failure output. Prompt To Fix With AIThis is a comment left during a code review.
Path: .github/workflows/prd-gate.yml
Line: 116-123
Comment:
**Token-backed issue metadata oracle**
If the shared token can read an issue that is hidden from readers of the caller repository, a PR author can submit that unrestricted owner/repository reference and the gate publishes the issue title and URL in the caller's workflow output. Restrict lookups to repositories with a compatible visibility boundary or avoid emitting metadata obtained with the shared credential.
**How this was verified:** PR-controlled repository coordinates flow without an allowlist into the token-authenticated fetch, and the returned metadata is written to notices, summaries, and failure output.
---
For each issue above, determine whether it is valid and should be fixed. If so, fix it directly. |
||
| 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}` | ||
| ); | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
actions/github-script@v7resolves through a mutable tag while this step receives repository permissions and the cross-repository issue token. Pinning the action to a reviewed full commit SHA prevents an upstream tag change from executing different code with those credentials.How this was verified: The executable action uses the mutable
v7tag and the same step receivesCROSS_REPO_TOKENthrough its environment.Prompt To Fix With AI
Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!