mirror of
https://github.com/whyour/qinglong.git
synced 2026-11-02 06:58:52 +08:00
fix: simplify npm packaging and align frontend Node builds
This commit is contained in:
@@ -83,7 +83,7 @@ jobs:
|
|||||||
|
|
||||||
- uses: actions/setup-node@v6
|
- uses: actions/setup-node@v6
|
||||||
with:
|
with:
|
||||||
node-version: "20"
|
node-version-file: ".nvmrc"
|
||||||
cache: "pnpm"
|
cache: "pnpm"
|
||||||
cache-dependency-path: pnpm-lock.yaml
|
cache-dependency-path: pnpm-lock.yaml
|
||||||
|
|
||||||
|
|||||||
-28
@@ -1,28 +0,0 @@
|
|||||||
/.tmp/
|
|
||||||
/.github/
|
|
||||||
/.vscode/
|
|
||||||
/.history/
|
|
||||||
/back/**/*.ts
|
|
||||||
/back/**/*.json
|
|
||||||
/data/
|
|
||||||
/src/
|
|
||||||
/.editorconfig
|
|
||||||
/.gitignore
|
|
||||||
/.prettierignore
|
|
||||||
/.prettierrc
|
|
||||||
/.umirc.ts
|
|
||||||
/nodemon.json
|
|
||||||
/tsconfig.back.json
|
|
||||||
/tsconfig.json
|
|
||||||
/typings.d.ts
|
|
||||||
/.env
|
|
||||||
/sample/tool.ts
|
|
||||||
/shell/pub.sh
|
|
||||||
/shell/preload/env.*
|
|
||||||
/shell/preload/notify.*
|
|
||||||
/shell/preload/__ql_notify__.*
|
|
||||||
/shell/preload/lang_env.sh
|
|
||||||
/shell/preload/*-notify.json
|
|
||||||
**/__pycache__/
|
|
||||||
**/*.pyc
|
|
||||||
**/.DS_Store
|
|
||||||
@@ -70,8 +70,12 @@ ARMv6 glibc 小设备(例如 Raspberry Pi 1/Zero 的 Raspbian/Raspberry Pi OS
|
|||||||
|
|
||||||
npm 会排除根目录的 `pnpm-lock.yaml`。主包的原生 `npm pack/publish` 在 `prepack` 中核对当前 Git 提交、干净工作区、锁文件及完整构建文件的哈希,通过后才将锁文件复制为 `shell/upgrade-lock.yaml`,安装时由仅使用 Node 内置模块的 `postinstall` 校验 SHA256 后恢复标准路径;源代码安装和 Docker 的最小依赖构建阶段继续使用原有锁文件。归档使用精确运行文件白名单,避免打入本地生成的预加载配置、缓存、测试和验收输出。静态构建中的 source map 与 gzip 文件也必须完整保留,否则清单复验会失败。
|
npm 会排除根目录的 `pnpm-lock.yaml`。主包的原生 `npm pack/publish` 在 `prepack` 中核对当前 Git 提交、干净工作区、锁文件及完整构建文件的哈希,通过后才将锁文件复制为 `shell/upgrade-lock.yaml`,安装时由仅使用 Node 内置模块的 `postinstall` 校验 SHA256 后恢复标准路径;源代码安装和 Docker 的最小依赖构建阶段继续使用原有锁文件。归档使用精确运行文件白名单,避免打入本地生成的预加载配置、缓存、测试和验收输出。静态构建中的 source map 与 gzip 文件也必须完整保留,否则清单复验会失败。
|
||||||
|
|
||||||
|
`package.json#files` 是 npm 包内容的唯一项目规则,不再维护根 `.npmignore`:npm 在根包使用 `files` 时会停用该目录的 `.npmignore` 和 `.gitignore` 规则。打包测试在没有 `.npmignore` 的独立源码目录中注入私有配置、密钥占位文件、缓存及生成的预加载配置,执行真实 `npm pack` 和安装,检查运行文件完整、私有文件及其内容不进入归档。此规则只用于 npm 包,完整平台 tar.gz 仍由平台包构建器处理。
|
||||||
|
|
||||||
干净源码完成 `pnpm build:front`、`pnpm build:back`、`pnpm build:info` 后,运行 `node scripts/verify-panel-package.cjs pack /tmp/qinglong-npm` 生成并检查真实 npm 归档。CI 在一次性 Linux 容器中,以 Node 20.18.1 和 Node 24 安装同一个归档,使用真实生产依赖和已安装的 `qinglong`、`ql` 入口,验证启动、损坏下载保留旧服务、在线准备与重启、离线准备与 Bash 重启及账号、配置、环境变量、任务和脚本保留。测试还安装完整性摘要固定的已发布 `@whyour/qinglong@2.23.0`,在旧服务中建立真实数据,再用新 `.tgz` 执行 npm 覆盖安装,验证账号、配置、环境变量、任务和脚本保留。之后升级到同次 Debian 镜像不可变 digest 通过正式 `Dockerfile.upgrade-bundle` 导出的原始平台包,确认运行目录变为 pnpm 布局。该单平台验收包仅上传短期 CI artifact,不发布 Release,也不推进开发或正式更新索引。
|
干净源码完成 `pnpm build:front`、`pnpm build:back`、`pnpm build:info` 后,运行 `node scripts/verify-panel-package.cjs pack /tmp/qinglong-npm` 生成并检查真实 npm 归档。CI 在一次性 Linux 容器中,以 Node 20.18.1 和 Node 24 安装同一个归档,使用真实生产依赖和已安装的 `qinglong`、`ql` 入口,验证启动、损坏下载保留旧服务、在线准备与重启、离线准备与 Bash 重启及账号、配置、环境变量、任务和脚本保留。测试还安装完整性摘要固定的已发布 `@whyour/qinglong@2.23.0`,在旧服务中建立真实数据,再用新 `.tgz` 执行 npm 覆盖安装,验证账号、配置、环境变量、任务和脚本保留。之后升级到同次 Debian 镜像不可变 digest 通过正式 `Dockerfile.upgrade-bundle` 导出的原始平台包,确认运行目录变为 pnpm 布局。该单平台验收包仅上传短期 CI artifact,不发布 Release,也不推进开发或正式更新索引。
|
||||||
|
|
||||||
|
Umi 4.4.4 的构建依赖仍使用 Node 24 已移除的 `http_parser`。前端源码构建使用 `.nvmrc` 中的 Node 22,CI 从该文件读取版本,Vercel 前后端项目均配置为 Node 22.x。`package.json` 不声明 `engines.node`,避免 Vercel 用开放的最低运行版本范围覆盖项目的构建版本;`pnpm build:front` 直接执行构建,不下载或切换 Node。面板运行时仍支持 Node 20.18.1 及以上,平台包的清单与安装器独立校验最低 Node 要求,npm 实装验收继续覆盖 Node 20/24。
|
||||||
|
|
||||||
服务集成测试只能在专用容器中运行:设置 `QL_PANEL_PACKAGE_TEST_ISOLATED=1`、指向隔离 PM2 的 `QL_PANEL_TEST_PM2` 和指向正式构建脚本输出目录的 `QL_PANEL_TEST_BUNDLE_DIR`,然后执行 `node scripts/verify-panel-package.cjs verify /tmp/qinglong-npm /tmp/qinglong-npm-results`。容器需已有 Bash、curl、OpenSSL、Python、jq、nginx 和 procps。旧正式包会重置 PATH,因此验收容器将 PM2 6.0.14 安装到 `/usr/local/bin/pm2`,并以此设置 `QL_PANEL_TEST_PM2`;隔离通过专属容器和独立 `PM2_HOME` 保证。测试创建独立的 npm prefix、HOME、PM2_HOME 和数据目录,结束后清理。
|
服务集成测试只能在专用容器中运行:设置 `QL_PANEL_PACKAGE_TEST_ISOLATED=1`、指向隔离 PM2 的 `QL_PANEL_TEST_PM2` 和指向正式构建脚本输出目录的 `QL_PANEL_TEST_BUNDLE_DIR`,然后执行 `node scripts/verify-panel-package.cjs verify /tmp/qinglong-npm /tmp/qinglong-npm-results`。容器需已有 Bash、curl、OpenSSL、Python、jq、nginx 和 procps。旧正式包会重置 PATH,因此验收容器将 PM2 6.0.14 安装到 `/usr/local/bin/pm2`,并以此设置 `QL_PANEL_TEST_PM2`;隔离通过专属容器和独立 `PM2_HOME` 保证。测试创建独立的 npm prefix、HOME、PM2_HOME 和数据目录,结束后清理。
|
||||||
|
|
||||||
通过 npm 覆盖升级前,应先停止服务,执行新包安装,再启动服务。正常 npm 安装的 `postinstall` 会从当前路径对应的 npm 旧包临时目录中自动保留 `.env`,逐字节恢复为权限 `0600` 的普通文件,不需要手工备份和恢复;新目录中已有的 `.env` 不会被覆盖。只读取包名匹配的旧包,不扫描其他安装或备份目录;异常配置(如符号链接)会使安装失败,由 npm 回退旧包,避免静默丢失配置。此机制需要启用 npm 安装脚本,不能使用 `--ignore-scripts`。持久数据仍必须放在包目录以外的 `QL_DATA_DIR`。验收直接检查真实旧 npm 包覆盖安装后配置自动保留,并校验外置用户数据。在线和离线平台包升级仍由事务保留现有 `.env`。
|
通过 npm 覆盖升级前,应先停止服务,执行新包安装,再启动服务。正常 npm 安装的 `postinstall` 会从当前路径对应的 npm 旧包临时目录中自动保留 `.env`,逐字节恢复为权限 `0600` 的普通文件,不需要手工备份和恢复;新目录中已有的 `.env` 不会被覆盖。只读取包名匹配的旧包,不扫描其他安装或备份目录;异常配置(如符号链接)会使安装失败,由 npm 回退旧包,避免静默丢失配置。此机制需要启用 npm 安装脚本,不能使用 `--ignore-scripts`。持久数据仍必须放在包目录以外的 `QL_DATA_DIR`。验收直接检查真实旧 npm 包覆盖安装后配置自动保留,并校验外置用户数据。在线和离线平台包升级仍由事务保留现有 `.env`。
|
||||||
|
|||||||
@@ -12,9 +12,6 @@
|
|||||||
"bugs": {
|
"bugs": {
|
||||||
"url": "https://github.com/whyour/qinglong/issues"
|
"url": "https://github.com/whyour/qinglong/issues"
|
||||||
},
|
},
|
||||||
"engines": {
|
|
||||||
"node": ">=20.18.1"
|
|
||||||
},
|
|
||||||
"files": [
|
"files": [
|
||||||
".env.example",
|
".env.example",
|
||||||
"back/protos/api.proto",
|
"back/protos/api.proto",
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ async function packageFixture(t) {
|
|||||||
await fs.mkdir(path.dirname(path.join(root, name)), { recursive: true });
|
await fs.mkdir(path.dirname(path.join(root, name)), { recursive: true });
|
||||||
await fs.copyFile(path.resolve(name), path.join(root, name));
|
await fs.copyFile(path.resolve(name), path.join(root, name));
|
||||||
};
|
};
|
||||||
for (const name of ['.gitignore', '.npmignore', 'docker/verify-build.cjs', 'docker/build-manifest.cjs', '.env.example', 'ecosystem.config.js', 'version.yaml', 'README.md', 'LICENSE', 'scripts/prepare-npm-package.cjs', 'back/protos/api.proto']) await copy(name);
|
for (const name of ['.gitignore', 'docker/verify-build.cjs', 'docker/build-manifest.cjs', '.env.example', 'ecosystem.config.js', 'version.yaml', 'README.md', 'LICENSE', 'scripts/prepare-npm-package.cjs', 'back/protos/api.proto']) await copy(name);
|
||||||
for (const directory of ['shell', 'sample', 'cli/src/internal/maintenance']) {
|
for (const directory of ['shell', 'sample', 'cli/src/internal/maintenance']) {
|
||||||
await fs.cp(directory, path.join(root, directory), {
|
await fs.cp(directory, path.join(root, directory), {
|
||||||
recursive: true,
|
recursive: true,
|
||||||
@@ -43,10 +43,11 @@ async function packageFixture(t) {
|
|||||||
await fs.writeFile(path.join(root, 'static', name), bytes);
|
await fs.writeFile(path.join(root, 'static', name), bytes);
|
||||||
files[name] = createHash('sha256').update(bytes).digest('hex');
|
files[name] = createHash('sha256').update(bytes).digest('hex');
|
||||||
}
|
}
|
||||||
for (const name of ['.env', 'output/private.json', 'data/config/auth.json', 'shell/preload/env.js', 'shell/preload/__ql_notify__.js', 'shell/preload/__pycache__/env.pyc', 'cli/dist/npm/ql.js']) {
|
for (const name of ['.env', '.env.local', '.env.production', '.aws/credentials', '.ssh/id_rsa', '.vscode/settings.json', '.github/workflows/private.yml', '.tmp/private.json', '.DS_Store', 'output/private.json', 'data/config/auth.json', 'back/private.json', 'back/private.ts', 'sample/tool.ts', 'sample/private.json', 'sample/__pycache__/notify.pyc', 'shell/preload/env.js', 'shell/preload/env.py', 'shell/preload/notify.js', 'shell/preload/__ql_notify__.js', 'shell/preload/lang_env.sh', 'shell/preload/task-notify.json', 'shell/preload/__pycache__/env.pyc', 'cli/dist/npm/ql.js', 'cli/src/internal/private.cjs']) {
|
||||||
await fs.mkdir(path.dirname(path.join(root, name)), { recursive: true });
|
await fs.mkdir(path.dirname(path.join(root, name)), { recursive: true });
|
||||||
await fs.writeFile(path.join(root, name), 'DO_NOT_PUBLISH');
|
await fs.writeFile(path.join(root, name), 'DO_NOT_PUBLISH');
|
||||||
}
|
}
|
||||||
|
await fs.writeFile(path.join(root, '.npmrc'), '# DO_NOT_PUBLISH\n');
|
||||||
const env = { ...process.env, HOME: temp, NODE_ENV: 'production' };
|
const env = { ...process.env, HOME: temp, NODE_ENV: 'production' };
|
||||||
await execute('git', ['init', '-q'], { cwd: root, env });
|
await execute('git', ['init', '-q'], { cwd: root, env });
|
||||||
await execute('git', ['add', '-A'], { cwd: root, env });
|
await execute('git', ['add', '-A'], { cwd: root, env });
|
||||||
@@ -61,6 +62,8 @@ async function packageFixture(t) {
|
|||||||
|
|
||||||
test('real npm pack excludes local state and its installed lifecycle restores verified runtime files', async (t) => {
|
test('real npm pack excludes local state and its installed lifecycle restores verified runtime files', async (t) => {
|
||||||
const { root, output, temp, env, lock, files, pack } = await packageFixture(t);
|
const { root, output, temp, env, lock, files, pack } = await packageFixture(t);
|
||||||
|
// The whitelist must work by itself, even for artifacts ignored by Git.
|
||||||
|
await assert.rejects(fs.access(path.join(root, '.npmignore')), { code: 'ENOENT' });
|
||||||
const packed = await pack();
|
const packed = await pack();
|
||||||
const [archive] = JSON.parse(packed.stdout);
|
const [archive] = JSON.parse(packed.stdout);
|
||||||
checkPackedFiles(archive.files);
|
checkPackedFiles(archive.files);
|
||||||
@@ -71,11 +74,16 @@ test('real npm pack excludes local state and its installed lifecycle restores ve
|
|||||||
const prefix = path.join(temp, 'installed');
|
const prefix = path.join(temp, 'installed');
|
||||||
await execute('npm', ['install', '--offline', '--omit=dev', '--no-audit', '--no-fund', '--cache', path.join(temp, 'cache'), '--prefix', prefix, path.join(output, archive.filename)], { cwd: temp, env, timeout: 60000 });
|
await execute('npm', ['install', '--offline', '--omit=dev', '--no-audit', '--no-fund', '--cache', path.join(temp, 'cache'), '--prefix', prefix, path.join(output, archive.filename)], { cwd: temp, env, timeout: 60000 });
|
||||||
const installed = path.join(prefix, 'node_modules/@whyour/qinglong');
|
const installed = path.join(prefix, 'node_modules/@whyour/qinglong');
|
||||||
|
for (const { path: name } of archive.files)
|
||||||
|
assert.equal((await fs.readFile(path.join(installed, name))).includes(Buffer.from('DO_NOT_PUBLISH')), false, `Private fixture content in npm package: ${name}`);
|
||||||
await assert.rejects(fs.access(path.join(installed, '.env')), { code: 'ENOENT' });
|
await assert.rejects(fs.access(path.join(installed, '.env')), { code: 'ENOENT' });
|
||||||
assert.equal(await fs.readFile(path.join(installed, 'pnpm-lock.yaml'), 'utf8'), lock);
|
assert.equal(await fs.readFile(path.join(installed, 'pnpm-lock.yaml'), 'utf8'), lock);
|
||||||
assert.equal(typeof require(path.join(installed, 'shell/upgrade.cjs')).stageUpgrade, 'function');
|
assert.equal(typeof require(path.join(installed, 'shell/upgrade.cjs')).stageUpgrade, 'function');
|
||||||
assert.ok((await fs.stat(path.join(installed, 'shell/update.sh'))).mode & 0o111);
|
assert.ok((await fs.stat(path.join(installed, 'shell/update.sh'))).mode & 0o111);
|
||||||
for (const name of ['ql', 'task', 'qinglong']) await fs.access(path.join(prefix, 'node_modules/.bin', name));
|
for (const [name, file] of Object.entries(JSON.parse(await fs.readFile(path.join(installed, 'package.json'), 'utf8')).bin)) {
|
||||||
|
await fs.access(path.join(prefix, 'node_modules/.bin', name));
|
||||||
|
assert.ok((await fs.stat(path.join(installed, file))).mode & 0o111, file);
|
||||||
|
}
|
||||||
await fs.writeFile(path.join(installed, 'shell/upgrade-lock.yaml'), 'tampered');
|
await fs.writeFile(path.join(installed, 'shell/upgrade-lock.yaml'), 'tampered');
|
||||||
assert.throws(() => restorePackageLock(installed), /does not match/);
|
assert.throws(() => restorePackageLock(installed), /does not match/);
|
||||||
assert.equal(await fs.readFile(path.join(installed, 'pnpm-lock.yaml'), 'utf8'), lock);
|
assert.equal(await fs.readFile(path.join(installed, 'pnpm-lock.yaml'), 'utf8'), lock);
|
||||||
|
|||||||
Reference in New Issue
Block a user