Files
tea-sdlc/scripts/issue-update.js
T
jiantw83andClaude Opus 5 0e05dd2066 feat(issue-update): 支援 --overview-url,把總覽網址寫回議題
沿用既有的 upsertLineInSection:連結以固定前綴獨佔總覽段落裡的一行,重跑時就地
更新,不會長出第二個連結;議題原本的 markdown 白話總覽一字不動——網頁是補充,
不是取代。

連結旁自動附上「此連結預設為私有,組織外無法開啟」。讀到的人多半會想轉寄給組織外
的人,這句話寫在議題上比寫在文件裡有用。

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-17 06:22:28 +00:00

135 lines
4.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
/**
* 更新議題的時程相關欄位:Milestone、截止日、人天估算。
*
* 三件事經同一個 PATCH 送出,因為排時程時它們幾乎總是一起改。
*
* 人天估算只寫進 body 的人類可讀一行。Gitea 1.27 的 API 沒有任何請求定義接受
* `time_estimate`——它只出現在 Issue 的回應裡——所以議題的估算欄位無法由 API 寫入。
*
* Milestone 只認既有的:指到不存在的就中止並列出可選項目,本工具不建立 Milestone。
*
* 也負責把圖解版總覽的網址寫回議題:連結以固定前綴獨佔一行,重跑時就地更新,
* 議題原本的 markdown 白話總覽一字不動——網頁是補充,不是取代。
*
* 用法:
* node scripts/issue-update.js --repo owner/name --index 12
* [--milestone <名稱>] [--due-date YYYY-MM-DD] [--estimate-days N]
* [--overview-url <網址>] [--host <網址>] [--dry-run]
*/
import {
ScriptError,
expectOk,
giteaRequest,
main,
parseFlags,
parseIndex,
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', 'overview-url', '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']);
const overviewUrl = parseOverviewUrl(flags['overview-url']);
if (flags.milestone === undefined && dueDate === null && days === null && overviewUrl === null) {
throw new ScriptError(
'NOTHING_TO_UPDATE',
'至少要指定 --milestone、--due-date、--estimate-days 或 --overview-url 其中一個',
);
}
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 || overviewUrl !== null) {
const issue = expectOk(await giteaRequest(login, 'GET', path), `GET ${path}`);
let body = issue.body ?? '';
if (days !== null) body = upsertLineInSection(body, '關聯', `估算人天:${days}`);
if (overviewUrl !== null) {
body = upsertLineInSection(body, '總覽', `圖解版總覽:${overviewUrl}${PRIVACY_NOTE}`);
}
// 沒變就不塞進 PATCH:無謂改寫 body 會在議題上留下一筆沒有內容的編輯紀錄
if (body !== issue.body) payload.body = body;
}
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 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;
}
/** artifact 預設私有,組織外開不起來——這件事要跟著連結一起留在議題上 */
const PRIVACY_NOTE = '(此連結預設為私有,組織外無法開啟)';
function parseOverviewUrl(value) {
if (value === undefined) return null;
if (!/^https?:\/\/\S+$/.test(value)) {
throw new ScriptError('BAD_OVERVIEW_URL', `--overview-url 需為 http(s) 網址,收到的是 ${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;
}