#!/usr/bin/env node /** * 更新議題的時程相關欄位:Milestone、截止日、人天估算。 * * 三件事經同一個 PATCH 送出,因為排時程時它們幾乎總是一起改。 * * 人天估算只寫進 body 的人類可讀一行。Gitea 1.27 的 API 沒有任何請求定義接受 * `time_estimate`——它只出現在 Issue 的回應裡——所以議題的估算欄位無法由 API 寫入。 * * Milestone 只認既有的:指到不存在的就中止並列出可選項目,本工具不建立 Milestone。 * * 用法: * node scripts/issue-update.js --repo owner/name --index 12 * [--milestone <名稱>] [--due-date YYYY-MM-DD] [--estimate-days N] * [--host <網址>] [--dry-run] */ import { ScriptError, expectOk, giteaRequest, main, parseFlags, parseRepo, preflight, resolveLogin, } from './lib.js'; import { upsertLineInSection } from './issue-body.js'; main(async () => { const flags = parseFlags(process.argv.slice(2), { required: ['repo', 'index'], optional: ['milestone', 'due-date', 'estimate-days', 'host'], booleans: ['dry-run'], }); const repo = parseRepo(flags.repo); const index = parseIndex(flags.index); const dueDate = parseDueDate(flags['due-date']); const days = parseDays(flags['estimate-days']); if (flags.milestone === undefined && dueDate === null && days === null) { throw new ScriptError( 'NOTHING_TO_UPDATE', '至少要指定 --milestone、--due-date 或 --estimate-days 其中一個', ); } const login = resolveLogin({ host: flags.host }); if (!flags['dry-run']) await preflight(login, repo); const path = `/repos/${repo}/issues/${index}`; const payload = {}; if (flags.milestone !== undefined) { payload.milestone = await resolveMilestoneId(login, repo, flags.milestone); } if (dueDate !== null) { payload.due_date = `${dueDate}T00:00:00Z`; } if (days !== null) { const issue = expectOk(await giteaRequest(login, 'GET', path), `GET ${path}`); const updated = upsertLineInSection(issue.body ?? '', '關聯', `估算人天:${days}`); // 沒變就不塞進 PATCH:無謂改寫 body 會在議題上留下一筆沒有內容的編輯紀錄 if (updated !== issue.body) payload.body = updated; } if (flags['dry-run']) { return { dryRun: true, repo, index, requests: [{ method: 'PATCH', path, body: payload }] }; } const issue = expectOk(await giteaRequest(login, 'PATCH', path, { body: payload }), `PATCH ${path}`); return { repo, index, updated: Object.keys(payload), url: issue.html_url, }; }); function parseIndex(value) { if (!/^[1-9]\d*$/.test(value)) { throw new ScriptError('BAD_INDEX', `--index 需為正整數,收到的是 ${value}`); } return Number(value); } function parseDueDate(value) { if (value === undefined) return null; if (!/^\d{4}-\d{2}-\d{2}$/.test(value)) { throw new ScriptError('BAD_DUE_DATE', `--due-date 需為 YYYY-MM-DD,收到的是 ${value}`); } return value; } function parseDays(value) { if (value === undefined) return null; const days = Number(value); if (!Number.isFinite(days) || days <= 0) { throw new ScriptError('BAD_ESTIMATE', `--estimate-days 需為正數,收到的是 ${value}`); } return days; } async function resolveMilestoneId(login, repo, title) { const path = `/repos/${repo}/milestones`; const milestones = expectOk( await giteaRequest(login, 'GET', path, { query: { state: 'all' } }), `GET ${path}`, ) ?? []; const hit = milestones.find((milestone) => milestone.title === title); if (!hit) { throw new ScriptError( 'UNKNOWN_MILESTONE', `${repo} 沒有名為「${title}」的 Milestone。本工具不建立 Milestone,` + `請改挑既有的:${milestones.map((m) => m.title).join('、') || '(這個 repo 目前沒有 Milestone)'}`, ); } return hit.id; }