diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index f2b237c..d9ef9cd 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -6,6 +6,7 @@ on: branches: - main - v2 + - v2.1 permissions: contents: read @@ -22,6 +23,29 @@ jobs: with: fetch-depth: 0 + - name: Enforce pull request branch policy + if: github.event_name == 'pull_request' + shell: bash + env: + BASE_REF: ${{ github.base_ref }} + HEAD_REF: ${{ github.head_ref }} + run: | + if [[ "$BASE_REF" == "v2.1" ]]; then + if [[ "$HEAD_REF" =~ ^(feat|fix|test|chore)/.+$ ]]; then + exit 0 + fi + echo "::error::Pull requests into v2.1 must come from feat/*, fix/*, test/*, or chore/* branches." + exit 1 + fi + + if [[ "$BASE_REF" == "main" ]]; then + if [[ "$HEAD_REF" == "v2.1" || "$HEAD_REF" =~ ^hotfix/.+$ ]]; then + exit 0 + fi + echo "::error::Only v2.1 release promotion or an explicit hotfix/* branch may target main." + exit 1 + fi + - name: Set up Node.js uses: actions/setup-node@v4 with: diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..00795a3 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,28 @@ +# Repository workflow policy + +## Protected branches + +- `main` is production-only. `v2.1` is the active integration branch for the v2.1.0 development cycle. +- Never commit feature, fix, test, or chore work directly to `main` or `v2.1`. + +## Development work + +- Create short-lived `feat/*`, `fix/*`, `test/*`, or `chore/*` branches from `v2.1`. +- Sprint and routine development pull requests target `v2.1`, not `main`. +- After a successful merge, delete only the merged short-lived branch when cleanup is authorized. + +## Merge authority + +- Creating a pull request and merging it are separate operations. +- An agent must not merge its own pull request automatically. Passing CI does not authorize a merge. +- Merge only when the user explicitly requests that specific merge after review. Never enable auto-merge without an explicit request. + +## Production promotion + +- Normal development reaches `main` only through a dedicated release or hardening pull request from `v2.1`. +- Creating the v2.1.0 tag or release requires separate explicit authorization after final verification. + +## Hotfixes + +- Production hotfixes use a dedicated `hotfix/*` branch and pull request into `main`. +- Never push a hotfix directly to `main`. diff --git a/docs/README.md b/docs/README.md index 251725f..8ac2650 100644 --- a/docs/README.md +++ b/docs/README.md @@ -7,6 +7,7 @@ The root [README](../README.md) introduces Secure Tools. This directory owns det | Document | Responsibility | | --- | --- | | [Architecture](./architecture.md) | Static application structure, shared browser foundations, delivery, and future ecosystem direction | +| [Development workflow](./development-workflow.md) | Production, integration, Sprint, release, hotfix, merge-authority, and branch-cleanup rules | | [Privacy model](./privacy-model.md) | Local-processing and network boundaries, storage, security controls, and bounded privacy claims | | [Dependencies](./dependencies.md) | Production runtime inventory, versions, vendoring, licenses, and integrity ownership | | [Local OCR foundation](./ocr-foundation.md) | Self-hosted Tesseract assets, languages, lifecycle, cancellation, caching, and privacy guarantees | diff --git a/docs/development-workflow.md b/docs/development-workflow.md new file mode 100644 index 0000000..44170aa --- /dev/null +++ b/docs/development-workflow.md @@ -0,0 +1,43 @@ +# Development workflow + +Secure Tools separates product versions from Sprint numbers. A Sprint is a bounded unit of work within a product development cycle; it does not create a version, tag, or release by itself. Sprint 16B, for example, belongs to the v2.1.0 development cycle. + +## Branch roles + +- `main` is the production branch. Routine development does not target it. +- `v2.1` is the integration branch for the v2.1.0 cycle. +- Short-lived `feat/*`, `fix/*`, `test/*`, and `chore/*` branches start from `v2.1` and return through pull requests into `v2.1`. +- Direct feature or fix commits to `main` or `v2.1` are prohibited. + +```text +main (production) + ↑ +release PR after hardening + ↑ +v2.1 (integration) + ↑ +Sprint PRs + ↑ +feat/* fix/* test/* chore/* +``` + +## Sprint delivery + +1. Update local `v2.1` from `origin/v2.1`. +2. Create a short-lived branch from that exact integration state. +3. Commit and validate only the Sprint’s intended changes. +4. Open a pull request into `v2.1` and wait for required CI. +5. Treat review and merge as a separate step. An agent does not merge its own pull request or enable auto-merge unless the user explicitly authorizes that specific action. +6. After a successful merge and verification, remove the merged short-lived branch when branch cleanup is authorized. + +## Production release + +After the v2.1.0 scope is integrated, complete release hardening and final verification on `v2.1`. Promote it through a dedicated `v2.1` → `main` pull request. Only after that pull request is explicitly reviewed and merged may a separately authorized task create the v2.1.0 tag and release. + +## Hotfixes + +Urgent production fixes use a dedicated `hotfix/*` branch and pull request into `main`. They are never pushed directly. After production verification, carry the correction back into the active integration line as needed through an appropriate pull request. + +## Enforced pull request policy + +CI permits routine `feat/*`, `fix/*`, `test/*`, and `chore/*` pull requests into `v2.1`. Pull requests into `main` pass the branch-policy gate only when the head is exactly `v2.1` or a dedicated `hotfix/*` branch. The repository protects both long-lived branches with required pull requests, the existing `Validate static tools` check, resolved review conversations, blocked force pushes, and blocked deletion. Because the repository currently has one maintainer, an approving-review count is not required; explicit merge authorization remains mandatory. diff --git a/tests/ci-foundation.test.mjs b/tests/ci-foundation.test.mjs index 9d3b9cf..63ef1c3 100644 --- a/tests/ci-foundation.test.mjs +++ b/tests/ci-foundation.test.mjs @@ -4,11 +4,19 @@ import fs from "node:fs"; const workflow = fs.readFileSync(".github/workflows/ci.yml", "utf8"); assert.match(workflow, /^name: CI$/m); assert.match(workflow, /^\s{2}pull_request:$/m); -assert.match(workflow, /^\s{2}push:\s*$[\s\S]*?^\s{6}- main$[\s\S]*?^\s{6}- v2$/m); +assert.match(workflow, /^\s{2}push:\s*$[\s\S]*?^\s{6}- main$[\s\S]*?^\s{6}- v2$[\s\S]*?^\s{6}- v2\.1$/m); assert.match(workflow, /uses: actions\/checkout@v4/); assert.match(workflow, /uses: actions\/setup-node@v4/); assert.match(workflow, /node-version: 24/); assert.match(workflow, /permissions:\s*\n\s+contents: read/); +assert.match(workflow, /name: Enforce pull request branch policy/); +assert.match(workflow, /if: github\.event_name == 'pull_request'/); +assert.match(workflow, /BASE_REF: \$\{\{ github\.base_ref \}\}/); +assert.match(workflow, /HEAD_REF: \$\{\{ github\.head_ref \}\}/); +assert.match(workflow, /\^\(feat\|fix\|test\|chore\)\/\.\+\$/); +assert.match(workflow, /HEAD_REF" == "v2\.1"/); +assert.match(workflow, /\^hotfix\/\.\+\$/); +assert.match(workflow, /Only v2\.1 release promotion or an explicit hotfix/); assert.match(workflow, /git diff --check/); assert.match(workflow, /run: npm ci --ignore-scripts/); assert.match(workflow, /run: npm run build/);