Files
tea-sdlc/scripts/issue-update.js
T
jiantw83andClaude Opus 5 c8f7ab42f2 refactor(參數): parseIndex 抽到 lib,四支腳本共用
同一段 /^[1-9]\d*$/ 與 BAD_INDEX 已經抄了四份。lib 本來就放著同性質的 parseRepo,
這一支該待在它旁邊。

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

115 lines
3.7 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。
*
* 用法:
* 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,
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', '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 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;
}