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