diff --git a/.gitea/workflows/prd-number-check.yml b/.gitea/workflows/prd-number-check.yml new file mode 100644 index 00000000..1c493b39 --- /dev/null +++ b/.gitea/workflows/prd-number-check.yml @@ -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." diff --git a/.gitea/workflows/prd-number.yml b/.gitea/workflows/prd-number.yml deleted file mode 100644 index 0784b1dc..00000000 --- a/.gitea/workflows/prd-number.yml +++ /dev/null @@ -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-.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 diff --git a/AGENTS.md b/AGENTS.md index 12c77420..27bfdf23 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -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 `docs/README.md` for when to write which: - - **PRDs** (`docs/prds/`) — one feature per file. While a PR is open - the file is named `prd-new-.md`; CI assigns a sequential - number on merge to `main` and renames it. A `Status:` line tracks - lifecycle: Draft → Active (shipped to `main`) → + - **PRDs** (`docs/prds/`) — one feature per file. A draft may initially + use `prd-new-.md`, but its author must assign the next + sequential number before merge; CI rejects unnumbered PRDs. A + `Status:` line tracks lifecycle: Draft → Active (shipped to `main`) → Superseded/Retargeted. Format in `docs/prds/README.md`. - **Research notes** (`docs/research/`) — opinionated investigations; unnumbered kebab-case, freeform and verdict-first. See diff --git a/bot_bottle/orchestrator/store/registry_store.py b/bot_bottle/orchestrator/store/registry_store.py index 05a2eb66..f6284929 100644 --- a/bot_bottle/orchestrator/store/registry_store.py +++ b/bot_bottle/orchestrator/store/registry_store.py @@ -113,7 +113,7 @@ _MIGRATIONS = TableMigrations( # egress allowlist / routes / git config selected by source IP. The # multi-tenant gateway resolves it per request via `attribute`. "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; # 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 diff --git a/bot_bottle/orchestrator/store/secret_store.py b/bot_bottle/orchestrator/store/secret_store.py index a7a6e51c..7daf0767 100644 --- a/bot_bottle/orchestrator/store/secret_store.py +++ b/bot_bottle/orchestrator/store/secret_store.py @@ -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, never logged or persisted. The host uses this key to encrypt each egress auth diff --git a/docs/prds/prd-new-secret-provider-encrypted-env.md b/docs/prds/0080-secret-provider-encrypted-env.md similarity index 98% rename from docs/prds/prd-new-secret-provider-encrypted-env.md rename to docs/prds/0080-secret-provider-encrypted-env.md index f01d4516..6f838195 100644 --- a/docs/prds/prd-new-secret-provider-encrypted-env.md +++ b/docs/prds/0080-secret-provider-encrypted-env.md @@ -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 - **Author:** didericis diff --git a/docs/prds/README.md b/docs/prds/README.md index a064c8a3..642c7a36 100644 --- a/docs/prds/README.md +++ b/docs/prds/README.md @@ -7,10 +7,13 @@ document vs. a research note or a decision record). ## Naming and numbering -New PRDs use a `prd-new-.md` placeholder name while the PR -is open. On merge to `main` a CI workflow assigns the next sequential -number (`0024-…`, `0025-…`), renames the file, and updates the title -header. Numbers are never reused; gaps are fine. +New PRDs may use a `prd-new-.md` placeholder name while the +design is being drafted. Before merge, assign the next sequential number +after the highest-numbered PRD on `main`, rename the file to +`NNNN-.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. @@ -26,7 +29,7 @@ The `Status:` line near the top tracks the PRD's lifecycle: ## Format ```markdown -# PRD prd-new: ← placeholder; CI fills in the number on merge +# PRD prd-new: ← replace with the final number before merge - **Status:** Draft - **Author:** diff --git a/tests/unit/test_orchestrator_secret_store.py b/tests/unit/test_orchestrator_secret_store.py index 2b0e0d51..7301b126 100644 --- a/tests/unit/test_orchestrator_secret_store.py +++ b/tests/unit/test_orchestrator_secret_store.py @@ -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