fix: harden issue status projection
This commit is contained in:
23
.github/issue-management/policy.mjs
vendored
23
.github/issue-management/policy.mjs
vendored
@@ -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 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 TYPES = new Set(['Idea', 'Feature', 'Bug', 'Research', 'Task'])
|
||||||
const PRIORITIES = ['p0', 'p1', 'p2', 'p3']
|
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.
|
* Return Markdown outside balanced details elements.
|
||||||
@@ -418,8 +423,7 @@ async function ensureProjectItem(number) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function setStatus(number, status) {
|
async function updateStatus(context, status) {
|
||||||
const context = await ensureProjectItem(number)
|
|
||||||
const option = context.statusField.options.find((candidate) => candidate.name === status)
|
const option = context.statusField.options.find((candidate) => candidate.name === status)
|
||||||
if (!option) throw new Error(`Status 不存在:${status}`)
|
if (!option) throw new Error(`Status 不存在:${status}`)
|
||||||
if (context.item.fieldValueByName?.name === status) return
|
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) {
|
async function upsertAudit(number, errors) {
|
||||||
const comments = await api(
|
const comments = await api(
|
||||||
`/repos/${config.organization}/${config.repository}/issues/${number}/comments?per_page=100`,
|
`/repos/${config.organization}/${config.repository}/issues/${number}/comments?per_page=100`,
|
||||||
@@ -510,11 +518,12 @@ async function pullRequestSnapshot(number) {
|
|||||||
|
|
||||||
async function advanceResolvingIssues(pull) {
|
async function advanceResolvingIssues(pull) {
|
||||||
for (const number of pull.references.resolving) {
|
for (const number of pull.references.resolving) {
|
||||||
const current = await issueSnapshot(number)
|
const context = await projectContext(number)
|
||||||
if (!current) continue
|
const target = nextResolvingIssueStatus(context.item?.fieldValueByName?.name ?? null, pull)
|
||||||
const target = nextResolvingIssueStatus(current.status, pull)
|
|
||||||
if (!target) continue
|
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)
|
await auditIssue(number)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -241,13 +241,19 @@ export function gatesForMode(selected: Mode): Gate[] {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function ciPrimaryGates(): Gate[] {
|
function ciSharedStaticGates(): Gate[] {
|
||||||
return [
|
return [
|
||||||
pnpmScript('runtime-closure', 'verify-runtime-closure', { label: 'runtime closure' }),
|
pnpmScript('runtime-closure', 'verify-runtime-closure', { label: 'runtime closure' }),
|
||||||
pnpmScript('constraints', 'constraints'),
|
pnpmScript('constraints', 'constraints'),
|
||||||
pnpmScript('package-invariants', 'verify-package-invariants', { label: 'package invariants' }),
|
pnpmScript('package-invariants', 'verify-package-invariants', { label: 'package invariants' }),
|
||||||
pnpmScript('cordis-config', 'verify-cordis-config', { label: 'Cordis config' }),
|
pnpmScript('cordis-config', 'verify-cordis-config', { label: 'Cordis config' }),
|
||||||
pnpmScript('issue-management', 'test:issue-management', { label: 'Issue management policy' }),
|
pnpmScript('issue-management', 'test:issue-management', { label: 'Issue management policy' }),
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
function ciPrimaryGates(): Gate[] {
|
||||||
|
return [
|
||||||
|
...ciSharedStaticGates(),
|
||||||
pnpmScript('typecheck', 'typecheck'),
|
pnpmScript('typecheck', 'typecheck'),
|
||||||
lintGate(),
|
lintGate(),
|
||||||
pnpmScript('duplication', 'duplication'),
|
pnpmScript('duplication', 'duplication'),
|
||||||
@@ -341,11 +347,7 @@ function runningNodeMajor(): number {
|
|||||||
|
|
||||||
function ciStaticGates(options: { ownsBuild: boolean }): Gate[] {
|
function ciStaticGates(options: { ownsBuild: boolean }): Gate[] {
|
||||||
return [
|
return [
|
||||||
pnpmScript('runtime-closure', 'verify-runtime-closure', { label: 'runtime closure' }),
|
...ciSharedStaticGates(),
|
||||||
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' }),
|
|
||||||
...options.ownsBuild ? [pnpmScript('build', 'build')] : [],
|
...options.ownsBuild ? [pnpmScript('build', 'build')] : [],
|
||||||
...docSyncLeafGates({
|
...docSyncLeafGates({
|
||||||
includeDocTypecheck: options.ownsBuild,
|
includeDocTypecheck: options.ownsBuild,
|
||||||
|
|||||||
Reference in New Issue
Block a user