#!/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()]; }