feat(issue-update): 支援 --overview-url,把總覽網址寫回議題

沿用既有的 upsertLineInSection:連結以固定前綴獨佔總覽段落裡的一行,重跑時就地
更新,不會長出第二個連結;議題原本的 markdown 白話總覽一字不動——網頁是補充,
不是取代。

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

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-09-17 06:22:28 +00:00
co-authored by Claude Opus 5
parent ed2d08de9f
commit 0e05dd2066
+27 -7
View File
@@ -9,10 +9,13 @@
* *
* Milestone 只認既有的:指到不存在的就中止並列出可選項目,本工具不建立 Milestone。 * Milestone 只認既有的:指到不存在的就中止並列出可選項目,本工具不建立 Milestone。
* *
* 也負責把圖解版總覽的網址寫回議題:連結以固定前綴獨佔一行,重跑時就地更新,
* 議題原本的 markdown 白話總覽一字不動——網頁是補充,不是取代。
*
* 用法: * 用法:
* node scripts/issue-update.js --repo owner/name --index 12 * node scripts/issue-update.js --repo owner/name --index 12
* [--milestone <名稱>] [--due-date YYYY-MM-DD] [--estimate-days N] * [--milestone <名稱>] [--due-date YYYY-MM-DD] [--estimate-days N]
* [--host <網址>] [--dry-run] * [--overview-url <網址>] [--host <網址>] [--dry-run]
*/ */
import { import {
ScriptError, ScriptError,
@@ -30,18 +33,19 @@ import { upsertLineInSection } from './issue-body.js';
main(async () => { main(async () => {
const flags = parseFlags(process.argv.slice(2), { const flags = parseFlags(process.argv.slice(2), {
required: ['repo', 'index'], required: ['repo', 'index'],
optional: ['milestone', 'due-date', 'estimate-days', 'host'], optional: ['milestone', 'due-date', 'estimate-days', 'overview-url', 'host'],
booleans: ['dry-run'], booleans: ['dry-run'],
}); });
const repo = parseRepo(flags.repo); const repo = parseRepo(flags.repo);
const index = parseIndex(flags.index); const index = parseIndex(flags.index);
const dueDate = parseDueDate(flags['due-date']); const dueDate = parseDueDate(flags['due-date']);
const days = parseDays(flags['estimate-days']); const days = parseDays(flags['estimate-days']);
const overviewUrl = parseOverviewUrl(flags['overview-url']);
if (flags.milestone === undefined && dueDate === null && days === null) { if (flags.milestone === undefined && dueDate === null && days === null && overviewUrl === null) {
throw new ScriptError( throw new ScriptError(
'NOTHING_TO_UPDATE', 'NOTHING_TO_UPDATE',
'至少要指定 --milestone、--due-date 或 --estimate-days 其中一個', '至少要指定 --milestone、--due-date、--estimate-days 或 --overview-url 其中一個',
); );
} }
@@ -57,11 +61,16 @@ main(async () => {
if (dueDate !== null) { if (dueDate !== null) {
payload.due_date = `${dueDate}T00:00:00Z`; payload.due_date = `${dueDate}T00:00:00Z`;
} }
if (days !== null) { if (days !== null || overviewUrl !== null) {
const issue = expectOk(await giteaRequest(login, 'GET', path), `GET ${path}`); const issue = expectOk(await giteaRequest(login, 'GET', path), `GET ${path}`);
const updated = upsertLineInSection(issue.body ?? '', '關聯', `估算人天:${days}`); let body = issue.body ?? '';
if (days !== null) body = upsertLineInSection(body, '關聯', `估算人天:${days}`);
if (overviewUrl !== null) {
body = upsertLineInSection(body, '總覽', `圖解版總覽:${overviewUrl}${PRIVACY_NOTE}`);
}
// 沒變就不塞進 PATCH:無謂改寫 body 會在議題上留下一筆沒有內容的編輯紀錄 // 沒變就不塞進 PATCH:無謂改寫 body 會在議題上留下一筆沒有內容的編輯紀錄
if (updated !== issue.body) payload.body = updated; if (body !== issue.body) payload.body = body;
} }
if (flags['dry-run']) { if (flags['dry-run']) {
@@ -86,6 +95,17 @@ function parseDueDate(value) {
return 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) { function parseDays(value) {
if (value === undefined) return null; if (value === undefined) return null;
const days = Number(value); const days = Number(value);