From 3b95c0e4e0077ed9e3e6f4c4f180bfa7752535a1 Mon Sep 17 00:00:00 2001 From: maruson08 Date: Sat, 26 Sep 2026 11:39:07 +0900 Subject: [PATCH] =?UTF-8?q?=E2=9A=99=EF=B8=8F[Chore]=20Bootstrap=20v2.2=20?= =?UTF-8?q?development=20cycle?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/ci.yml | 29 +++++-------------- AGENTS.md | 12 ++++---- docs/development-workflow.md | 18 ++++++------ scripts/validate-branch-policy.mjs | 45 ++++++++++++++++++++++++++++++ tests/branch-policy.test.mjs | 44 +++++++++++++++++++++++++++++ tests/ci-foundation.test.mjs | 8 ++---- tests/run-all.mjs | 1 + 7 files changed, 115 insertions(+), 42 deletions(-) create mode 100644 scripts/validate-branch-policy.mjs create mode 100644 tests/branch-policy.test.mjs diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index d5ef3a5..4d13473 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -6,7 +6,7 @@ on: branches: - main - v2 - - v2.1 + - v2.2 permissions: contents: read @@ -23,33 +23,18 @@ jobs: with: fetch-depth: 0 + - name: Set up Node.js + uses: actions/setup-node@v4 + with: + node-version: 24 + - 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: - node-version: 24 + run: node scripts/validate-branch-policy.mjs "$BASE_REF" "$HEAD_REF" - name: Enforce commit and pull request title conventions if: github.event_name == 'pull_request' diff --git a/AGENTS.md b/AGENTS.md index 38c1065..c1e8df3 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -2,13 +2,13 @@ ## 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`. +- `main` is production-only. `v2.2` is the active integration branch for the v2.2.0 development cycle. +- Never commit feature, fix, test, or chore work directly to `main` or `v2.2`. ## 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`. +- Create short-lived `feat/*`, `fix/*`, `test/*`, or `chore/*` branches from `v2.2`. +- Sprint and routine development pull requests target `v2.2`, not `main`. - After a successful merge, delete only the merged short-lived branch when cleanup is authorized. ## Commit messages @@ -30,8 +30,8 @@ ## 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. +- Normal development reaches `main` only through a dedicated release or hardening pull request from `v2.2`. +- Creating the v2.2.0 tag or release requires separate explicit authorization after final verification. ## Hotfixes diff --git a/docs/development-workflow.md b/docs/development-workflow.md index 5ded476..a8df0e6 100644 --- a/docs/development-workflow.md +++ b/docs/development-workflow.md @@ -5,16 +5,16 @@ Secure Tools separates product versions from Sprint numbers. A Sprint is a bound ## 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. +- `v2.2` is the active integration branch for the v2.2.0 cycle. +- Short-lived `feat/*`, `fix/*`, `test/*`, and `chore/*` branches start from `v2.2` and return through pull requests into `v2.2`. +- Direct feature or fix commits to `main` or `v2.2` are prohibited. ```text main (production) ↑ -release PR after hardening +release PR after v2.2 hardening ↑ -v2.1 (integration) +v2.2 (active integration) ↑ Sprint PRs ↑ @@ -23,10 +23,10 @@ feat/* fix/* test/* chore/* ## Sprint delivery -1. Update local `v2.1` from `origin/v2.1`. +1. Update local `v2.2` from `origin/v2.2`. 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. +4. Open a pull request into `v2.2` 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. @@ -40,7 +40,7 @@ CI validates non-merge commits introduced by the pull request’s actual base-to ## 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. +The v2.1.0 cycle is released. The v2.2.0 cycle is active development. After the v2.2.0 scope is integrated, complete release hardening and final verification on `v2.2`. Promote it through a dedicated `v2.2` → `main` pull request. Only after that pull request is explicitly reviewed and merged may a separately authorized task create the v2.2.0 tag and release. ## Hotfixes @@ -48,4 +48,4 @@ Urgent production fixes use a dedicated `hotfix/*` branch and pull request into ## 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. +CI permits routine `feat/*`, `fix/*`, `test/*`, and `chore/*` pull requests into `v2.2`. Pull requests into `main` pass the branch-policy gate only when the head is exactly `v2.2` 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/scripts/validate-branch-policy.mjs b/scripts/validate-branch-policy.mjs new file mode 100644 index 0000000..f853482 --- /dev/null +++ b/scripts/validate-branch-policy.mjs @@ -0,0 +1,45 @@ +import { fileURLToPath } from "node:url"; + +export const ACTIVE_INTEGRATION_BRANCH = "v2.2"; + +const routineBranch = /^(?:feat|fix|test|chore)\/.+$/; +const hotfixBranch = /^hotfix\/.+$/; + +export function validateBranchPolicy(baseRef, headRef) { + if (baseRef === ACTIVE_INTEGRATION_BRANCH) { + return routineBranch.test(headRef) + ? { valid: true, reason: null } + : { + valid: false, + reason: `Pull requests into ${ACTIVE_INTEGRATION_BRANCH} must come from feat/*, fix/*, test/*, or chore/* branches.`, + }; + } + + if (baseRef === "main") { + return headRef === ACTIVE_INTEGRATION_BRANCH || hotfixBranch.test(headRef) + ? { valid: true, reason: null } + : { + valid: false, + reason: `Only ${ACTIVE_INTEGRATION_BRANCH} release promotion or an explicit hotfix/* branch may target main.`, + }; + } + + return { valid: true, reason: null }; +} + +export function run([baseRef, headRef] = process.argv.slice(2)) { + if (!baseRef || !headRef) { + throw new Error("Usage: node scripts/validate-branch-policy.mjs "); + } + const result = validateBranchPolicy(baseRef, headRef); + if (!result.valid) throw new Error(result.reason); + console.log(`Branch policy accepted ${headRef} → ${baseRef}.`); +} + +if (process.argv[1] && fileURLToPath(import.meta.url) === process.argv[1]) { + try { run(); } + catch (error) { + console.error(error.message); + process.exitCode = 1; + } +} diff --git a/tests/branch-policy.test.mjs b/tests/branch-policy.test.mjs new file mode 100644 index 0000000..17c6fbd --- /dev/null +++ b/tests/branch-policy.test.mjs @@ -0,0 +1,44 @@ +import assert from "node:assert/strict"; + +import { + ACTIVE_INTEGRATION_BRANCH, + run, + validateBranchPolicy, +} from "../scripts/validate-branch-policy.mjs"; + +assert.equal(ACTIVE_INTEGRATION_BRANCH, "v2.2"); + +for (const [headRef, baseRef] of [ + ["feat/example", "v2.2"], + ["fix/example", "v2.2"], + ["test/example", "v2.2"], + ["chore/example", "v2.2"], + ["v2.2", "main"], + ["hotfix/example", "main"], +]) { + assert.deepEqual(validateBranchPolicy(baseRef, headRef), { valid: true, reason: null }, `${headRef} → ${baseRef}`); + assert.doesNotThrow(() => run([baseRef, headRef])); +} + +for (const [headRef, baseRef] of [ + ["feat/example", "main"], + ["fix/example", "main"], + ["test/example", "main"], + ["chore/example", "main"], + ["v2.1", "main"], +]) { + const result = validateBranchPolicy(baseRef, headRef); + assert.equal(result.valid, false, `${headRef} → ${baseRef}`); + assert.match(result.reason, /Only v2\.2 release promotion or an explicit hotfix/); + assert.throws(() => run([baseRef, headRef]), /Only v2\.2 release promotion or an explicit hotfix/); +} + +for (const headRef of ["v2.1", "main", "hotfix/example", "feature/example"]) { + const result = validateBranchPolicy("v2.2", headRef); + assert.equal(result.valid, false, `${headRef} → v2.2`); + assert.match(result.reason, /Pull requests into v2\.2 must come from feat\/\*, fix\/\*, test\/\*, or chore\/\*/); +} + +assert.throws(() => run([]), /Usage:/); + +console.log("v2.2 integration, production promotion, hotfix, and rejection branch-policy checks passed."); diff --git a/tests/ci-foundation.test.mjs b/tests/ci-foundation.test.mjs index 478d19f..f4631b9 100644 --- a/tests/ci-foundation.test.mjs +++ b/tests/ci-foundation.test.mjs @@ -4,7 +4,7 @@ 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$[\s\S]*?^\s{6}- v2\.1$/m); +assert.match(workflow, /^\s{2}push:\s*$[\s\S]*?^\s{6}- main$[\s\S]*?^\s{6}- v2$[\s\S]*?^\s{6}- v2\.2$/m); assert.match(workflow, /uses: actions\/checkout@v4/); assert.match(workflow, /uses: actions\/setup-node@v4/); assert.match(workflow, /node-version: 24/); @@ -13,10 +13,8 @@ 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, /run: node scripts\/validate-branch-policy\.mjs "\$BASE_REF" "\$HEAD_REF"/); +assert.doesNotMatch(workflow, /v2\.1/); assert.match(workflow, /name: Enforce commit and pull request title conventions/); assert.match(workflow, /BASE_SHA: \$\{\{ github\.event\.pull_request\.base\.sha \}\}/); assert.match(workflow, /HEAD_SHA: \$\{\{ github\.event\.pull_request\.head\.sha \}\}/); diff --git a/tests/run-all.mjs b/tests/run-all.mjs index f0916d6..2cb414e 100644 --- a/tests/run-all.mjs +++ b/tests/run-all.mjs @@ -45,6 +45,7 @@ for (const test of [ "tests/pdf-metadata.test.mjs", "tests/i18n-quality.test.mjs", "tests/ux-consistency.test.mjs", + "tests/branch-policy.test.mjs", "tests/ci-foundation.test.mjs", "tests/commit-message.test.mjs", "tests/cloudflare-bridge.test.mjs",