模板正文目录门禁:CLI 打包与后台上传统一拒绝身份/版本库/依赖/构建目录
Project CI / Frontend tests (push) Has been cancelled
Project CI / Backend tests (push) Has been cancelled
Project CI / Native shell tests (push) Has been cancelled
Project CI / AI game creator shell Rust lane 2/2 (push) Has been cancelled
Project CI / AI game creator shell Rust crates (push) Has been cancelled
Project CI / Repository checks (push) Has been cancelled
Project CI / AI game creator shell web tests (push) Has been cancelled
Project CI / AI game creator shell Rust lane 1/2 (push) Has been cancelled
Project CI / AI game creator shell Rust smoke (push) Has been cancelled
Project CI / Frontend tests (push) Has been cancelled
Project CI / Backend tests (push) Has been cancelled
Project CI / Native shell tests (push) Has been cancelled
Project CI / AI game creator shell Rust lane 2/2 (push) Has been cancelled
Project CI / AI game creator shell Rust crates (push) Has been cancelled
Project CI / Repository checks (push) Has been cancelled
Project CI / AI game creator shell web tests (push) Has been cancelled
Project CI / AI game creator shell Rust lane 1/2 (push) Has been cancelled
Project CI / AI game creator shell Rust smoke (push) Has been cancelled
- scripts/agc-template-library-publish.mjs:readProjectFiles 增加正文门禁,任意层级拒绝 .agent/.git/.svn/node_modules,根目录拒绝 dist/build/library/temp/local/.idea/.vscode - scripts/agc-template-library-publish.test.mjs:新增夹具源目录与门禁用例,覆盖 13 条拒绝路径及 game/dist/**、game/.gitignore 放行 - server-rs/crates/api-server/src/admin_templates.rs:上传归档校验与 CLI 用同一份段名单,新增 template_import_archive_rejects_identity_and_build_directories - docs/【模板规范】AGC模板包组织指南-2026-09-21.md:已知边界改为「已有机器门禁」,并说明同名目录段只在根目录受限 - docs/technical/【技术方案】AGC模板库与模板建项-2026-09-17.md:CLI 与后台上传的校验口径补门禁,并追加 2026-09-21 证据(九个模板本地重打包摘要与线上清单逐条一致) - docs/project-memory/plans/【里程碑】后台模板上传-2026-09-21.md:验收标准 2 补充门禁证据 - docs/project-memory/shared-memory/decision-log.md:新增 2026-09-21 正文目录门禁决策记录
This commit is contained in:
@@ -200,12 +200,39 @@ function buildZip(entries) {
|
||||
}
|
||||
|
||||
function readProjectFiles(projectRoot) {
|
||||
/** 正文任何层级都不允许出现的目录段:项目身份与版本库 / 依赖元数据。 */
|
||||
const forbiddenSegments = new Set(['.agent', '.git', '.svn', 'node_modules']);
|
||||
/** 只允许出现在正文根目录之外的构建产物与编辑器工作区目录。 */
|
||||
const forbiddenRootSegments = new Set([
|
||||
'dist',
|
||||
'build',
|
||||
'library',
|
||||
'temp',
|
||||
'local',
|
||||
'.idea',
|
||||
'.vscode',
|
||||
]);
|
||||
// 与后台「上传模板」共用同一条门禁,依据 docs/【模板规范】AGC模板包组织指南-2026-09-21.md。
|
||||
const violation = (relative) => {
|
||||
const segments = relative.split('/');
|
||||
for (const segment of segments) {
|
||||
if (forbiddenSegments.has(segment)) {
|
||||
return `模板正文不允许包含 ${segment} 目录:${relative}`;
|
||||
}
|
||||
}
|
||||
if (forbiddenRootSegments.has(segments[0])) {
|
||||
return `模板正文根目录不允许包含 ${segments[0]} 目录:${relative}`;
|
||||
}
|
||||
return null;
|
||||
};
|
||||
const files = [];
|
||||
const walk = (directory, prefix) => {
|
||||
for (const entry of readdirSync(directory, { withFileTypes: true }).sort(
|
||||
(left, right) => left.name.localeCompare(right.name),
|
||||
)) {
|
||||
const relative = prefix ? `${prefix}/${entry.name}` : entry.name;
|
||||
const rejected = violation(relative);
|
||||
if (rejected) throw new Error(rejected);
|
||||
const full = join(directory, entry.name);
|
||||
if (entry.isDirectory()) walk(full, relative);
|
||||
else if (entry.isFile())
|
||||
|
||||
@@ -1,9 +1,15 @@
|
||||
import assert from 'node:assert/strict';
|
||||
import { spawnSync } from 'node:child_process';
|
||||
import { createHash, createHmac } from 'node:crypto';
|
||||
import { mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
|
||||
import {
|
||||
mkdirSync,
|
||||
mkdtempSync,
|
||||
readFileSync,
|
||||
rmSync,
|
||||
writeFileSync,
|
||||
} from 'node:fs';
|
||||
import { tmpdir } from 'node:os';
|
||||
import { basename, join, resolve } from 'node:path';
|
||||
import { basename, dirname, join, resolve } from 'node:path';
|
||||
import test from 'node:test';
|
||||
import { fileURLToPath, pathToFileURL } from 'node:url';
|
||||
|
||||
@@ -87,6 +93,86 @@ test('官方 Cocos 模板生成完整且可重复的原生项目包', async () =
|
||||
}
|
||||
});
|
||||
|
||||
function fixtureSource(projectEntries = []) {
|
||||
const directory = mkdtempSync(join(tmpdir(), 'agc-template-publish-'));
|
||||
const templateRoot = join(directory, 'v1', 'fixture-template');
|
||||
const write = (relative, bytes) => {
|
||||
const full = join(templateRoot, relative);
|
||||
mkdirSync(dirname(full), { recursive: true });
|
||||
writeFileSync(full, bytes);
|
||||
};
|
||||
write(
|
||||
'meta.json',
|
||||
`${JSON.stringify(
|
||||
{
|
||||
id: 'fixture-template',
|
||||
title: '夹具模板',
|
||||
summary: '',
|
||||
tags: ['fixture'],
|
||||
runtime: 'html',
|
||||
entry: 'game/index.html',
|
||||
templateVersion: '0.1.0',
|
||||
},
|
||||
null,
|
||||
2,
|
||||
)}\n`,
|
||||
);
|
||||
write('cover.svg', '<svg xmlns="http://www.w3.org/2000/svg"/>\n');
|
||||
write('game/index.html', '<html></html>\n');
|
||||
for (const relative of projectEntries) write(`project/${relative}`, 'x\n');
|
||||
return directory;
|
||||
}
|
||||
|
||||
test('模板正文含身份/版本库/依赖/构建目录时拒绝打包', async () => {
|
||||
const rejected = [
|
||||
['.agent/manifest.json', '不允许包含 .agent 目录'],
|
||||
['game/.agent/ledger.json', '不允许包含 .agent 目录'],
|
||||
['.git/config', '不允许包含 .git 目录'],
|
||||
['.svn/entries', '不允许包含 .svn 目录'],
|
||||
['node_modules/three/package.json', '不允许包含 node_modules 目录'],
|
||||
['assets/node_modules/keep.txt', '不允许包含 node_modules 目录'],
|
||||
['dist/game.js', '根目录不允许包含 dist 目录'],
|
||||
['build/index.html', '根目录不允许包含 build 目录'],
|
||||
['library/import.json', '根目录不允许包含 library 目录'],
|
||||
['temp/asset.json', '根目录不允许包含 temp 目录'],
|
||||
['local/settings.json', '根目录不允许包含 local 目录'],
|
||||
['.vscode/settings.json', '根目录不允许包含 .vscode 目录'],
|
||||
['.idea/misc.xml', '根目录不允许包含 .idea 目录'],
|
||||
];
|
||||
for (const [relative, expected] of rejected) {
|
||||
const directory = fixtureSource([relative]);
|
||||
try {
|
||||
assert.throws(
|
||||
() => buildLibrary(directory, 'templates'),
|
||||
(error) => String(error.message).includes(expected),
|
||||
`${relative} 必须被拒绝`,
|
||||
);
|
||||
} finally {
|
||||
rmSync(directory, { recursive: true, force: true });
|
||||
}
|
||||
}
|
||||
|
||||
// 同名目录段只在根目录被拒绝:正文内 game/dist/** 属于模板自身内容,.gitignore 也不是 .git。
|
||||
const directory = fixtureSource(['game/dist/app.js', 'game/.gitignore']);
|
||||
try {
|
||||
const built = buildLibrary(directory, 'templates');
|
||||
assert.deepEqual(
|
||||
built.indexJson.templates.map((entry) => entry.id),
|
||||
['fixture-template'],
|
||||
);
|
||||
const zip = await JSZip.loadAsync(
|
||||
built.objects.find(
|
||||
(object) => object.key === built.indexJson.templates[0].zipKey,
|
||||
).body,
|
||||
{ checkCRC32: true },
|
||||
);
|
||||
assert.ok(Object.keys(zip.files).includes('game/dist/app.js'));
|
||||
assert.ok(Object.keys(zip.files).includes('game/.gitignore'));
|
||||
} finally {
|
||||
rmSync(directory, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
const digest = (bytes) => createHash('sha256').update(bytes).digest('hex');
|
||||
const indexKey = 'templates/index.json';
|
||||
const lockKey = 'templates/.publish-lock.json';
|
||||
|
||||
Reference in New Issue
Block a user