chore: drop Project v2 from the process, the docs and the release script

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
This commit is contained in:
Matysh
2026-08-14 10:48:58 +03:00
parent 6ecbedfb85
commit eef3634f23
2 changed files with 21 additions and 28 deletions
+17 -27
View File
@@ -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,
]);
}
}
};