Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 18 additions & 2 deletions .github/scripts/release-guard.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,19 @@ function buildArchives(temp, dist) {
}
return digests;
}
// 仅输出诊断白名单;禁止输出令牌、完整响应头或原始响应正文。
function formatApiError(method, endpoint, response, body, secrets = []) {
const clean = value => {
let text = String(value ?? '');
for (const secret of secrets.filter(Boolean)) text = text.split(secret).join('[REDACTED]');
return text.replace(/Bearer\s+[^\s,;]+/gi, 'Bearer [REDACTED]').replace(/[\r\n\x00-\x1f\x7f]/g, ' ');
};
const route = endpoint.startsWith('https://') ? new URL(endpoint).pathname : endpoint.split('?')[0];
const details = ['x-ratelimit-remaining', 'x-ratelimit-reset', 'x-ratelimit-resource', 'retry-after']
.map(name => `${name}=${clean(response.headers.get(name) ?? 'unknown').slice(0, 100)}`).join(' ');
const message = typeof body?.message === 'string' ? clean(body.message).slice(0, 300) : 'unavailable';
return `GitHub API ${clean(method)} ${clean(route)}: ${response.status}; ${details}; message=${message}`;
}
async function main() {
insist(process.env.GITHUB_EVENT_NAME === 'workflow_run', '仅允许 workflow_run');
const repo = process.env.GITHUB_REPOSITORY;
Expand All @@ -74,7 +87,10 @@ async function main() {
insist(url.startsWith(`${root}/`) || url.startsWith(`https://uploads.github.com/repos/${repo}/`), '非预期 API 目标');
const response = await fetch(url, { signal: AbortSignal.timeout(30000), method, headers: { ...(endpoint.startsWith('/actions/') && method === 'GET' ? {} : { Authorization: `Bearer ${token}` }), Accept: 'application/vnd.github+json', 'X-GitHub-Api-Version': '2022-11-28', ...(body ? { 'Content-Type': binary ? 'application/zip' : 'application/json' } : {}) }, body: body ? (binary ? body : JSON.stringify(body)) : undefined });
if (optional && response.status === 404) return null;
insist(response.ok, `GitHub API ${method} ${endpoint}: ${response.status}`);
if (!response.ok) {
const errorBody = await response.json().catch(() => null);
throw new Error(formatApiError(method, endpoint, response, errorBody, [token]));
}
return response.json();
}
const verifyDownloads = async release => {
Expand Down Expand Up @@ -138,5 +154,5 @@ async function main() {
console.log(`发布成功:${release.html_url};SHA=${sha};两份 ZIP SHA-256 已验证`);
} finally { fs.rmSync(temp, { recursive: true, force: true }); }
}
module.exports = { guardRun, guardVersions, guardExisting, buildArchives, VERSION, TAG, ASSETS };
module.exports = { guardRun, guardVersions, guardExisting, buildArchives, formatApiError, VERSION, TAG, ASSETS };
if (require.main === module) main().catch(error => { console.error(error.message); process.exitCode = 1; });
18 changes: 17 additions & 1 deletion .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -92,9 +92,25 @@ jobs:
const repo = process.env.GITHUB_REPOSITORY;
const assert = (ok, message) => { if (!ok) throw new Error(message); };
assert(repo === 'Thregren/InfinityTime' && event.repository.full_name === repo && event.repository.fork === false, '目标仓库不匹配');
// 仅输出诊断白名单;禁止输出令牌、完整响应头或原始响应正文。
function formatApiError(method, endpoint, response, body, secrets = []) {
const clean = value => {
let text = String(value ?? '');
for (const secret of secrets.filter(Boolean)) text = text.split(secret).join('[REDACTED]');
return text.replace(/Bearer\s+[^\s,;]+/gi, 'Bearer [REDACTED]').replace(/[\r\n\x00-\x1f\x7f]/g, ' ');
};
const route = endpoint.startsWith('https://') ? new URL(endpoint).pathname : endpoint.split('?')[0];
const details = ['x-ratelimit-remaining', 'x-ratelimit-reset', 'x-ratelimit-resource', 'retry-after']
.map(name => `${name}=${clean(response.headers.get(name) ?? 'unknown').slice(0, 100)}`).join(' ');
const message = typeof body?.message === 'string' ? clean(body.message).slice(0, 300) : 'unavailable';
return `GitHub API ${clean(method)} ${clean(route)}: ${response.status}; ${details}; message=${message}`;
}
const get = async path => {
const response = await fetch(`https://api.github.com/repos/${repo}${path}`, { signal: AbortSignal.timeout(30000), headers: { ...(path.startsWith('/actions/') ? {} : { Authorization: `Bearer ${process.env.GH_TOKEN}` }), Accept: 'application/vnd.github+json', 'X-GitHub-Api-Version': '2022-11-28' } });
assert(response.ok, `GitHub API ${response.status}`);
if (!response.ok) {
const errorBody = await response.json().catch(() => null);
throw new Error(formatApiError('GET', path, response, errorBody, [process.env.GH_TOKEN]));
}
return response.json();
};
const main = await get('/git/ref/heads/main');
Expand Down
20 changes: 18 additions & 2 deletions tests/gallery-loading-browser.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -166,8 +166,24 @@ const pixel = '<svg xmlns="http://www.w3.org/2000/svg" width="640" height="400">
assert.equal(await page.locator('#stale').count(), 0);
assert.equal(await page.locator('#fresh').count(), 1);
assert.equal(await page.locator('.pp-load-retry').isVisible(), false);
await page.evaluate(() => InfinityWaterfall.reset({ nextUrl: '/empty' }));
assert.equal(await page.evaluate(() => InfinityWaterfall.loadMore()), true, '合法的搜索末尾空批次能正常结束');
// 同一浏览器任务内重置并发起明确请求,避免原生滚动事件在两次 evaluate 间抢先加载。
assert.equal(await page.evaluate(() => {
InfinityWaterfall.reset({ nextUrl: '/empty' });
return InfinityWaterfall.loadMore();
}), true, '合法的搜索末尾空批次能正常结束');
assert.equal(await page.locator('.pp-load-retry').isVisible(), false);
// 自动滚动另行通过真实事件路径验证,不依赖上面的手动调用时序。
const emptyRequests = requests.filter(p => p === '/empty').length;
await page.evaluate(() => {
InfinityWaterfall.reset({ nextUrl: '/empty' });
window.scrollTo(0, document.documentElement.scrollHeight);
window.dispatchEvent(new Event('scroll'));
});
await page.waitForFunction(() => {
const more = document.querySelector('#load-more');
return more.getAttribute('data-next-url') === '' && more.getAttribute('aria-busy') === 'false';
});
assert.equal(requests.filter(p => p === '/empty').length, emptyRequests + 1, '自动滚动恰好加载一次合法末尾空批次');
assert.equal(await page.locator('.pp-load-retry').isVisible(), false);
// AVIF 请求和解码失败均必须尝试可用 WebP,不能仅显示永久失败。
await page.evaluate(html => {
Expand Down
24 changes: 24 additions & 0 deletions tests/release-guards.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -88,3 +88,27 @@ test('真实 ZIP 构建确定性、单根目录、排除 data,拒绝 symlink',
assert.throws(() => build('unsafe'), /非普通文件/);
} finally { process.chdir(old); fs.rmSync(root, { recursive: true, force: true }); }
});

test('API 错误诊断仅输出白名单和截断 message,隐藏令牌与敏感头', () => {
const { formatApiError } = require('../.github/scripts/release-guard.cjs');
const response = { status: 403, headers: new Headers({
'x-ratelimit-remaining': '0', 'x-ratelimit-reset': '1234567890',
'x-ratelimit-resource': 'core', 'retry-after': '60',
authorization: 'Bearer secret-header', 'set-cookie': 'private-cookie',
}) };
const result = formatApiError('GET', '/actions/runs/123?secret=query-secret', response,
{ message: `rate limited known-token Bearer unknown-token\n${'x'.repeat(500)}`, secret: 'raw-body-secret' }, ['known-token']);
assert.match(result, /GET \/actions\/runs\/123: 403/);
for (const detail of ['x-ratelimit-remaining=0', 'x-ratelimit-reset=1234567890', 'x-ratelimit-resource=core', 'retry-after=60']) assert(result.includes(detail));
for (const secret of ['known-token', 'unknown-token', 'secret-header', 'private-cookie', 'raw-body-secret', 'query-secret', '\n']) assert(!result.includes(secret));
assert(result.includes('[REDACTED]'));
assert(result.split('; message=')[1].length <= 300);
assert.match(formatApiError('POST', 'https://uploads.github.com/repos/a/b/releases/1/assets?name=test', response, null), /POST \/repos\/a\/b\/releases\/1\/assets: 403/);
});
test('checkout 前 inline API 错误格式与共享测试函数完全一致', () => {
const fs = require('node:fs');
const { formatApiError } = require('../.github/scripts/release-guard.cjs');
const workflow = fs.readFileSync('.github/workflows/release.yml', 'utf8');
const lines = workflow.split('\n').map(line => line.startsWith(' ') ? line.slice(12) : line).join('\n');
assert(lines.includes(formatApiError.toString()));
});
Loading