Files
tea-sdlc/scripts/project-add.js
T
jiantw83andClaude Opus 5 c8f7ab42f2 refactor(參數): parseIndex 抽到 lib,四支腳本共用
同一段 /^[1-9]\d*$/ 與 BAD_INDEX 已經抄了四份。lib 本來就放著同性質的 parseRepo,
這一支該待在它旁邊。

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-17 14:05:19 +08:00

111 lines
3.5 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/usr/bin/env node
/**
* 把議題放進 Gitea 專案看板。
*
* Gitea 1.27 沒有「列出專案」的 endpoint,所以 `--project` 給名稱時只能反查:
* 掃最近 50 筆議題,從它們身上的 projects 欄位湊出 id→名稱對照。全都沒掛看板時
* 就湊不出來,這時請使用者直接貼專案網址(結尾即 id)。
*
* 本工具不建立專案——API 也沒有這個能力。
*
* 用法:
* node scripts/project-add.js --repo owner/name --index 12 --project <名稱或網址>
* [--host <網址>] [--dry-run]
*/
import {
ScriptError,
expectOk,
giteaRequest,
main,
parseFlags,
parseIndex,
parseRepo,
preflight,
resolveLogin,
} from './lib.js';
main(async () => {
const flags = parseFlags(process.argv.slice(2), {
required: ['repo', 'index', 'project'],
optional: ['host'],
booleans: ['dry-run'],
});
const repo = parseRepo(flags.repo);
const index = parseIndex(flags.index);
const path = `/repos/${repo}/issues/${index}`;
const login = resolveLogin({ host: flags.host });
if (!flags['dry-run']) await preflight(login, repo);
const project = await resolveProject(login, repo, flags.project);
const issue = expectOk(await giteaRequest(login, 'GET', path), `GET ${path}`);
const current = (issue.projects ?? []).map((item) => item.id);
if (current.includes(project.id)) {
return { repo, index, project, alreadyThere: true };
}
// projects 是整份取代,不是附加:漏掉既有的等於把這顆議題踢出原本的看板
const payload = { projects: [...current, project.id] };
if (flags['dry-run']) {
return {
dryRun: true,
repo,
index,
project,
requests: [{ method: 'PATCH', path, body: payload }],
};
}
expectOk(await giteaRequest(login, 'PATCH', path, { body: payload }), `PATCH ${path}`);
return { repo, index, project, alreadyThere: false };
});
/**
* 把 `--project` 解析成 {id, title}。
* 看起來像網址就取結尾的 id;否則掃議題反查名稱。
*/
async function resolveProject(login, repo, value) {
if (/^https?:\/\//.test(value)) {
const id = value.replace(/\/+$/, '').match(/(\d+)$/)?.[1];
if (!id) {
throw new ScriptError('PROJECT_NOT_FOUND', `專案網址的結尾必須是專案 id:${value}`);
}
return { id: Number(id), title: null };
}
const known = await scanProjects(login, repo);
const hit = known.find((project) => project.title === value);
if (hit) return hit;
const seen = known.map((project) => project.title).join('、');
throw new ScriptError(
'PROJECT_NOT_FOUND',
seen
? `反查不到名為「${value}」的看板。掃到的有:${seen}。若它不在其中,請直接貼專案網址(結尾即 id)`
: `最近的議題沒有任何一顆掛在看板上,無法反查看板 id。請直接貼專案網址(結尾即 id)`,
);
}
/**
* 掃最近 50 筆議題,湊出這個 repo 用過的看板。
* Gitea 沒給列出專案的 endpoint,這是唯一拿得到 id→名稱對照的辦法。
*/
async function scanProjects(login, repo) {
const path = `/repos/${repo}/issues`;
const issues = expectOk(
await giteaRequest(login, 'GET', path, { query: { state: 'all', limit: 50 } }),
`GET ${path}`,
) ?? [];
const byId = new Map();
for (const issue of issues) {
for (const project of issue.projects ?? []) {
byId.set(project.id, { id: project.id, title: project.title });
}
}
return [...byId.values()];
}