feat(issue-create): 建立議題,標籤限既有、以標題查重
標籤以名稱指定,送出前換成 repo 上既有標籤的 id;指到不存在的標籤即中止並列出 可選項目。本工具不具備建立標籤的能力,錯字當場講清楚,不悄悄少貼一個。 以標題查重,同名議題已存在就回傳既有那一顆並把 created 設為 false,讓中斷後 重跑不產生重複議題。先驗標籤再查重:參數打錯要立刻講。 --dry-run 不寫入,但會讀。要讓預覽忠實反映將送出的請求,就得先把標籤名稱換成 id、也得先查過重——否則預覽看起來會建一顆議題,實跑卻是 no-op,或反過來實跑 才爆標籤錯字。同名議題已存在時,預覽的 requests 為空陣列並附上既有議題編號。 body 由 --body-file 讀入後原樣送出,避免長 markdown 擠在命令列上。 Closes #4 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,120 @@
|
|||||||
|
#!/usr/bin/env node
|
||||||
|
/**
|
||||||
|
* 建立議題。
|
||||||
|
*
|
||||||
|
* 兩個刻意的限制:
|
||||||
|
* - 標籤只能指定 repo 上已經存在的,指到不存在的就中止。本工具不建立標籤。
|
||||||
|
* - 以標題查重,同名議題已存在就回傳既有那一顆,讓中斷後重跑不產生重複議題。
|
||||||
|
*
|
||||||
|
* `--dry-run` 不寫入,但會讀:要讓預覽忠實反映將送出的請求,就得先把標籤名稱換成 id、
|
||||||
|
* 也得先查過重——否則預覽看起來會建一顆議題,實跑卻是 no-op,或反過來實跑才爆標籤錯字。
|
||||||
|
*
|
||||||
|
* 用法:
|
||||||
|
* node scripts/issue-create.js --repo owner/name --title <標題> --body-file <路徑>
|
||||||
|
* [--labels a,b] [--host <網址>] [--dry-run]
|
||||||
|
*/
|
||||||
|
import { existsSync, readFileSync } from 'node:fs';
|
||||||
|
import {
|
||||||
|
ScriptError,
|
||||||
|
expectOk,
|
||||||
|
findIssueByTitle,
|
||||||
|
giteaRequest,
|
||||||
|
listLabels,
|
||||||
|
main,
|
||||||
|
parseFlags,
|
||||||
|
parseRepo,
|
||||||
|
preflight,
|
||||||
|
resolveLogin,
|
||||||
|
} from './lib.js';
|
||||||
|
|
||||||
|
main(async () => {
|
||||||
|
const flags = parseFlags(process.argv.slice(2), {
|
||||||
|
required: ['repo', 'title', 'body-file'],
|
||||||
|
optional: ['labels', 'host'],
|
||||||
|
booleans: ['dry-run'],
|
||||||
|
});
|
||||||
|
const repo = parseRepo(flags.repo);
|
||||||
|
const title = flags.title.trim();
|
||||||
|
const body = readBody(flags['body-file']);
|
||||||
|
const labelNames = splitLabels(flags.labels);
|
||||||
|
const path = `/repos/${repo}/issues`;
|
||||||
|
const dryRun = flags['dry-run'] === true;
|
||||||
|
|
||||||
|
const login = resolveLogin({ host: flags.host });
|
||||||
|
// 試跑不做前置檢查:那一層擋的是寫入能力與時間追蹤,而試跑本來就不寫。
|
||||||
|
if (!dryRun) await preflight(login, repo);
|
||||||
|
|
||||||
|
// 先驗標籤再查重:參數打錯要立刻講,不要等到重跑時才發現
|
||||||
|
const labelIds = labelNames.length > 0 ? await resolveLabelIds(login, repo, labelNames) : null;
|
||||||
|
|
||||||
|
const existing = await findIssueByTitle(login, repo, title);
|
||||||
|
|
||||||
|
const payload = { title, body };
|
||||||
|
if (labelIds) payload.labels = labelIds;
|
||||||
|
|
||||||
|
if (dryRun) {
|
||||||
|
return {
|
||||||
|
dryRun: true,
|
||||||
|
repo,
|
||||||
|
labels: labelNames,
|
||||||
|
existing: existing ? { number: existing.number, url: existing.html_url } : null,
|
||||||
|
// 同名議題已存在時實跑是 no-op,預覽就不該顯示將建立議題
|
||||||
|
requests: existing ? [] : [{ method: 'POST', path, body: payload }],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (existing) {
|
||||||
|
return {
|
||||||
|
repo,
|
||||||
|
number: existing.number,
|
||||||
|
url: existing.html_url,
|
||||||
|
title: existing.title,
|
||||||
|
created: false,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const issue = expectOk(await giteaRequest(login, 'POST', path, { body: payload }), `POST ${path}`);
|
||||||
|
|
||||||
|
return {
|
||||||
|
repo,
|
||||||
|
number: issue.number,
|
||||||
|
url: issue.html_url,
|
||||||
|
title: issue.title,
|
||||||
|
labels: (issue.labels ?? []).map((label) => label.name),
|
||||||
|
created: true,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
function readBody(bodyFile) {
|
||||||
|
if (!existsSync(bodyFile)) {
|
||||||
|
throw new ScriptError('BODY_FILE_MISSING', `找不到 --body-file 指定的檔案 ${bodyFile}`);
|
||||||
|
}
|
||||||
|
return readFileSync(bodyFile, 'utf8');
|
||||||
|
}
|
||||||
|
|
||||||
|
function splitLabels(value) {
|
||||||
|
if (value === undefined) return [];
|
||||||
|
return value
|
||||||
|
.split(',')
|
||||||
|
.map((name) => name.trim())
|
||||||
|
.filter((name) => name !== '');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 把標籤名稱換成 repo 上既有標籤的 id。指到不存在的標籤一律中止 ——
|
||||||
|
* 本工具不建立標籤,錯字應該當場講清楚,而不是悄悄少貼一個。
|
||||||
|
*/
|
||||||
|
async function resolveLabelIds(login, repo, names) {
|
||||||
|
const labels = await listLabels(login, repo);
|
||||||
|
const byName = new Map(labels.map((label) => [label.name, label.id]));
|
||||||
|
|
||||||
|
const missing = names.filter((name) => !byName.has(name));
|
||||||
|
if (missing.length > 0) {
|
||||||
|
throw new ScriptError(
|
||||||
|
'UNKNOWN_LABEL',
|
||||||
|
`${repo} 沒有這些標籤:${missing.join('、')}。` +
|
||||||
|
`本工具不建立標籤,請改挑既有的:${[...byName.keys()].join('、') || '(這個 repo 目前沒有任何標籤)'}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return names.map((name) => byName.get(name));
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user