Files
tea-sdlc/scripts/project-add.js
T
jiantw83andClaude Opus 5 28182f1709 feat(project-add): 把議題放進看板,看板 id 靠掃議題反查
Gitea 1.27 沒有「列出專案」的 endpoint,名稱只能反查:掃最近 50 筆議題,從它們
身上的 projects 欄位湊出 id→名稱對照。全都沒掛看板時湊不出來,這時請使用者直接
貼專案網址,結尾即 id。

projects 欄位是整份取代不是附加,所以要先讀出議題既有的看板再聯集——漏掉就等於
把這顆議題踢出原本的看板。

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

116 lines
3.6 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,
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 };
});
function parseIndex(value) {
if (!/^[1-9]\d*$/.test(value)) {
throw new ScriptError('BAD_INDEX', `--index 需為正整數,收到的是 ${value}`);
}
return Number(value);
}
/**
* 把 `--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()];
}