From 1c3133c1dd2a40e4e002d48dd23f4c3648fc05cf Mon Sep 17 00:00:00 2001 From: lanrtop Date: Fri, 11 Sep 2026 21:16:01 +0900 Subject: [PATCH] =?UTF-8?q?ci:=20=E6=9C=8D=E5=8A=A1=E7=AB=AF=20commit=20me?= =?UTF-8?q?ssage=20=E6=A0=A1=E9=AA=8C=EF=BC=88Conventional=20+=20Feature-C?= =?UTF-8?q?onfirmed=20trailer=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit lefthook 在 agent 沙箱(DSH workspace-write)内静默失效的机器层硬补偿, dsh-takeover-readiness T2。 Co-Authored-By: Claude Fable 5 --- .gitea/scripts/check-commit-msg.mjs | 67 +++++++++++++++++++++++++++++ .gitea/workflows/pr-check.yml | 11 +++++ 2 files changed, 78 insertions(+) create mode 100644 .gitea/scripts/check-commit-msg.mjs diff --git a/.gitea/scripts/check-commit-msg.mjs b/.gitea/scripts/check-commit-msg.mjs new file mode 100644 index 0000000..fab783e --- /dev/null +++ b/.gitea/scripts/check-commit-msg.mjs @@ -0,0 +1,67 @@ +// CI 侧 commit message 校验:Conventional Commits + feat 须带 Feature-Confirmed trailer。 +// 与 lefthook commit-msg 同一套规则的服务端硬补偿——agent 沙箱(如 DSH workspace-write) +// 内 lefthook 会静默失效,本脚本保证机器校验层不缺位(dsh-takeover-readiness T2)。 +// 环境变量:EVENT_NAME / BEFORE_SHA / BASE_SHA / HEAD_SHA(由 workflow 从 github context 注入) +import { execSync } from 'node:child_process'; + +const git = (cmd) => execSync(`git ${cmd}`, { encoding: 'utf8' }).trim(); + +const CONVENTIONAL = + /^(feat|fix|docs|chore|refactor|ci|perf|style|test|build|revert)(\(.+\))?!?: .+/; +const ZERO_SHA = /^0+$/; + +const { EVENT_NAME, BEFORE_SHA, BASE_SHA, HEAD_SHA } = process.env; + +let range; +if (EVENT_NAME === 'pull_request' && BASE_SHA) { + range = `${BASE_SHA}..${HEAD_SHA}`; +} else if (BEFORE_SHA && !ZERO_SHA.test(BEFORE_SHA)) { + range = `${BEFORE_SHA}..${HEAD_SHA}`; +} else { + // 新分支首推等场景拿不到 before:与 master 比对兜底,再退化为仅查 HEAD + try { + git('fetch origin master --quiet'); + range = `origin/master..${HEAD_SHA || 'HEAD'}`; + } catch { + range = `${HEAD_SHA || 'HEAD'} -1`; + } +} + +let shas; +try { + shas = git(`rev-list --no-merges ${range}`).split('\n').filter(Boolean); +} catch { + // force push 后旧 before 在 clone 里不存在等场景:退化为与 master 比对,再退化为仅查 HEAD + try { + git('fetch origin master --quiet'); + range = `origin/master..${HEAD_SHA || 'HEAD'}`; + shas = git(`rev-list --no-merges ${range}`).split('\n').filter(Boolean); + } catch { + range = 'HEAD -1'; + shas = [git('rev-parse HEAD')]; + } +} +console.log(`commit message 校验:范围 ${range},共 ${shas.length} 个 commit`); + +const errors = []; +for (const sha of shas) { + const subject = git(`log -1 --format=%s ${sha}`); + if (!CONVENTIONAL.test(subject)) { + errors.push(`${sha.slice(0, 8)} 首行不符合 Conventional Commits:${subject}`); + continue; + } + if (/^feat/.test(subject)) { + const body = git(`log -1 --format=%B ${sha}`); + if (!body.includes('Feature-Confirmed: true')) { + errors.push(`${sha.slice(0, 8)} feat 提交缺少 "Feature-Confirmed: true" trailer:${subject}`); + } + } +} + +if (errors.length > 0) { + console.error('\n❌ commit message 校验失败:'); + for (const e of errors) console.error(` ${e}`); + console.error('\n 规则:首行须为 (): ;feat 提交末尾须有 Feature-Confirmed: true'); + process.exit(1); +} +console.log('✅ commit message 校验通过'); diff --git a/.gitea/workflows/pr-check.yml b/.gitea/workflows/pr-check.yml index 3bf1d23..1a2733d 100644 --- a/.gitea/workflows/pr-check.yml +++ b/.gitea/workflows/pr-check.yml @@ -12,6 +12,8 @@ jobs: runs-on: windows steps: - uses: actions/checkout@v4 + with: + fetch-depth: 0 - uses: pnpm/action-setup@v4 with: @@ -23,6 +25,15 @@ jobs: node-version: '20' cache: 'pnpm' + # 服务端 commit message 校验(lefthook 在 agent 沙箱内会静默失效,此为机器层硬补偿) + - name: Commit message 校验 + env: + EVENT_NAME: ${{ github.event_name }} + BEFORE_SHA: ${{ github.event.before }} + BASE_SHA: ${{ github.event.pull_request.base.sha }} + HEAD_SHA: ${{ github.sha }} + run: node .gitea/scripts/check-commit-msg.mjs + - run: pnpm install --frozen-lockfile - run: pnpm typecheck - run: pnpm test