97 lines
3.7 KiB
JavaScript
Executable File
97 lines
3.7 KiB
JavaScript
Executable File
#!/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(';')}`);
|
||
}
|