ci(prd): require manual numbering before merge
prd-number-check / require-numbered-prds (pull_request) Successful in 10s
tracker-policy-pr / check-pr (pull_request) Successful in 14s
test / unit (pull_request) Successful in 54s
test / integration-docker (pull_request) Successful in 54s
test / coverage (pull_request) Successful in 18s
test / integration-docker (push) Successful in 16s
test / unit (push) Successful in 50s
lint / lint (push) Successful in 1m0s
test / coverage (push) Successful in 15s
Update Quality Badges / update-badges (push) Successful in 4m1s

This commit was merged in pull request #490.
This commit is contained in:
2026-07-26 06:14:35 +00:00
committed by didericis
parent 39167528db
commit d3370a88bb
8 changed files with 42 additions and 135 deletions
+26
View File
@@ -0,0 +1,26 @@
name: prd-number-check
on:
pull_request:
types: [opened, reopened, synchronize]
jobs:
require-numbered-prds:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Reject unnumbered PRDs
run: |
unnumbered=$(find docs/prds -maxdepth 1 -type f \
-name 'prd-new-*.md' -print | sort)
if [ -n "$unnumbered" ]; then
echo "::error::Assign every new PRD its final sequential number before merge."
echo "Unnumbered PRDs:"
echo "$unnumbered"
exit 1
fi
echo "All PRDs have final numbers."
-122
View File
@@ -1,122 +0,0 @@
# Assign sequential numbers to prd-new-*.md files on merge to main.
#
# When a PR merges to main and includes prd-new-*.md files this workflow:
# 1. Finds the next available NNNN number by scanning existing PRDs.
# 2. Renames each prd-new-*.md to NNNN-<slug>.md.
# 3. Updates the title header (# PRD prd-new: → # PRD NNNN:).
# 4. Flips Status: Draft → Active when the push touched files outside
# docs/prds/ anywhere in its commit range (i.e. the implementation
# shipped together with the PRD).
# 5. Commits the renaming back to main.
#
# No-op if the working tree contains no prd-new-*.md files.
#
# NOTE: The workflow scans the working tree (not just HEAD~1..HEAD) because
# PRs land as multi-commit pushes and the prd-new file is often added in an
# earlier commit on the branch, not in the final squash/merge commit.
name: prd-number
on:
push:
branches:
- main
paths:
- 'docs/prds/prd-new-*.md'
jobs:
assign-numbers:
runs-on: ubuntu-latest
permissions:
contents: write
steps:
- name: Checkout
uses: actions/checkout@v4
with:
fetch-depth: 0
token: ${{ secrets.GITHUB_TOKEN }}
# No actions/setup-python: the inline script is stdlib-only on the
# image's system Python 3.12 (older act_runner mishandles its PATH).
- name: Configure git
run: |
git config user.name "github-actions[bot]"
git config user.email "github-actions[bot]@users.noreply.github.com"
- name: Assign PRD numbers
run: |
python3 - <<'EOF'
import os
import re
import subprocess
import sys
from pathlib import Path
prds_dir = Path("docs/prds")
# Scan the working tree — prd-new files may have landed in any
# commit of a multi-commit push, not just HEAD.
new_prds = sorted(prds_dir.glob("prd-new-*.md"))
if not new_prds:
print("No prd-new-*.md files found — nothing to do.")
sys.exit(0)
# Determine whether non-PRD files were also changed anywhere in
# the push range (BEFORE_SHA → HEAD). Falls back to HEAD~1 when
# the env var isn't set (e.g. local act runs).
before_sha = os.environ.get("GITHUB_EVENT_BEFORE", "HEAD~1")
all_changed = subprocess.run(
["git", "diff", "--name-only", before_sha, "HEAD"],
capture_output=True, text=True, check=True,
).stdout.splitlines()
non_prd_changed = any(
not f.startswith("docs/prds/") for f in all_changed
)
# Find next available number.
existing = sorted(
int(m.group(1))
for p in prds_dir.glob("*.md")
if (m := re.match(r"^(\d{4})-", p.name))
)
next_num = (max(existing) + 1) if existing else 1
for prd_path in sorted(new_prds):
slug = re.sub(r"^prd-new-", "", prd_path.stem)
new_name = f"{next_num:04d}-{slug}.md"
new_path = prds_dir / new_name
print(f" {prd_path.name} → {new_name}")
content = prd_path.read_text()
# Update title header.
content = re.sub(
r"^(#\s+PRD\s+)prd-new(:)",
rf"\g<1>{next_num:04d}\2",
content,
count=1,
flags=re.MULTILINE,
)
# Conditionally flip Status.
if non_prd_changed:
content = re.sub(
r"(\*\*Status:\*\*\s*)Draft",
r"\g<1>Active",
content,
count=1,
)
new_path.write_text(content)
subprocess.run(["git", "rm", str(prd_path)], check=True)
subprocess.run(["git", "add", str(new_path)], check=True)
next_num += 1
subprocess.run(
["git", "commit", "-m", "ci(prd): assign sequential numbers to new PRDs"],
check=True,
)
subprocess.run(["git", "push"], check=True)
EOF
+4 -4
View File
@@ -44,10 +44,10 @@ backend remains available with `BOT_BOTTLE_BACKEND=docker` or
- Three kinds of doc, each with its own conventions in-folder; see - Three kinds of doc, each with its own conventions in-folder; see
`docs/README.md` for when to write which: `docs/README.md` for when to write which:
- **PRDs** (`docs/prds/`) — one feature per file. While a PR is open - **PRDs** (`docs/prds/`) — one feature per file. A draft may initially
the file is named `prd-new-<kebab>.md`; CI assigns a sequential use `prd-new-<kebab>.md`, but its author must assign the next
number on merge to `main` and renames it. A `Status:` line tracks sequential number before merge; CI rejects unnumbered PRDs. A
lifecycle: Draft → Active (shipped to `main`) → `Status:` line tracks lifecycle: Draft → Active (shipped to `main`) →
Superseded/Retargeted. Format in `docs/prds/README.md`. Superseded/Retargeted. Format in `docs/prds/README.md`.
- **Research notes** (`docs/research/`) — opinionated investigations; - **Research notes** (`docs/research/`) — opinionated investigations;
unnumbered kebab-case, freeform and verdict-first. See unnumbered kebab-case, freeform and verdict-first. See
@@ -113,7 +113,7 @@ _MIGRATIONS = TableMigrations(
# egress allowlist / routes / git config selected by source IP. The # egress allowlist / routes / git config selected by source IP. The
# multi-tenant gateway resolves it per request via `attribute`. # multi-tenant gateway resolves it per request via `attribute`.
"ALTER TABLE orchestrator_bottles ADD COLUMN policy TEXT NOT NULL DEFAULT ''", "ALTER TABLE orchestrator_bottles ADD COLUMN policy TEXT NOT NULL DEFAULT ''",
# v4 — per-bottle encrypted egress secrets (PRD prd-new-secret-provider). # v4 — per-bottle encrypted egress secrets (PRD 0080).
# One row per env-var: key (env-var name) is plaintext for auditing; # One row per env-var: key (env-var name) is plaintext for auditing;
# value is the encrypted token string. The encryption key (ENV_VAR_SECRET) # value is the encrypted token string. The encryption key (ENV_VAR_SECRET)
# lives only in the agent's environment — a row alone cannot recover the # lives only in the agent's environment — a row alone cannot recover the
@@ -1,4 +1,4 @@
"""Symmetric encryption for per-bottle egress secrets (PRD prd-new-secret-provider). """Symmetric encryption for per-bottle egress secrets (PRD 0080).
Each agent receives a random ENV_VAR_SECRET at startup — passed as an env var, Each agent receives a random ENV_VAR_SECRET at startup — passed as an env var,
never logged or persisted. The host uses this key to encrypt each egress auth never logged or persisted. The host uses this key to encrypt each egress auth
@@ -1,4 +1,4 @@
# PRD prd-new: Encrypted at-rest egress secrets (SecretProvider, interim slice) # PRD 0080: Encrypted at-rest egress secrets (SecretProvider, interim slice)
- **Status:** Draft - **Status:** Draft
- **Author:** didericis - **Author:** didericis
+8 -5
View File
@@ -7,10 +7,13 @@ document vs. a research note or a decision record).
## Naming and numbering ## Naming and numbering
New PRDs use a `prd-new-<kebab-title>.md` placeholder name while the PR New PRDs may use a `prd-new-<kebab-title>.md` placeholder name while the
is open. On merge to `main` a CI workflow assigns the next sequential design is being drafted. Before merge, assign the next sequential number
number (`0024-…`, `0025-…`), renames the file, and updates the title after the highest-numbered PRD on `main`, rename the file to
header. Numbers are never reused; gaps are fine. `NNNN-<kebab-title>.md`, and update the title header. CI blocks merging
while any `prd-new-*.md` placeholder remains. If concurrent PRs select the
same number, the later PR must take the next available number before it
merges. Numbers are never reused; gaps are fine.
Once numbered, the filename stays fixed for the life of the doc. Once numbered, the filename stays fixed for the life of the doc.
@@ -26,7 +29,7 @@ The `Status:` line near the top tracks the PRD's lifecycle:
## Format ## Format
```markdown ```markdown
# PRD prd-new: <short title> ← placeholder; CI fills in the number on merge # PRD prd-new: <short title> ← replace with the final number before merge
- **Status:** Draft - **Status:** Draft
- **Author:** <who> - **Author:** <who>
+1 -1
View File
@@ -1,4 +1,4 @@
"""Unit tests for per-bottle egress secret encryption (PRD prd-new-secret-provider).""" """Unit tests for per-bottle egress secret encryption (PRD 0080)."""
from __future__ import annotations from __future__ import annotations