Browse Source

fix: harden issue status projection

Tianyi Cui 2 tháng trước cách đây
mục cha
commit
7aa0ae34b3
2 tập tin đã thay đổi với 24 bổ sung và 13 xóa
  1. 16 7
      .github/issue-management/policy.mjs
  2. 8 6
      scripts/run-gates.ts

+ 16 - 7
.github/issue-management/policy.mjs

@@ -12,7 +12,12 @@ const AUDIT_MARKER = '<!-- dsh-issue-policy -->'
 const OWNER_LINE = /^Owner: @([A-Za-z0-9](?:[A-Za-z0-9-]{0,37}[A-Za-z0-9])?)$/
 const TYPES = new Set(['Idea', 'Feature', 'Bug', 'Research', 'Task'])
 const PRIORITIES = ['p0', 'p1', 'p2', 'p3']
-const ACTIVE_STATUS_ORDER = ['Inbox', 'Backlog', 'Ready', 'In progress', 'In review']
+const TERMINAL_STATUSES = new Set(['Done', 'No action'])
+const ACTIVE_STATUS_ORDER = config.statuses.filter((status) => !TERMINAL_STATUSES.has(status))
+
+for (const status of ['In progress', 'In review']) {
+  if (!ACTIVE_STATUS_ORDER.includes(status)) throw new Error(`config.statuses 缺少 ${status}`)
+}
 
 /**
  * Return Markdown outside balanced details elements.
@@ -418,8 +423,7 @@ async function ensureProjectItem(number) {
   }
 }
 
-async function setStatus(number, status) {
-  const context = await ensureProjectItem(number)
+async function updateStatus(context, status) {
   const option = context.statusField.options.find((candidate) => candidate.name === status)
   if (!option) throw new Error(`Status 不存在:${status}`)
   if (context.item.fieldValueByName?.name === status) return
@@ -441,6 +445,10 @@ async function setStatus(number, status) {
   )
 }
 
+async function setStatus(number, status) {
+  await updateStatus(await ensureProjectItem(number), status)
+}
+
 async function upsertAudit(number, errors) {
   const comments = await api(
     `/repos/${config.organization}/${config.repository}/issues/${number}/comments?per_page=100`,
@@ -510,11 +518,12 @@ async function pullRequestSnapshot(number) {
 
 async function advanceResolvingIssues(pull) {
   for (const number of pull.references.resolving) {
-    const current = await issueSnapshot(number)
-    if (!current) continue
-    const target = nextResolvingIssueStatus(current.status, pull)
+    const context = await projectContext(number)
+    const target = nextResolvingIssueStatus(context.item?.fieldValueByName?.name ?? null, pull)
     if (!target) continue
-    await setStatus(number, target)
+    // TODO: Replace this latest-state guard with per-Issue serialization or a
+    // conditional ProjectV2 update; GraphQL currently has no compare-and-swap.
+    await updateStatus(context, target)
     await auditIssue(number)
   }
 }

+ 8 - 6
scripts/run-gates.ts

@@ -241,13 +241,19 @@ export function gatesForMode(selected: Mode): Gate[] {
   }
 }
 
-function ciPrimaryGates(): Gate[] {
+function ciSharedStaticGates(): Gate[] {
   return [
     pnpmScript('runtime-closure', 'verify-runtime-closure', { label: 'runtime closure' }),
     pnpmScript('constraints', 'constraints'),
     pnpmScript('package-invariants', 'verify-package-invariants', { label: 'package invariants' }),
     pnpmScript('cordis-config', 'verify-cordis-config', { label: 'Cordis config' }),
     pnpmScript('issue-management', 'test:issue-management', { label: 'Issue management policy' }),
+  ]
+}
+
+function ciPrimaryGates(): Gate[] {
+  return [
+    ...ciSharedStaticGates(),
     pnpmScript('typecheck', 'typecheck'),
     lintGate(),
     pnpmScript('duplication', 'duplication'),
@@ -341,11 +347,7 @@ function runningNodeMajor(): number {
 
 function ciStaticGates(options: { ownsBuild: boolean }): Gate[] {
   return [
-    pnpmScript('runtime-closure', 'verify-runtime-closure', { label: 'runtime closure' }),
-    pnpmScript('constraints', 'constraints'),
-    pnpmScript('package-invariants', 'verify-package-invariants', { label: 'package invariants' }),
-    pnpmScript('cordis-config', 'verify-cordis-config', { label: 'Cordis config' }),
-    pnpmScript('issue-management', 'test:issue-management', { label: 'Issue management policy' }),
+    ...ciSharedStaticGates(),
     ...options.ownsBuild ? [pnpmScript('build', 'build')] : [],
     ...docSyncLeafGates({
       includeDocTypecheck: options.ownsBuild,