Files
tea-sdlc/scripts/issue-assets.js
T

97 lines
3.7 KiB
JavaScript
Executable File
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
/**
* Upload overview screenshots to a Gitea issue and update its attachment index.
* The manifest is produced by the preview step and contains local PNG paths and hashes.
*/
import { existsSync, readFileSync } from 'node:fs';
import { resolve } from 'node:path';
import {
ScriptError,
expectOk,
fetchIssue,
giteaRequest,
giteaUpload,
main,
parseFlags,
parseIndex,
parseRepo,
preflight,
resolveLogin,
} from './lib.js';
import { upsertLineInSection } from './issue-body.js';
const INDEX_LINE = '圖解版總覽附件:';
main(async () => {
const flags = parseFlags(process.argv.slice(2), {
required: ['repo', 'index', 'manifest'],
optional: ['host'],
booleans: ['dry-run'],
});
const repo = parseRepo(flags.repo);
const index = parseIndex(flags.index);
const manifest = JSON.parse(readFileSync(resolve(flags.manifest), 'utf8'));
const screenshots = manifest.screenshots;
if (!Array.isArray(screenshots) || screenshots.length === 0) {
throw new ScriptError('MANIFEST_INVALID', 'manifest 必須包含至少一張 screenshots');
}
for (const item of screenshots) {
if (!item.role || !item.path || !item.fileName || !item.sha256 || !existsSync(resolve(item.path))) {
throw new ScriptError('MANIFEST_INVALID', '每張截圖都必須有 role、path、fileName、sha256 且檔案存在');
}
}
const login = resolveLogin({ host: flags.host });
const issuePath = `/repos/${repo}/issues/${index}`;
const assetsPath = `${issuePath}/assets`;
if (flags['dry-run']) {
return {
dryRun: true,
repo,
index,
requests: [
{ method: 'GET', path: issuePath },
{ method: 'GET', path: assetsPath },
...screenshots.map((item) => ({ method: 'POST', path: assetsPath, file: item.path })),
],
};
}
await preflight(login, repo);
const issue = await fetchIssue(login, repo, index);
const assets = expectOk(await giteaRequest(login, 'GET', assetsPath), `GET ${assetsPath}`);
const existing = Array.isArray(assets) ? assets : [];
const indexed = parseIndexedAssets(issue.body ?? '');
const uploaded = [];
for (const item of screenshots) {
const indexedAsset = indexed.find((asset) => asset.fileName === item.fileName && asset.sha256 === item.sha256);
const existingAsset = existing.find((asset) => asset.name === item.fileName && asset.sha256 === item.sha256);
const asset = indexedAsset ?? existingAsset;
if (asset) {
uploaded.push({ ...item, url: asset.url ?? asset.browser_download_url ?? asset.download_url, reused: true });
continue;
}
const response = expectOk(
await giteaUpload(login, `${assetsPath}?name=${encodeURIComponent(item.fileName)}`, resolve(item.path), item.fileName),
`POST ${assetsPath}`,
);
uploaded.push({ ...item, url: response.browser_download_url ?? response.download_url, reused: false });
}
const body = updateIndex(issue.body ?? '', uploaded);
const update = body === issue.body ? null : expectOk(await giteaRequest(login, 'PATCH', issuePath, { body: { body } }), `PATCH ${issuePath}`);
return { repo, index, uploaded, updated: update !== null, url: update?.html_url ?? issue.html_url };
});
function parseIndexedAssets(body) {
const line = body.split('\n').find((row) => row.startsWith(INDEX_LINE)) ?? '';
return [...line.matchAll(/([^=;\s]+)=([^;\s]+)\s+sha256=([a-f0-9]{64})\s+(\S+)/g)].map((match) => ({
role: match[1],
fileName: match[2],
sha256: match[3],
url: match[4],
}));
}
function updateIndex(body, assets) {
const lines = assets.map((asset) => `${asset.role}=${asset.fileName} sha256=${asset.sha256} ${asset.url ?? '(網址未回傳)'}`);
return upsertLineInSection(body, '總覽', `${INDEX_LINE}${lines.join(';')}`);
}