From eef3634f2346a33e1db4b9443e563aea6bff425c Mon Sep 17 00:00:00 2001 From: Matysh Date: Fri, 14 Aug 2026 10:48:58 +0300 Subject: [PATCH] chore: drop Project v2 from the process, the docs and the release script MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The owner stopped using GitHub Projects. Most of this is wording, but one part was not: release-prerelease.mjs talked to the Project in code. finishIssues looked up the project id, listed its items and its Status=Done option, and threw when an issue was missing from the board — so the first release that closed an issue would have died on a step with nothing to do with publishing. Found by reading rather than by releasing, which was luck. Closing issues stays, and now strips the status label first. That order is not cosmetic: the invariant that a closed issue carries no status label has broken twice already, both times because a manual step did it the other way round. The close-merged job already does it in this order. The documents now say labels and only labels. The explicit "no longer used" lines are kept on purpose, in PROCESS.md and next to the code that used to sync: a decision that vanishes quietly gets reintroduced a month later by someone who never knew it was made. Issue: #139 User-Visible: no --- scripts/release-prerelease.mjs | 44 +++++++++++++--------------------- test/release-contract.test.mjs | 5 +++- 2 files changed, 21 insertions(+), 28 deletions(-) diff --git a/scripts/release-prerelease.mjs b/scripts/release-prerelease.mjs index 9ef995dc..632883e9 100644 --- a/scripts/release-prerelease.mjs +++ b/scripts/release-prerelease.mjs @@ -30,7 +30,7 @@ export function parsePrereleaseArgs(args) { const values = new Map(); const switches = new Set(); const positionals = []; - const valueNames = new Set(['repo', 'branch', 'project', 'issues']); + const valueNames = new Set(['repo', 'branch', 'issues']); const switchNames = new Set(['check', 'yes']); for (const arg of args) { if (!arg.startsWith('--')) { positionals.push(arg); continue; } @@ -53,7 +53,6 @@ export function parsePrereleaseArgs(args) { tag: positionals[0], repo: values.get('repo') || 'Matysh/houseplan-card', branch: values.get('branch') || 'dev', - projectNumber: values.get('project') || '1', issueOption: values.get('issues') || '', checkOnly: switches.has('check'), confirmed: switches.has('yes'), @@ -184,7 +183,7 @@ const invokedDirectly = process.argv[1] if (invokedDirectly) { try { const { - tag, repo, branch, projectNumber, issueOption, checkOnly, confirmed, + tag, repo, branch, issueOption, checkOnly, confirmed, } = parsePrereleaseArgs(process.argv.slice(2)); let issues = []; const root = process.cwd(); @@ -400,39 +399,30 @@ if (invokedDirectly) { } }; + // Project v2 больше не используется (решение владельца 2026-08-14). Раньше + // здесь выяснялся id проекта, список его элементов и опция Status=Done, а + // отсутствие задачи в проекте роняло публикацию. Статус живёт в метках + // (PROCESS.md §9), и релизу нечего синхронизировать: он закрывает issue и + // снимает статусную метку, как это делает job close-merged (#120). const finishIssues = (releaseUrl) => { if (!issues.length) return; - const projectInfo = ghJson([ - 'project', 'view', projectNumber, '--owner', owner, '--format', 'json', - ]); - const project = ghJson([ - 'project', 'item-list', projectNumber, '--owner', owner, '--limit', '1000', '--format', 'json', - ]); - const fields = ghJson(['project', 'field-list', projectNumber, '--owner', owner, '--format', 'json']); - const statusField = fields.fields.find((field) => field.name === 'Status'); - const done = statusField?.options?.find((entry) => entry.name === 'Done'); - if (!statusField || !done) throw new Error(`Project ${owner}/${projectNumber} has no Status=Done option`); - const items = new Map(project.items - .filter((item) => item.content?.repository === repo && issues.includes(item.content?.number)) - .map((item) => [item.content.number, item])); - const missing = issues.filter((issue) => !items.has(issue)); - if (missing.length) throw new Error(`Issues are missing from Project ${projectNumber}: ${missing.join(', ')}`); - for (const issue of issues) { - const row = ghJson(['issue', 'view', String(issue), '--repo', repo, '--json', 'state']); + const row = ghJson(['issue', 'view', String(issue), '--repo', repo, '--json', 'state,labels']); if (row.state === 'OPEN') { + const status = (row.labels || []) + .map((label) => label.name) + .filter((name) => /^S\d-/.test(name)); + // Метка снимается ДО закрытия: инвариант «закрытый issue не несёт + // статусных меток» ломался уже дважды, и оба раза из-за обратного + // порядка в ручном шаге. + for (const name of status) { + run('gh', ['issue', 'edit', String(issue), '--repo', repo, '--remove-label', name]); + } run('gh', [ 'issue', 'close', String(issue), '--repo', repo, '--reason', 'completed', '--comment', `Реализовано и опубликовано в [${tag}](${releaseUrl}).`, ], { inherit: true }); } - const item = items.get(issue); - if (item.status !== 'Done') { - run('gh', [ - 'project', 'item-edit', '--id', item.id, '--project-id', projectInfo.id, - '--field-id', statusField.id, '--single-select-option-id', done.id, - ]); - } } }; diff --git a/test/release-contract.test.mjs b/test/release-contract.test.mjs index 1529ef9f..ea40e670 100644 --- a/test/release-contract.test.mjs +++ b/test/release-contract.test.mjs @@ -121,10 +121,13 @@ test('local orchestrator validates issue lists and public release assets', () => assert.deepEqual(parseIssueList('63, 56,63'), [63, 56]); assert.throws(() => parseIssueList('63,nope'), /positive issue numbers/); assert.deepEqual(parsePrereleaseArgs([tag, '--issues=63,64', '--yes']), { - tag, repo, branch: 'dev', projectNumber: '1', issueOption: '63,64', + tag, repo, branch: 'dev', issueOption: '63,64', checkOnly: false, confirmed: true, }); assert.throws(() => parsePrereleaseArgs([tag, '--isues=63']), /Unknown or malformed/); + // Project v2 больше не используется: опция снята вместе с синхронизацией + // статуса, и её молчаливое принятие обещало бы работу, которой нет. + assert.throws(() => parsePrereleaseArgs([tag, '--project=1']), /Unknown or malformed/); assert.throws(() => parsePrereleaseArgs([tag, 'extra']), /Exactly one/); const release = { tagName: tag, isDraft: false, isPrerelease: true,