mirror of
https://github.com/whyour/qinglong.git
synced 2026-11-04 00:42:01 +08:00
Compare commits
7
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ac8567e537 | ||
|
|
59979ee061 | ||
|
|
449a8a471c | ||
|
|
4b4e485305 | ||
|
|
2f359c6bf0 | ||
|
|
a70ec66fe3 | ||
|
|
5038d2d9a0 |
@@ -38,6 +38,10 @@ jobs:
|
||||
if: ${{ !inputs.static_sync_only }}
|
||||
uses: ./.github/workflows/validate.yml
|
||||
|
||||
runtime-tools:
|
||||
if: ${{ !inputs.static_sync_only }}
|
||||
uses: ./.github/workflows/runtime-tools.yml
|
||||
|
||||
code_gitlab:
|
||||
if: ${{ !inputs.static_sync_only }}
|
||||
runs-on: ubuntu-latest
|
||||
@@ -58,17 +62,17 @@ jobs:
|
||||
|
||||
build-static:
|
||||
if: ${{ !inputs.static_sync_only }}
|
||||
needs: validate
|
||||
needs: [validate, runtime-tools]
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
- uses: pnpm/action-setup@v6
|
||||
with:
|
||||
version: "8.3.1"
|
||||
version: "10.34.6"
|
||||
|
||||
- uses: actions/setup-node@v6
|
||||
with:
|
||||
node-version: "20"
|
||||
node-version: "22"
|
||||
cache: "pnpm"
|
||||
cache-dependency-path: pnpm-lock.yaml
|
||||
|
||||
@@ -77,7 +81,12 @@ jobs:
|
||||
pnpm install --frozen-lockfile
|
||||
pnpm build:front
|
||||
pnpm build:back
|
||||
pnpm build:info
|
||||
|
||||
- uses: actions/download-artifact@v7
|
||||
with:
|
||||
name: qinglong-runtime-tools-${{ github.sha }}
|
||||
path: static/
|
||||
- run: pnpm build:info
|
||||
|
||||
- uses: actions/upload-artifact@v6
|
||||
with:
|
||||
@@ -94,6 +103,13 @@ jobs:
|
||||
run: |
|
||||
bash scripts/publish-static.sh "https://${{ secrets.API_TOKEN }}@${GITHUB_REPO}.git" "$GITHUB_BRANCH" "$GITHUB_REF_TYPE"
|
||||
|
||||
npm-package:
|
||||
if: ${{ !inputs.static_sync_only }}
|
||||
needs: build-static
|
||||
uses: ./.github/workflows/npm-package.yml
|
||||
with:
|
||||
static-artifact: qinglong-static-${{ github.sha }}
|
||||
|
||||
static_gitlab:
|
||||
needs: build-static
|
||||
runs-on: ubuntu-latest
|
||||
@@ -209,10 +225,10 @@ jobs:
|
||||
path: static/
|
||||
- uses: pnpm/action-setup@v6
|
||||
with:
|
||||
version: "8.3.1"
|
||||
version: "10.34.6"
|
||||
- uses: actions/setup-node@v6
|
||||
with:
|
||||
node-version: "20"
|
||||
node-version: "22"
|
||||
cache: "pnpm"
|
||||
cache-dependency-path: pnpm-lock.yaml
|
||||
|
||||
@@ -295,10 +311,10 @@ jobs:
|
||||
path: static/
|
||||
- uses: pnpm/action-setup@v6
|
||||
with:
|
||||
version: "8.3.1"
|
||||
version: "10.34.6"
|
||||
- uses: actions/setup-node@v6
|
||||
with:
|
||||
node-version: "20"
|
||||
node-version: "22"
|
||||
cache: "pnpm"
|
||||
cache-dependency-path: pnpm-lock.yaml
|
||||
|
||||
@@ -381,10 +397,10 @@ jobs:
|
||||
path: static/
|
||||
- uses: pnpm/action-setup@v6
|
||||
with:
|
||||
version: "8.3.1"
|
||||
version: "10.34.6"
|
||||
- uses: actions/setup-node@v6
|
||||
with:
|
||||
node-version: "20"
|
||||
node-version: "22"
|
||||
cache: "pnpm"
|
||||
cache-dependency-path: pnpm-lock.yaml
|
||||
|
||||
@@ -453,10 +469,10 @@ jobs:
|
||||
path: static/
|
||||
- uses: pnpm/action-setup@v6
|
||||
with:
|
||||
version: "8.3.1"
|
||||
version: "10.34.6"
|
||||
- uses: actions/setup-node@v6
|
||||
with:
|
||||
node-version: "20"
|
||||
node-version: "22"
|
||||
cache: "pnpm"
|
||||
cache-dependency-path: pnpm-lock.yaml
|
||||
|
||||
@@ -513,46 +529,115 @@ jobs:
|
||||
|
||||
publish:
|
||||
if: ${{ github.repository == 'whyour/qinglong' && github.ref == 'refs/heads/master' }}
|
||||
needs: [build-alpine, build-debian, build-alpine310, build-debian310]
|
||||
needs: [build-alpine, build-debian, build-alpine310, build-debian310, npm-package]
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 10
|
||||
permissions:
|
||||
contents: read
|
||||
id-token: write
|
||||
env:
|
||||
SOURCE_SHA: ${{ github.sha }}
|
||||
VERIFIED_TARBALL_SHA256: ${{ needs.npm-package.outputs.tarball-sha256 }}
|
||||
VERIFIED_PACKAGE_VERSION: ${{ needs.npm-package.outputs.package-version }}
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: pnpm/action-setup@v3
|
||||
- uses: actions/checkout@v6
|
||||
with:
|
||||
version: "8.3.1"
|
||||
|
||||
- uses: actions/setup-node@v3
|
||||
ref: ${{ github.sha }}
|
||||
- uses: actions/setup-node@v6
|
||||
with:
|
||||
node-version: "20"
|
||||
cache: "pnpm"
|
||||
|
||||
- name: build front and back
|
||||
run: |
|
||||
pnpm install --frozen-lockfile
|
||||
pnpm build:front
|
||||
pnpm build:back
|
||||
|
||||
- name: Set up Node for trusted publishing
|
||||
uses: actions/setup-node@v6
|
||||
with:
|
||||
node-version: '24'
|
||||
registry-url: https://registry.npmjs.org
|
||||
node-version: '22.23.3'
|
||||
package-manager-cache: false
|
||||
|
||||
- name: Publish npm package with OIDC
|
||||
- uses: actions/download-artifact@v7
|
||||
with:
|
||||
name: ${{ needs.npm-package.outputs.artifact-name }}
|
||||
path: ${{ runner.temp }}/main-npm-publish/
|
||||
- uses: actions/download-artifact@v7
|
||||
with:
|
||||
name: qinglong-runtime-tools-${{ github.sha }}
|
||||
path: ${{ runner.temp }}/npm-publisher-runtime-tools/
|
||||
- name: Verify the gate-approved archive and its source receipts
|
||||
shell: bash
|
||||
run: |
|
||||
set -euo pipefail
|
||||
node <<'NODE'
|
||||
const fs = require('node:fs'), path = require('node:path'), crypto = require('node:crypto'), assert = require('node:assert/strict');
|
||||
const { spawnSync } = require('node:child_process');
|
||||
const hash = (bytes) => crypto.createHash('sha256').update(bytes).digest('hex');
|
||||
const fileHash = (file) => hash(fs.readFileSync(file));
|
||||
const directory = path.join(process.env.RUNNER_TEMP, 'main-npm-publish');
|
||||
const summary = JSON.parse(fs.readFileSync(path.join(directory, 'package-build-summary.json')));
|
||||
const pkg = JSON.parse(fs.readFileSync('package.json'));
|
||||
assert.match(process.env.VERIFIED_TARBALL_SHA256 || '', /^[a-f0-9]{64}$/);
|
||||
assert.equal(summary.sourceCommit, process.env.SOURCE_SHA); assert.equal(summary.previewMetadata, false);
|
||||
assert.deepEqual(summary.sourceMetadataChanges, []); assert(summary.helperProvenance.length > 0);
|
||||
assert(summary.helperProvenance.every((item) => item.matchesGitSource));
|
||||
assert.equal(summary.package, pkg.name); assert.equal(summary.packageVersion, pkg.version);
|
||||
assert.equal(summary.packageVersion, process.env.VERIFIED_PACKAGE_VERSION);
|
||||
assert.equal(summary.productionManifestSHA256, fileHash('package.json')); assert.equal(summary.productionLockSHA256, fileHash('pnpm-lock.yaml'));
|
||||
const source = spawnSync('git', ['archive', process.env.SOURCE_SHA], { maxBuffer: 128 * 1024 * 1024 });
|
||||
assert.equal(source.status, 0); assert.equal(hash(source.stdout), summary.sourceArchiveSHA256);
|
||||
assert.equal(summary.runtimeToolsIncluded, true); assert.equal(summary.runtimeTools.sourceCommit, process.env.SOURCE_SHA);
|
||||
assert.equal(summary.runtimeTools.builder.platform, 'linux'); assert.equal(summary.runtimeTools.builder.releaseBuild, true);
|
||||
assert.equal(path.basename(summary.tarball.filename), summary.tarball.filename); assert(summary.tarball.filename.endsWith('.tgz'));
|
||||
assert.deepEqual(fs.readdirSync(directory).filter((name) => name.endsWith('.tgz')), [summary.tarball.filename]);
|
||||
const tarball = path.join(directory, summary.tarball.filename);
|
||||
assert.equal(summary.tarball.sha256, process.env.VERIFIED_TARBALL_SHA256);
|
||||
assert.equal(fileHash(tarball), process.env.VERIFIED_TARBALL_SHA256); assert.equal(fs.statSync(tarball).size, summary.tarball.bytes);
|
||||
const tar = (args) => {
|
||||
const result = spawnSync('tar', args, { maxBuffer: 128 * 1024 * 1024 });
|
||||
assert.equal(result.status, 0, 'Cannot read the approved archive'); return result.stdout;
|
||||
};
|
||||
const entries = tar(['-tzf', tarball]).toString().split('\n').filter(Boolean);
|
||||
assert.equal(entries.length, new Set(entries).size);
|
||||
for (const name of entries) assert(name.startsWith('package/') && !name.split('/').includes('..') && !name.endsWith('.node') && !name.split('/').some((part) => ['.npmrc', '.git', '.pnpm', '_cacache'].includes(part)), 'Forbidden archive item ' + name);
|
||||
const read = (relative) => tar(['-xOf', tarball, 'package/' + relative]);
|
||||
const receipt = JSON.parse(read('qinglong-npm-dependency-proof.json'));
|
||||
const { tarball: ignoredTarball, ...producerReceipt } = summary; assert.deepEqual(receipt, producerReceipt);
|
||||
assert.deepEqual(entries.map((name) => name.slice('package/'.length)).sort(), receipt.publishedFilePaths);
|
||||
const published = JSON.parse(read('package.json')); assert.equal(published.name, pkg.name); assert.equal(published.version, pkg.version);
|
||||
assert.equal(hash(read('package.json')), receipt.publishedRootManifestSHA256);
|
||||
const buildInfo = read('static/build-info.json'), build = JSON.parse(buildInfo);
|
||||
assert.equal(hash(buildInfo), summary.ciBuildInfoSHA256); assert.equal(build.sourceCommit, process.env.SOURCE_SHA); assert.equal(build.dirty, false);
|
||||
assert.equal(build.lockfileSha256, summary.productionLockSHA256);
|
||||
assert.deepEqual(entries.filter((name) => name.startsWith('package/static/')).map((name) => name.slice('package/static/'.length)).sort(), [...Object.keys(build.files), 'build-info.json'].sort());
|
||||
const toolsRoot = path.join(process.env.RUNNER_TEMP, 'npm-publisher-runtime-tools');
|
||||
for (const [name, expected] of [['runtime-tools.tgz', summary.runtimeTools.archiveSHA256], ['runtime-tools-proof.json', summary.runtimeTools.proofSHA256]]) {
|
||||
assert.equal(fileHash(path.join(toolsRoot, name)), expected); assert.equal(hash(read('static/' + name)), expected); assert.equal(build.files[name], expected);
|
||||
}
|
||||
const tools = JSON.parse(fs.readFileSync(path.join(toolsRoot, 'runtime-tools-proof.json')));
|
||||
assert.equal(tools.sourceCommit, process.env.SOURCE_SHA); assert.equal(tools.archive.sha256, summary.runtimeTools.archiveSHA256);
|
||||
assert.equal(tools.builder.platform, 'linux'); assert.equal(tools.builder.releaseBuild, true);
|
||||
assert.equal(tools.sourceAttestation.verified, true); assert.equal(tools.sourceAttestation.head, process.env.SOURCE_SHA); assert(tools.inputs.length > 0);
|
||||
for (const input of tools.inputs) {
|
||||
assert(input.path && !path.isAbsolute(input.path) && !input.path.split(/[\\/]/).includes('..'));
|
||||
assert.equal(fileHash(input.path), input.sha256, 'Tool source input changed: ' + input.path);
|
||||
}
|
||||
fs.appendFileSync(process.env.GITHUB_ENV, 'QINGLONG_PUBLISH_TARBALL=' + tarball + '\n');
|
||||
console.log(JSON.stringify({ sourceCommit: summary.sourceCommit, packageVersion: summary.packageVersion, tarballSHA256: summary.tarball.sha256, cleanSource: true, runtimeToolsIncluded: true }));
|
||||
NODE
|
||||
- name: Install the same fixed npm CLI for trusted publishing
|
||||
run: |
|
||||
node scripts/install-runtime-tools.cjs \
|
||||
--archive "$RUNNER_TEMP/npm-publisher-runtime-tools/runtime-tools.tgz" \
|
||||
--proof "$RUNNER_TEMP/npm-publisher-runtime-tools/runtime-tools-proof.json" \
|
||||
--prefix "$RUNNER_TEMP/npm-publisher-tools"
|
||||
- name: Publish the verified npm archive with OIDC
|
||||
shell: bash
|
||||
run: |
|
||||
set -euo pipefail
|
||||
npm_cli="$RUNNER_TEMP/npm-publisher-tools/lib/node_modules/npm/bin/npm-cli.js"
|
||||
export NPM_CONFIG_USERCONFIG="$RUNNER_TEMP/npm-publish-public.npmrc"
|
||||
export NPM_CONFIG_GLOBALCONFIG=/dev/null
|
||||
export NPM_CONFIG_CACHE="$RUNNER_TEMP/npm-publish-cache"
|
||||
printf 'registry=https://registry.npmjs.org/\n' > "$NPM_CONFIG_USERCONFIG"
|
||||
test "$(node "$npm_cli" --version)" = '12.2.0'
|
||||
name=$(node -p 'require("./package.json").name')
|
||||
version=$(node -p 'require("./package.json").version')
|
||||
if npm view "$name@$version" version --json --registry=https://registry.npmjs.org > "$RUNNER_TEMP/npm-version.json" 2> "$RUNNER_TEMP/npm-version-error.log"; then
|
||||
if node "$npm_cli" view "$name@$version" version --json --registry=https://registry.npmjs.org > "$RUNNER_TEMP/npm-version.json" 2> "$RUNNER_TEMP/npm-version-error.log"; then
|
||||
jq -e --arg version "$version" '. == $version' "$RUNNER_TEMP/npm-version.json" > /dev/null
|
||||
echo "::notice::$name@$version is already published; skipping npm publication for this image rebuild."
|
||||
elif jq -e '.error.code == "E404"' "$RUNNER_TEMP/npm-version.json" > /dev/null; then
|
||||
npm publish --access public --registry=https://registry.npmjs.org
|
||||
node "$npm_cli" publish "$QINGLONG_PUBLISH_TARBALL" --ignore-scripts --access public --registry=https://registry.npmjs.org
|
||||
else
|
||||
echo '::error::Could not check the published npm version; refusing to publish.'
|
||||
exit 1
|
||||
|
||||
@@ -0,0 +1,312 @@
|
||||
name: Verify main npm archive
|
||||
|
||||
on:
|
||||
workflow_call:
|
||||
inputs:
|
||||
static-artifact:
|
||||
description: Complete static artifact including the same-source runtime tools
|
||||
required: true
|
||||
type: string
|
||||
outputs:
|
||||
artifact-name:
|
||||
description: Main npm archive and producer summary, after every consumer passes
|
||||
value: ${{ jobs.verified.outputs.artifact-name }}
|
||||
tarball-sha256:
|
||||
description: SHA256 of the exact archive consumed by all six cases
|
||||
value: ${{ jobs.verified.outputs.tarball-sha256 }}
|
||||
package-version:
|
||||
description: Source package version verified against the archive
|
||||
value: ${{ jobs.verified.outputs.package-version }}
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
env:
|
||||
SOURCE_SHA: ${{ github.event.pull_request.head.sha || github.sha }}
|
||||
|
||||
jobs:
|
||||
archive:
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 20
|
||||
outputs:
|
||||
artifact-name: ${{ steps.archive.outputs.artifact-name }}
|
||||
tarball-sha256: ${{ steps.archive.outputs.tarball-sha256 }}
|
||||
package-version: ${{ steps.archive.outputs.package-version }}
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
with:
|
||||
ref: ${{ github.event.pull_request.head.sha || github.sha }}
|
||||
- uses: actions/setup-node@v6
|
||||
with:
|
||||
node-version: '22.23.3'
|
||||
package-manager-cache: false
|
||||
- uses: actions/download-artifact@v7
|
||||
with:
|
||||
name: ${{ inputs.static-artifact }}
|
||||
path: static/
|
||||
- name: Verify tool artifact source before executing the producer tools
|
||||
shell: bash
|
||||
run: |
|
||||
set -euo pipefail
|
||||
node <<'NODE'
|
||||
const fs = require('node:fs'), crypto = require('node:crypto'), assert = require('node:assert/strict');
|
||||
const hash = (file) => crypto.createHash('sha256').update(fs.readFileSync(file)).digest('hex');
|
||||
const build = JSON.parse(fs.readFileSync('static/build-info.json'));
|
||||
const tools = JSON.parse(fs.readFileSync('static/runtime-tools-proof.json'));
|
||||
assert.equal(build.sourceCommit, process.env.SOURCE_SHA); assert.equal(build.dirty, false);
|
||||
assert.equal(build.lockfileSha256, hash('pnpm-lock.yaml'));
|
||||
assert.equal(tools.sourceCommit, process.env.SOURCE_SHA);
|
||||
assert.equal(tools.builder.platform, 'linux'); assert.equal(tools.builder.releaseBuild, true);
|
||||
assert.equal(tools.sourceAttestation.verified, true); assert.equal(tools.sourceAttestation.head, process.env.SOURCE_SHA);
|
||||
for (const name of ['runtime-tools.tgz', 'runtime-tools-proof.json']) assert.equal(hash(`static/${name}`), build.files[name]);
|
||||
assert.equal(hash('static/runtime-tools.tgz'), tools.archive.sha256);
|
||||
for (const input of tools.inputs) {
|
||||
assert(input.path && !input.path.startsWith('/') && !input.path.split(/[\\/]/).includes('..'));
|
||||
assert.equal(hash(input.path), input.sha256, `Runtime source input changed: ${input.path}`);
|
||||
}
|
||||
NODE
|
||||
- name: Install fixed producer tools into an isolated prefix
|
||||
run: |
|
||||
node scripts/install-runtime-tools.cjs \
|
||||
--archive "$GITHUB_WORKSPACE/static/runtime-tools.tgz" \
|
||||
--proof "$GITHUB_WORKSPACE/static/runtime-tools-proof.json" \
|
||||
--prefix "$RUNNER_TEMP/npm-producer-tools"
|
||||
- name: Export the single frozen production graph
|
||||
run: |
|
||||
node scripts/build-npm-package.cjs \
|
||||
--repo "$GITHUB_WORKSPACE" \
|
||||
--source-ref "$SOURCE_SHA" \
|
||||
--artifact-dir "$GITHUB_WORKSPACE/static" \
|
||||
--pnpm-cli "$RUNNER_TEMP/npm-producer-tools/lib/node_modules/qinglong-runtime-tools/node_modules/pnpm/bin/pnpm.cjs" \
|
||||
--npm-cli "$RUNNER_TEMP/npm-producer-tools/lib/node_modules/npm/bin/npm-cli.js" \
|
||||
--output "$RUNNER_TEMP/qinglong-npm-export"
|
||||
- name: Verify and retain the exact consumer archive
|
||||
id: archive
|
||||
shell: bash
|
||||
run: |
|
||||
set -euo pipefail
|
||||
node <<'NODE'
|
||||
const fs = require('node:fs'), path = require('node:path'), crypto = require('node:crypto'), assert = require('node:assert/strict');
|
||||
const root = path.join(process.env.RUNNER_TEMP, 'qinglong-npm-export');
|
||||
const summary = JSON.parse(fs.readFileSync(path.join(root, 'package-build-summary.json')));
|
||||
const pkg = JSON.parse(fs.readFileSync('package.json'));
|
||||
const hash = (file) => crypto.createHash('sha256').update(fs.readFileSync(file)).digest('hex');
|
||||
assert.equal(summary.sourceCommit, process.env.SOURCE_SHA); assert.equal(summary.previewMetadata, false);
|
||||
assert.deepEqual(summary.sourceMetadataChanges, []); assert(summary.helperProvenance.every((item) => item.matchesGitSource));
|
||||
assert.equal(summary.package, pkg.name); assert.equal(summary.packageVersion, pkg.version);
|
||||
assert.equal(summary.productionManifestSHA256, hash('package.json')); assert.equal(summary.productionLockSHA256, hash('pnpm-lock.yaml'));
|
||||
assert.equal(summary.runtimeToolsIncluded, true); assert.equal(summary.runtimeTools.sourceCommit, process.env.SOURCE_SHA);
|
||||
assert.equal(summary.runtimeTools.builder.releaseBuild, true);
|
||||
assert.equal(path.basename(summary.tarball.filename), summary.tarball.filename);
|
||||
const tarball = path.join(root, 'artifacts', summary.tarball.filename);
|
||||
assert.equal(hash(tarball), summary.tarball.sha256); assert.equal(fs.statSync(tarball).size, summary.tarball.bytes);
|
||||
const delivery = path.join(process.env.RUNNER_TEMP, 'main-npm-delivery'); fs.mkdirSync(delivery);
|
||||
fs.copyFileSync(tarball, path.join(delivery, summary.tarball.filename));
|
||||
fs.copyFileSync(path.join(root, 'package-build-summary.json'), path.join(delivery, 'package-build-summary.json'));
|
||||
fs.appendFileSync(process.env.GITHUB_OUTPUT, `artifact-name=qinglong-main-npm-${process.env.SOURCE_SHA}\ntarball-sha256=${summary.tarball.sha256}\npackage-version=${pkg.version}\n`);
|
||||
NODE
|
||||
- uses: actions/upload-artifact@v6
|
||||
with:
|
||||
name: ${{ steps.archive.outputs.artifact-name }}
|
||||
path: ${{ runner.temp }}/main-npm-delivery/
|
||||
compression-level: 0
|
||||
if-no-files-found: error
|
||||
- name: Preserve producer diagnostics
|
||||
if: always()
|
||||
uses: actions/upload-artifact@v6
|
||||
with:
|
||||
name: qinglong-main-npm-producer-${{ github.event.pull_request.head.sha || github.sha }}
|
||||
path: ${{ runner.temp }}/qinglong-npm-export/logs/
|
||||
if-no-files-found: ignore
|
||||
|
||||
consumer:
|
||||
needs: archive
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 20
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
client:
|
||||
- {npm: '10', mode: local}
|
||||
- {npm: '12', mode: local}
|
||||
- {npm: '12', mode: global}
|
||||
sqlite: [prebuilt, source]
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
with:
|
||||
ref: ${{ github.event.pull_request.head.sha || github.sha }}
|
||||
- uses: actions/setup-node@v6
|
||||
with:
|
||||
node-version: '22.23.3'
|
||||
package-manager-cache: false
|
||||
- uses: actions/download-artifact@v7
|
||||
with:
|
||||
name: ${{ needs.archive.outputs.artifact-name }}
|
||||
path: ${{ runner.temp }}/main-npm-input/
|
||||
- uses: actions/download-artifact@v7
|
||||
with:
|
||||
name: qinglong-runtime-tools-${{ github.event.pull_request.head.sha || github.sha }}
|
||||
path: ${{ runner.temp }}/runtime-tools-input/
|
||||
- name: Verify archive and client tools against the producer source
|
||||
env:
|
||||
VERIFIED_TARBALL_SHA256: ${{ needs.archive.outputs.tarball-sha256 }}
|
||||
shell: bash
|
||||
run: |
|
||||
set -euo pipefail
|
||||
node <<'NODE'
|
||||
const fs = require('node:fs'), path = require('node:path'), crypto = require('node:crypto'), assert = require('node:assert/strict');
|
||||
const directory = path.join(process.env.RUNNER_TEMP, 'main-npm-input');
|
||||
const summary = JSON.parse(fs.readFileSync(path.join(directory, 'package-build-summary.json')));
|
||||
const pkg = JSON.parse(fs.readFileSync('package.json'));
|
||||
const hash = (file) => crypto.createHash('sha256').update(fs.readFileSync(file)).digest('hex');
|
||||
assert.equal(summary.sourceCommit, process.env.SOURCE_SHA); assert.equal(summary.previewMetadata, false);
|
||||
assert.equal(summary.package, pkg.name); assert.equal(summary.packageVersion, pkg.version);
|
||||
assert.equal(summary.productionManifestSHA256, hash('package.json')); assert.equal(summary.productionLockSHA256, hash('pnpm-lock.yaml'));
|
||||
assert.equal(summary.runtimeToolsIncluded, true); assert.equal(summary.runtimeTools.sourceCommit, process.env.SOURCE_SHA);
|
||||
assert.equal(summary.tarball.sha256, process.env.VERIFIED_TARBALL_SHA256);
|
||||
assert.equal(path.basename(summary.tarball.filename), summary.tarball.filename);
|
||||
assert.deepEqual(fs.readdirSync(directory).filter((name) => name.endsWith('.tgz')), [summary.tarball.filename]);
|
||||
assert.equal(hash(path.join(directory, summary.tarball.filename)), process.env.VERIFIED_TARBALL_SHA256);
|
||||
const tools = path.join(process.env.RUNNER_TEMP, 'runtime-tools-input');
|
||||
assert.equal(hash(path.join(tools, 'runtime-tools.tgz')), summary.runtimeTools.archiveSHA256);
|
||||
assert.equal(hash(path.join(tools, 'runtime-tools-proof.json')), summary.runtimeTools.proofSHA256);
|
||||
NODE
|
||||
- name: Install native compiler prerequisites
|
||||
if: matrix.sqlite == 'source'
|
||||
run: sudo apt-get update && sudo apt-get install -y --no-install-recommends build-essential python3
|
||||
- name: Select the isolated npm client
|
||||
env:
|
||||
CLIENT_NPM: ${{ matrix.client.npm }}
|
||||
shell: bash
|
||||
run: |
|
||||
set -euo pipefail
|
||||
npm10_cli="$(node -p "require('node:path').join(require('node:path').dirname(process.execPath), '../lib/node_modules/npm/bin/npm-cli.js')")"
|
||||
if [[ "$(node "$npm10_cli" --version)" != 10.* ]]; then
|
||||
echo '::error::The pinned Node22 client must supply npm10.'
|
||||
exit 1
|
||||
fi
|
||||
node scripts/install-runtime-tools.cjs \
|
||||
--archive "$RUNNER_TEMP/runtime-tools-input/runtime-tools.tgz" \
|
||||
--proof "$RUNNER_TEMP/runtime-tools-input/runtime-tools-proof.json" \
|
||||
--prefix "$RUNNER_TEMP/npm-consumer-tools"
|
||||
if [[ "$CLIENT_NPM" == 10 ]]; then
|
||||
npm_cli="$npm10_cli"
|
||||
else
|
||||
npm_cli="$RUNNER_TEMP/npm-consumer-tools/lib/node_modules/npm/bin/npm-cli.js"
|
||||
fi
|
||||
echo "QINGLONG_CONSUMER_NPM_CLI=$npm_cli" >> "$GITHUB_ENV"
|
||||
- name: Consume the same archive and verify the selected SQLite installation
|
||||
env:
|
||||
CONSUMER_MODE: ${{ matrix.client.mode }}
|
||||
SQLITE_INSTALL: ${{ matrix.sqlite }}
|
||||
shell: bash
|
||||
run: |
|
||||
set -euo pipefail
|
||||
extra_args=()
|
||||
if [[ "$CONSUMER_MODE" == global ]]; then extra_args+=(--global); fi
|
||||
if [[ "$SQLITE_INSTALL" == source ]]; then extra_args+=(--source-build); else extra_args+=(--require-prebuilt); fi
|
||||
tarball="$(node -e 'const fs=require("node:fs"),p=require("node:path"); const d=process.argv[1]; const s=JSON.parse(fs.readFileSync(p.join(d,"package-build-summary.json"))); process.stdout.write(p.join(d,s.tarball.filename))' "$RUNNER_TEMP/main-npm-input")"
|
||||
node scripts/verify-npm-package.cjs \
|
||||
--tarball "$tarball" \
|
||||
--build-summary "$RUNNER_TEMP/main-npm-input/package-build-summary.json" \
|
||||
--npm-cli "$QINGLONG_CONSUMER_NPM_CLI" \
|
||||
--output "$RUNNER_TEMP/qinglong-npm-consumer" \
|
||||
"${extra_args[@]}"
|
||||
- name: Preserve actual consumer evidence
|
||||
if: always()
|
||||
uses: actions/upload-artifact@v6
|
||||
with:
|
||||
name: qinglong-main-npm-consumer-${{ matrix.client.npm }}-${{ matrix.client.mode }}-${{ matrix.sqlite }}-${{ github.event.pull_request.head.sha || github.sha }}
|
||||
path: |
|
||||
${{ runner.temp }}/qinglong-npm-consumer/*.log
|
||||
${{ runner.temp }}/qinglong-npm-consumer/npm-consumer-validation.json
|
||||
if-no-files-found: ignore
|
||||
|
||||
source-fallback:
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 20
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
with:
|
||||
ref: ${{ github.event.pull_request.head.sha || github.sha }}
|
||||
- uses: actions/setup-node@v6
|
||||
with:
|
||||
node-version: '22.23.3'
|
||||
package-manager-cache: false
|
||||
- uses: actions/download-artifact@v7
|
||||
with:
|
||||
name: ${{ inputs.static-artifact }}
|
||||
path: static/
|
||||
- name: Install native compiler prerequisites
|
||||
run: sudo apt-get update && sudo apt-get install -y --no-install-recommends build-essential python3
|
||||
- name: Prepare source using an older manager and the same verified static tools
|
||||
env:
|
||||
npm_config_build_from_source: 'true'
|
||||
shell: bash
|
||||
run: |
|
||||
set -euo pipefail
|
||||
mkdir "$RUNNER_TEMP/source-tool-fallback"
|
||||
git archive "$SOURCE_SHA" | tar -x -C "$RUNNER_TEMP/source-tool-fallback"
|
||||
npm10_cli="$(node -p "require('node:path').join(require('node:path').dirname(process.execPath), '../lib/node_modules/npm/bin/npm-cli.js')")"
|
||||
printf 'registry=https://registry.npmjs.org/\n' > "$RUNNER_TEMP/legacy-pnpm-public.npmrc"
|
||||
export NPM_CONFIG_USERCONFIG="$RUNNER_TEMP/legacy-pnpm-public.npmrc"
|
||||
export npm_config_userconfig="$NPM_CONFIG_USERCONFIG"
|
||||
export NPM_CONFIG_GLOBALCONFIG=/dev/null npm_config_globalconfig=/dev/null
|
||||
export NPM_CONFIG_CACHE="$RUNNER_TEMP/legacy-pnpm-cache"
|
||||
export PNPM_HOME="$RUNNER_TEMP/legacy-pnpm-home"
|
||||
export XDG_CACHE_HOME="$RUNNER_TEMP/legacy-pnpm-xdg-cache" XDG_STATE_HOME="$RUNNER_TEMP/legacy-pnpm-xdg-state"
|
||||
node "$npm10_cli" install --global pnpm@8.3.1 \
|
||||
--prefix "$RUNNER_TEMP/legacy-pnpm" --ignore-scripts --no-audit --no-fund \
|
||||
--userconfig "$RUNNER_TEMP/legacy-pnpm-public.npmrc" --globalconfig /dev/null \
|
||||
--cache "$RUNNER_TEMP/legacy-pnpm-cache" --registry=https://registry.npmjs.org \
|
||||
2>&1 | tee "$RUNNER_TEMP/legacy-pnpm-install.log"
|
||||
legacy_cli="$RUNNER_TEMP/legacy-pnpm/lib/node_modules/pnpm/bin/pnpm.cjs"
|
||||
node "$legacy_cli" --version | tee "$RUNNER_TEMP/legacy-pnpm-version.log"
|
||||
test "$(cat "$RUNNER_TEMP/legacy-pnpm-version.log")" = '8.3.1'
|
||||
node -e 'const fs=require("node:fs"),os=require("node:os"); fs.writeFileSync(process.argv[1],JSON.stringify(fs.readdirSync(os.tmpdir()).filter(n=>n.startsWith("qinglong-source-tools-")).sort()))' "$RUNNER_TEMP/source-tool-prefixes-before.json"
|
||||
node scripts/install-panel-dependencies.cjs \
|
||||
--root "$RUNNER_TEMP/source-tool-fallback" \
|
||||
--pnpm-cli "$legacy_cli" \
|
||||
--static-dir "$GITHUB_WORKSPACE/static" \
|
||||
--source-commit "$SOURCE_SHA" \
|
||||
2>&1 | tee "$RUNNER_TEMP/source-tool-fallback.log"
|
||||
grep -q 'SOLINK_MODULE' "$RUNNER_TEMP/source-tool-fallback.log"
|
||||
grep -q 'node-gyp@12.4.0' "$RUNNER_TEMP/source-tool-fallback.log"
|
||||
grep -q '"prefixScope":"fresh temporary only"' "$RUNNER_TEMP/source-tool-fallback.log"
|
||||
node <<'NODE'
|
||||
const fs = require('node:fs'), path = require('node:path'), os = require('node:os'), crypto = require('node:crypto'), assert = require('node:assert/strict');
|
||||
const { createRequire } = require('node:module');
|
||||
const root = path.join(process.env.RUNNER_TEMP, 'source-tool-fallback');
|
||||
const hash = (file) => crypto.createHash('sha256').update(fs.readFileSync(file)).digest('hex');
|
||||
assert.equal(hash(path.join(root, 'pnpm-lock.yaml')), hash('pnpm-lock.yaml'));
|
||||
const before = JSON.parse(fs.readFileSync(path.join(process.env.RUNNER_TEMP, 'source-tool-prefixes-before.json')));
|
||||
assert.deepEqual(fs.readdirSync(os.tmpdir()).filter((name) => name.startsWith('qinglong-source-tools-')).sort(), before);
|
||||
const requireSource = createRequire(path.join(root, 'package.json')); requireSource('sqlite3');
|
||||
const binding = Object.keys(require.cache).find((file) => file.endsWith('.node'));
|
||||
assert(binding); assert.equal(fs.readFileSync(binding).subarray(0, 4).toString('hex'), '7f454c46');
|
||||
fs.writeFileSync(path.join(process.env.RUNNER_TEMP, 'source-tool-native-proof.json'), JSON.stringify({ sourceCommit: process.env.SOURCE_SHA, productionLockSHA256: hash('pnpm-lock.yaml'), legacyManagerVersion: '8.3.1', legacyManagerCliSHA256: hash(path.join(process.env.RUNNER_TEMP, 'legacy-pnpm/lib/node_modules/pnpm/bin/pnpm.cjs')), binding: path.relative(root, binding), bindingSHA256: hash(binding), temporaryPrefixRemoved: true }, null, 2));
|
||||
NODE
|
||||
node "$RUNNER_TEMP/source-tool-fallback/scripts/verify-production-dependencies.cjs" \
|
||||
2>&1 | tee "$RUNNER_TEMP/source-tool-fallback-protocols.log"
|
||||
- name: Preserve source migration evidence
|
||||
if: always()
|
||||
uses: actions/upload-artifact@v6
|
||||
with:
|
||||
name: qinglong-source-tool-fallback-${{ github.event.pull_request.head.sha || github.sha }}
|
||||
path: |
|
||||
${{ runner.temp }}/legacy-pnpm-install.log
|
||||
${{ runner.temp }}/legacy-pnpm-version.log
|
||||
${{ runner.temp }}/source-tool-fallback.log
|
||||
${{ runner.temp }}/source-tool-fallback-protocols.log
|
||||
${{ runner.temp }}/source-tool-native-proof.json
|
||||
if-no-files-found: ignore
|
||||
|
||||
verified:
|
||||
needs: [archive, consumer, source-fallback]
|
||||
runs-on: ubuntu-latest
|
||||
outputs:
|
||||
artifact-name: ${{ needs.archive.outputs.artifact-name }}
|
||||
tarball-sha256: ${{ needs.archive.outputs.tarball-sha256 }}
|
||||
package-version: ${{ needs.archive.outputs.package-version }}
|
||||
steps:
|
||||
- run: echo 'The exact main npm archive passed six Linux consumers and the older-manager source migration.'
|
||||
@@ -19,7 +19,11 @@ jobs:
|
||||
regression:
|
||||
uses: ./.github/workflows/validate.yml
|
||||
|
||||
runtime-tools:
|
||||
uses: ./.github/workflows/runtime-tools.yml
|
||||
|
||||
static:
|
||||
needs: runtime-tools
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 15
|
||||
steps:
|
||||
@@ -28,13 +32,19 @@ jobs:
|
||||
ref: ${{ github.event.pull_request.head.sha || github.sha }}
|
||||
- uses: pnpm/action-setup@v6
|
||||
with:
|
||||
version: '8.3.1'
|
||||
version: '10.34.6'
|
||||
- uses: actions/setup-node@v6
|
||||
with:
|
||||
node-version: '20'
|
||||
node-version: '22'
|
||||
cache: pnpm
|
||||
- run: pnpm install --frozen-lockfile
|
||||
- run: pnpm build:front && pnpm build:back && pnpm build:info
|
||||
- run: pnpm audit --prod --audit-level high
|
||||
- run: pnpm build:front && pnpm build:back
|
||||
- uses: actions/download-artifact@v7
|
||||
with:
|
||||
name: qinglong-runtime-tools-${{ github.event.pull_request.head.sha || github.sha }}
|
||||
path: static/
|
||||
- run: pnpm build:info
|
||||
- uses: actions/upload-artifact@v6
|
||||
with:
|
||||
name: release-readiness-static
|
||||
@@ -42,8 +52,41 @@ jobs:
|
||||
include-hidden-files: true
|
||||
if-no-files-found: error
|
||||
|
||||
sqlite-install:
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 15
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
with:
|
||||
ref: ${{ github.event.pull_request.head.sha || github.sha }}
|
||||
- uses: pnpm/action-setup@v6
|
||||
with:
|
||||
version: '10.34.6'
|
||||
- uses: actions/setup-node@v6
|
||||
with:
|
||||
node-version: '22'
|
||||
cache: pnpm
|
||||
- run: pnpm install --prod --frozen-lockfile
|
||||
- name: Verify precompiled SQLite and production protocols
|
||||
run: node scripts/verify-production-dependencies.cjs
|
||||
- name: Rebuild SQLite from source
|
||||
env:
|
||||
npm_config_build_from_source: 'true'
|
||||
PYTHON: /usr/bin/python3
|
||||
run: |
|
||||
export npm_config_node_gyp="$(node -p 'require("node:module").createRequire(require.resolve("sqlite3/package.json")).resolve("node-gyp/bin/node-gyp.js")')"
|
||||
pnpm rebuild @whyour/sqlite3
|
||||
- name: Verify source-built SQLite and production protocols
|
||||
run: node scripts/verify-production-dependencies.cjs
|
||||
|
||||
npm-package:
|
||||
needs: static
|
||||
uses: ./.github/workflows/npm-package.yml
|
||||
with:
|
||||
static-artifact: release-readiness-static
|
||||
|
||||
images:
|
||||
needs: [regression, static]
|
||||
needs: [regression, static, sqlite-install]
|
||||
runs-on: ubuntu-22.04
|
||||
timeout-minutes: 60
|
||||
strategy:
|
||||
@@ -120,17 +163,32 @@ jobs:
|
||||
run: |
|
||||
docker run --rm --network none --entrypoint node \
|
||||
qinglong-readiness:${{ matrix.image }} \
|
||||
-e 'const assert=require("node:assert/strict"); const {createRequire}=require("node:module"); for(const [name,want] of Object.entries({"@grpc/grpc-js":"1.14.5",compression:"1.8.2",multer:"2.4.0",sequelize:"6.37.8"}))assert.equal(require(name+"/package.json").version,want); const expressRequire=createRequire(require.resolve("express")); assert.equal(expressRequire("proxy-addr/package.json").version,"2.0.8"); const sockRequire=createRequire(require.resolve("sockjs")); assert.equal(sockRequire("websocket-driver/package.json").version,"0.7.5"); require("sqlite3"); require("nodemailer"); require("undici"); console.log("Minimal production patches and runtime imports verified")'
|
||||
- name: Export native Debian image for isolated dev validation
|
||||
--eval 'console.log(JSON.stringify({node: process.version, arch: process.arch, platform: process.platform}))'
|
||||
docker run --rm --network none --entrypoint python3 \
|
||||
qinglong-readiness:${{ matrix.image }} --version
|
||||
docker run --rm --network none --entrypoint node \
|
||||
qinglong-readiness:${{ matrix.image }} \
|
||||
scripts/verify-system-runtime.cjs
|
||||
docker run --rm --network none --entrypoint node \
|
||||
qinglong-readiness:${{ matrix.image }} \
|
||||
scripts/verify-production-dependencies.cjs
|
||||
- name: Archive verified native Debian image for isolated dev validation
|
||||
if: matrix.image == 'debian'
|
||||
shell: bash
|
||||
run: |
|
||||
docker save qinglong-readiness:debian | gzip -1 > release-readiness-debian-image.tar.gz
|
||||
docker image inspect qinglong-readiness:debian \
|
||||
--format '{"id":{{json .Id}},"revision":{{json (index .Config.Labels "org.opencontainers.image.revision")}},"architecture":{{json .Architecture}},"os":{{json .Os}}}' \
|
||||
> release-readiness-debian-image.json
|
||||
docker save qinglong-readiness:debian | gzip > release-readiness-debian-image.tar.gz
|
||||
sha256sum release-readiness-debian-image.tar.gz > release-readiness-debian-image.sha256
|
||||
docker image inspect qinglong-readiness:debian > release-readiness-debian-image.json
|
||||
- uses: actions/upload-artifact@v6
|
||||
if: matrix.image == 'debian'
|
||||
with:
|
||||
name: release-readiness-debian-image
|
||||
path: release-readiness-debian-image.*
|
||||
path: |
|
||||
release-readiness-debian-image.tar.gz
|
||||
release-readiness-debian-image.sha256
|
||||
release-readiness-debian-image.json
|
||||
compression-level: 0
|
||||
retention-days: 3
|
||||
if-no-files-found: error
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
name: Build verified runtime tools
|
||||
|
||||
on:
|
||||
workflow_call:
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
tools:
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 20
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
with:
|
||||
ref: ${{ github.event.pull_request.head.sha || github.sha }}
|
||||
- uses: actions/setup-node@v6
|
||||
with:
|
||||
node-version: '22.23.3'
|
||||
package-manager-cache: false
|
||||
- name: Build the pinned universal Linux tool archive
|
||||
env:
|
||||
QL_TOOLS_SOURCE_COMMIT: ${{ github.event.pull_request.head.sha || github.sha }}
|
||||
run: bash scripts/build-runtime-tools.sh "$RUNNER_TEMP/qinglong-runtime-tools"
|
||||
- uses: actions/upload-artifact@v6
|
||||
with:
|
||||
name: qinglong-runtime-tools-${{ github.event.pull_request.head.sha || github.sha }}
|
||||
path: |
|
||||
${{ runner.temp }}/qinglong-runtime-tools/runtime-tools.tgz
|
||||
${{ runner.temp }}/qinglong-runtime-tools/runtime-tools-proof.json
|
||||
compression-level: 0
|
||||
if-no-files-found: error
|
||||
- name: Preserve build diagnostics
|
||||
if: always()
|
||||
uses: actions/upload-artifact@v6
|
||||
with:
|
||||
name: qinglong-runtime-tools-diagnostics-${{ github.event.pull_request.head.sha || github.sha }}
|
||||
path: ${{ runner.temp }}/qinglong-runtime-tools/logs/
|
||||
if-no-files-found: ignore
|
||||
@@ -11,21 +11,26 @@ permissions:
|
||||
jobs:
|
||||
regression:
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
node: ['22', '24']
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
- uses: pnpm/action-setup@v6
|
||||
with:
|
||||
version: '8.3.1'
|
||||
version: '10.34.6'
|
||||
- uses: actions/setup-node@v6
|
||||
with:
|
||||
node-version: '20'
|
||||
node-version: ${{ matrix.node }}
|
||||
cache: pnpm
|
||||
- run: pnpm install --frozen-lockfile
|
||||
- run: cp .env.example .env
|
||||
- run: pnpm test
|
||||
- run: pnpm build:back
|
||||
- run: pnpm build:front
|
||||
- run: pnpm benchmark:execution -- --runs=10 --output=execution-baseline.json
|
||||
- uses: actions/upload-artifact@v6
|
||||
with:
|
||||
name: execution-baseline
|
||||
name: execution-baseline-node-${{ matrix.node }}
|
||||
path: execution-baseline.json
|
||||
|
||||
@@ -1 +1,2 @@
|
||||
strict-peer-dependencies=false
|
||||
strict-peer-dependencies=false
|
||||
strict-dep-builds=true
|
||||
|
||||
+10
-4
@@ -62,10 +62,14 @@ docker run -d \
|
||||
|
||||
### npm
|
||||
|
||||
The npm version supports `debian/ubuntu/alpine` systems and requires `node/npm/python3/pip3/pnpm` to be installed.
|
||||
The npm deployment supports `debian/ubuntu/alpine` with Node.js 22.x >=22.22.2 or 24.x >=24.15.0, npm, Python 3 and pip. The 2.23.0 archive includes frozen production dependencies and verified runtime tools; a separate pnpm installation is unnecessary. After this version is published, use the command below. With npm 12, approve this exact package lifecycle in your project's `package.json` first:
|
||||
|
||||
```json
|
||||
{"allowScripts":{"@whyour/qinglong@2.23.0":true}}
|
||||
```
|
||||
|
||||
```bash
|
||||
npm i @whyour/qinglong
|
||||
npm i @whyour/qinglong@2.23.0
|
||||
```
|
||||
|
||||
## Deployment
|
||||
@@ -105,13 +109,15 @@ The package includes a [remote management skill](cli/skills/qinglong-cli/SKILL.m
|
||||
|
||||
## Development
|
||||
|
||||
Use Node.js 22.x starting at 22.22.2, or Node.js 24.x starting at 24.15.0.
|
||||
|
||||
```bash
|
||||
git clone https://github.com/whyour/qinglong.git
|
||||
cd qinglong
|
||||
cp .env.example .env
|
||||
# Recommended use pnpm https://pnpm.io/zh/installation
|
||||
npm install -g pnpm@8.3.1
|
||||
pnpm install
|
||||
npm install -g pnpm@10.34.6
|
||||
pnpm install --frozen-lockfile
|
||||
pnpm start
|
||||
```
|
||||
|
||||
|
||||
@@ -64,10 +64,14 @@ docker run -d \
|
||||
|
||||
### npm
|
||||
|
||||
npm 版本支持 `debian/ubuntu/alpine` 系统,需要自行安装 `node/npm/python3/pip3/pnpm`
|
||||
npm 版本支持 `debian/ubuntu/alpine` 系统,需要自行安装 Node.js 22.x >=22.22.2 或 24.x >=24.15.0,以及 npm、Python 3 和 pip。2.23.0 归档携带冻结生产依赖和受控工具,不需要单独安装 pnpm。该版本发布后使用以下命令;npm 12 的项目安装需要在当前项目 `package.json` 中精确批准安装脚本:
|
||||
|
||||
```json
|
||||
{"allowScripts":{"@whyour/qinglong@2.23.0":true}}
|
||||
```
|
||||
|
||||
```bash
|
||||
npm i @whyour/qinglong
|
||||
npm i @whyour/qinglong@2.23.0
|
||||
```
|
||||
|
||||
## 部署
|
||||
@@ -107,13 +111,15 @@ npm CLI 用于远程管理。面板内置 `ql` 同样使用这个命令名,已
|
||||
|
||||
## 开发
|
||||
|
||||
使用 Node.js 22.22.2 及以上的 22.x,或 Node.js 24.x >=24.15.0。
|
||||
|
||||
```bash
|
||||
git clone https://github.com/whyour/qinglong.git
|
||||
cd qinglong
|
||||
cp .env.example .env
|
||||
# 推荐使用 pnpm https://pnpm.io/zh/installation
|
||||
npm install -g pnpm@8.3.1
|
||||
pnpm install
|
||||
npm install -g pnpm@10.34.6
|
||||
pnpm install --frozen-lockfile
|
||||
pnpm start
|
||||
```
|
||||
|
||||
|
||||
+16
-4
@@ -5,7 +5,7 @@
|
||||
|
||||
The npm and panel-internal entries both use the name `ql`, but have separate Commander command trees. The npm entry only calls remote APIs; the internal entry only runs local tools. Verify the absolute executable path and `--help` before use. Installing the npm package does not migrate the built-in Shell commands.
|
||||
|
||||
`@whyour/qinglong-cli` is a standalone npm client for the panel's open API. It registers only `ql`, for the currently supported OpenAPI resources. Local execution, repo/raw workers, reload/update/account recovery belong to the panel's internal tools and are excluded from npm. Development publishing is outside both command sets.
|
||||
`@whyour/qinglong-cli` is a standalone npm client for the panel's open API. It registers only `ql`, for the currently supported OpenAPI resources. Local execution, repo/raw workers, reload/update/account recovery belong to the panel's internal tools and are excluded from npm. Remote CLI commands do not manage development publishing.
|
||||
|
||||
## Installation and commands
|
||||
|
||||
@@ -43,7 +43,7 @@ Use command `--help` and `QL_LANG=en` for English. Command names and JSON fields
|
||||
|
||||
## Complete OpenAPI management
|
||||
|
||||
The CLI also supports task/subscription CRUD, application and secret management, environment variables, configuration, scripts, logs, dependencies, system, dashboard and user APIs. `ql api routes --json` lists all 143 active routes; three retired file-reading endpoints are excluded. CI compares the catalogue against backend routes. See the [complete bilingual reference](skills/qinglong-cli/references/openapi.md) for every command and payload.
|
||||
The CLI also supports task/subscription CRUD, application and secret management, environment variables, configuration, scripts, logs, dependencies, system, dashboard and user APIs. `ql api routes --json` lists all 146 active routes; three retired file-reading endpoints are excluded. CI compares the catalogue against backend routes. See the [complete bilingual reference](skills/qinglong-cli/references/openapi.md) for every command and payload.
|
||||
|
||||
```sh
|
||||
ql task create --name demo --command 'task demo.js' --schedule '0 0 * * *' --json
|
||||
@@ -53,8 +53,20 @@ ql env create --data @envs.json --json
|
||||
ql api request PUT /open/crons/run --data '[12,13]' --json
|
||||
```
|
||||
|
||||
CLI 0.1.2 adds script history listing, detail and confirmed restore commands, requiring the history APIs in QingLong 2.23.0. Restore uses the current hash from the detail response; on 409, read and review the current state again.
|
||||
|
||||
```sh
|
||||
ql script history-list --query '{"filename":"demo.js","path":""}' --json
|
||||
ql script history-detail --query '{"filename":"demo.js","path":"","id":"VERSION_UUID"}' --json
|
||||
ql script history-restore --data '{"filename":"demo.js","path":"","id":"VERSION_UUID","expectedHash":"CURRENT_SHA256"}' --json
|
||||
```
|
||||
|
||||
Replace `VERSION_UUID` and `CURRENT_SHA256` with values returned by the list and detail commands.
|
||||
|
||||
Use --data JSON/@file/- for bodies, --query for query objects, --file for uploads and --output for downloads. New commands support --timeout seconds. Existing command contracts remain; raw API requests expose all fields and batch operations. Downloads do not overwrite files. App secrets require --show-secrets; other raw resources may contain sensitive data.
|
||||
|
||||
`--timeout` only limits waiting for the HTTP response; it does not stop server-side tasks. A failed write may already have taken effect, so inspect the resource before retrying.
|
||||
|
||||
Remote ql system commands call the target panel API. Local reload/reset tools remain excluded from npm.
|
||||
|
||||
## Authentication
|
||||
@@ -130,9 +142,9 @@ node cli/scripts/verify-package.cjs
|
||||
|
||||
Tests cover requests, token refresh, output, errors, no automatic retries, permissions and isolated installation. Package verification installs the archive offline, verifies the sole ql executable and rejects local commands.
|
||||
|
||||
The CLI package workflow checks, builds and tests Node 22.12/24 on relevant PRs, develop/master pushes and manual runs. Node 24 uploads the archive verified by offline installation as `qinglong-cli-<commit>`. After both matrix jobs succeed, master pushes publish that exact archive to npm as latest, using GitHub Actions OIDC trusted publishing without an NPM_TOKEN secret. Manual runs publish only when run on master with publish enabled; PRs, develop and forks never publish. Configure the npm Trusted Publisher for repository `whyour/qinglong`, workflow `cli-package.yml`, with `npm publish` allowed. The panel package `@whyour/qinglong` separately trusts `build-docker-image.yml`. Publishing uses Node 24 with `id-token: write` granted only to the publish job. New packages need an initial publication before configuring their trusted publisher.
|
||||
The CLI package workflow checks, builds and tests Node 22.12/24 on relevant PRs, develop/master pushes and manual runs. Node 24 uploads the archive verified by offline installation as `qinglong-cli-<commit>`. After both matrix jobs succeed, master pushes publish that exact archive to npm as latest, using GitHub Actions OIDC trusted publishing without an NPM_TOKEN secret. A manual release requires publish on master, or publish plus an explicit `X.Y.Z-alpha.N`, `X.Y.Z-beta.N` or `X.Y.Z-rc.N` version on develop. Develop prereleases set the same requested version before both Node verification jobs and publish to alpha/beta/rc without changing latest. Ordinary PRs, develop pushes, validation-only manual runs and forks do not publish. Releases and validation use separate concurrency groups; new validation does not cancel an active release. Configure the npm Trusted Publisher for repository `whyour/qinglong`, workflow `cli-package.yml`, with `npm publish` allowed. The panel package `@whyour/qinglong` separately trusts `build-docker-image.yml`. Publishing uses Node 24 with `id-token: write` granted only to the publish job. New packages need an initial publication before configuring their trusted publisher.
|
||||
|
||||
The CLI has an independent stable version in cli/package.json and cli/package-lock.json. Before releasing changes, run `npm version patch --prefix cli --no-git-tag-version` (or minor/major) and commit both files. Existing versions are skipped with a notice; registry failures stop publication. Only stable X.Y.Z versions are published by this workflow. Publication does not rebuild the verified archive or run package lifecycle scripts.
|
||||
The CLI has an independent stable version in cli/package.json and cli/package-lock.json. Before releasing changes, run `npm version patch --prefix cli --no-git-tag-version` (or minor/major) and commit both files. Existing stable versions are skipped with a notice; an existing prerelease is an error and requires a new version. Registry failures stop publication. Manual develop prereleases only update the version in that verification workspace and do not replace stable source metadata maintenance. Publication does not rebuild the verified archive or run package lifecycle scripts.
|
||||
|
||||
## Panel-internal tools
|
||||
|
||||
|
||||
+16
-4
@@ -5,7 +5,7 @@
|
||||
|
||||
npm 与面板内部入口都叫 `ql`,但使用独立的 Commander 命令树:npm 入口只调用远程 API,内部入口只运行本机工具。使用前确认可执行文件的绝对路径和 `--help`;安装 npm 包不会迁移内置 Shell 命令。
|
||||
|
||||
`@whyour/qinglong-cli` 是独立 npm 包,覆盖当前 develop 的有效 OpenAPI,只注册一个 `ql` 命令。它不包含脚本执行器或本机运维实现;`task exec`、`repo/raw`、`reload/update/reset*` 等属于面板内部工具,不随 npm 包分发。开发发布也不属于 CLI 范围。
|
||||
`@whyour/qinglong-cli` 是独立 npm 包,覆盖当前 develop 的有效 OpenAPI,只注册一个 `ql` 命令。它不包含脚本执行器或本机运维实现;`task exec`、`repo/raw`、`reload/update/reset*` 等属于面板内部工具,不随 npm 包分发。远程 CLI 命令不提供开发发布操作。
|
||||
|
||||
## 安装与使用
|
||||
|
||||
@@ -43,7 +43,7 @@ ql auth logout --json
|
||||
|
||||
## 全量 OpenAPI 管理
|
||||
|
||||
现在还支持任务/订阅创建、修改、删除,应用管理与密钥重置,以及环境变量、配置、脚本、日志、依赖、系统、仪表盘和用户管理。`ql api routes --json` 列出全部 143 条有效路由;3 条已下线文件读取接口不包含在内。新增命令在 [完整双语参考](skills/qinglong-cli/references/openapi.md) 中逐项列出,路由覆盖由 CI 与后端代码核对。
|
||||
现在还支持任务/订阅创建、修改、删除,应用管理与密钥重置,以及环境变量、配置、脚本、日志、依赖、系统、仪表盘和用户管理。`ql api routes --json` 列出全部 146 条有效路由;3 条已下线文件读取接口不包含在内。新增命令在 [完整双语参考](skills/qinglong-cli/references/openapi.md) 中逐项列出,路由覆盖由 CI 与后端代码核对。
|
||||
|
||||
```sh
|
||||
ql task create --name demo --command 'task demo.js' --schedule '0 0 * * *' --json
|
||||
@@ -53,8 +53,20 @@ ql env create --data @envs.json --json
|
||||
ql api request PUT /open/crons/run --data '[12,13]' --json
|
||||
```
|
||||
|
||||
CLI 0.1.2 增加脚本历史列表、详情和确认恢复命令,需要青龙 2.23.0 的历史 API。恢复使用详情返回的当前哈希,409 后应重新读取并确认。
|
||||
|
||||
```sh
|
||||
ql script history-list --query '{"filename":"demo.js","path":""}' --json
|
||||
ql script history-detail --query '{"filename":"demo.js","path":"","id":"VERSION_UUID"}' --json
|
||||
ql script history-restore --data '{"filename":"demo.js","path":"","id":"VERSION_UUID","expectedHash":"CURRENT_SHA256"}' --json
|
||||
```
|
||||
|
||||
将 `VERSION_UUID` 和 `CURRENT_SHA256` 替换为列表和详情实际返回值。
|
||||
|
||||
请求体使用 --data JSON/@file/-,查询使用 --query,上传 --file,下载 --output。新命令支持 --timeout 秒数;旧命令行为保留,完整参数可用 api request。下载不覆盖现有文件,应用密钥默认隐藏,明确加 --show-secrets 才输出。新增资源通常保留原始返回字段,注意环境、配置和会话信息可能敏感。
|
||||
|
||||
`--timeout` 只限制等待 HTTP 响应,不停止服务器上的任务;失败的写入可能已经生效,应先查询状态。
|
||||
|
||||
远程 `ql system ...` 调用面板 API;本机 reload/reset 等仍不在 npm 包中。不要将远程 API 覆盖理解为本机运维重新混入包。
|
||||
|
||||
## 认证
|
||||
@@ -130,9 +142,9 @@ node cli/scripts/verify-package.cjs
|
||||
|
||||
测试覆盖请求格式、认证刷新、输出、错误、禁止自动重试、权限和独立安装。打包验证会离线安装 tgz,并确认唯一入口是 `ql`,本机命令不可调用。
|
||||
|
||||
CLI package 工作流在相关 PR、develop/master 推送和手动触发时执行 Node 22.12/24 类型检查、构建及测试。Node 24 上传通过离线安装验证的 `qinglong-cli-<commit>` artifact。两个矩阵任务成功后,master 推送会将这份已验证的 tgz 发布到 npm 的 latest 标签,通过 GitHub Actions OIDC 可信发布,无需 `NPM_TOKEN` Secret。手动运行需选择 master 并勾选 publish;PR、develop 和 fork 不发布。npm 的 Trusted Publisher 需绑定仓库 `whyour/qinglong` 和工作流 `cli-package.yml`,允许 `npm publish`;面板包 `@whyour/qinglong` 单独绑定 `build-docker-image.yml`。发布 job 使用 Node 24,并仅在该 job 授予 `id-token: write`。新包需先完成首次发布,再配置对应包的可信发布关系。
|
||||
CLI package 工作流在相关 PR、develop/master 推送和手动触发时执行 Node 22.12/24 类型检查、构建及测试。Node 24 上传通过离线安装验证的 `qinglong-cli-<commit>` artifact。两个矩阵任务成功后,master 推送会将这份已验证的 tgz 发布到 npm 的 latest 标签,通过 GitHub Actions OIDC 可信发布,无需 `NPM_TOKEN` Secret。手动发布在 master 勾选 publish,或在 develop 勾选 publish 并填写 `X.Y.Z-alpha.N`、`X.Y.Z-beta.N` 或 `X.Y.Z-rc.N` 版本。develop 预发布在两个 Node 矩阵中先设置相同版本并验证,发布对应 alpha/beta/rc 标签,不更新 latest。普通 PR、develop 推送、仅验证的手动运行和 fork 不发布。发布与验证使用独立并发组,正在运行的发布不会被新验证取消。npm 的 Trusted Publisher 需绑定仓库 `whyour/qinglong` 和工作流 `cli-package.yml`,允许 `npm publish`;面板包 `@whyour/qinglong` 单独绑定 `build-docker-image.yml`。发布 job 使用 Node 24,并仅在该 job 授予 `id-token: write`。新包需先完成首次发布,再配置对应包的可信发布关系。
|
||||
|
||||
CLI 版本独立维护在 cli/package.json 和 cli/package-lock.json。发布改动前执行 `npm version patch --prefix cli --no-git-tag-version`(也可用 minor/major),提交这两个文件。已发布的版本会提示并跳过;registry 查询失败则停止发布。该流程仅发布 X.Y.Z 稳定版本,不重新构建产物或执行包生命周期脚本。
|
||||
CLI 版本独立维护在 cli/package.json 和 cli/package-lock.json。发布改动前执行 `npm version patch --prefix cli --no-git-tag-version`(也可用 minor/major),提交这两个文件。已存在的稳定版本提示并跳过;预发布版本已存在则失败,需选择新版本。registry 查询失败停止发布。手动 develop 预发布只临时修改本轮验证工作区的版本,不代替稳定版本元数据维护。发布不重新构建已验证归档,也不执行包生命周期脚本。
|
||||
|
||||
## 面板内部工具
|
||||
|
||||
|
||||
@@ -8,6 +8,7 @@ import { repairConfiguration, startPanel } from './operator';
|
||||
import { operatingSystem } from './bot';
|
||||
import { fail } from '../../shared/errors';
|
||||
import { registerHostServices } from '../runtime/hostServices';
|
||||
import { installRuntimeTools } from './tools';
|
||||
|
||||
export function bootstrapPackages(
|
||||
os: string,
|
||||
@@ -92,10 +93,13 @@ export function runtimeEnvironment(
|
||||
.filter(Boolean)
|
||||
.join(path.delimiter),
|
||||
NODE_PATH: [
|
||||
context.env.NODE_PATH,
|
||||
'/usr/local/bin',
|
||||
'/usr/local/lib/node_modules',
|
||||
path.join(node, 'global/5/node_modules'),
|
||||
].join(path.delimiter),
|
||||
]
|
||||
.filter(Boolean)
|
||||
.join(path.delimiter),
|
||||
PYTHONPATH: [
|
||||
python,
|
||||
path.join(python, `lib/python${pythonVersion}`),
|
||||
@@ -176,11 +180,7 @@ export async function bootstrapPanel(
|
||||
root ? args : [packages.program, ...args],
|
||||
{ cwd: context.root, env: context.env },
|
||||
);
|
||||
await checkedProcess(
|
||||
'npm',
|
||||
['install', '-g', 'pnpm@8.3.1', 'pm2', 'ts-node', 'typescript@5'],
|
||||
{ cwd: context.root, env: context.env },
|
||||
);
|
||||
await installRuntimeTools(context);
|
||||
}
|
||||
const python = await checkedProcess(
|
||||
'python3',
|
||||
|
||||
@@ -4,7 +4,7 @@ import path from 'node:path';
|
||||
import http from 'node:http';
|
||||
import type { LocalContext } from '../runtime/context';
|
||||
import { atomicWrite } from '../runtime/files';
|
||||
import { checkedProcess } from '../runtime/process';
|
||||
import { installRuntimeTools } from './tools';
|
||||
import {
|
||||
repairConfiguration,
|
||||
installPanelDependencies,
|
||||
@@ -152,11 +152,7 @@ export async function diagnosticLog(file: string): Promise<{
|
||||
}
|
||||
|
||||
export async function checkAndRepair(context: LocalContext): Promise<unknown> {
|
||||
await checkedProcess(
|
||||
'npm',
|
||||
['i', '-g', 'pnpm@8.3.1', 'pm2', 'ts-node', 'typescript@5'],
|
||||
{ cwd: context.root, env: context.env },
|
||||
);
|
||||
await installRuntimeTools(context);
|
||||
const restored = await repairConfiguration(context);
|
||||
await installPanelDependencies(context);
|
||||
const copied: string[] = [];
|
||||
|
||||
@@ -71,14 +71,18 @@ export async function executableAvailable(
|
||||
export async function installPanelDependencies(
|
||||
context: LocalContext,
|
||||
cwd = context.root,
|
||||
staticRoot?: string,
|
||||
sourceCommit?: string,
|
||||
): Promise<void> {
|
||||
const termux = context.env.is_termux === '1';
|
||||
const pnpm = !termux && (await executableAvailable('pnpm', context.env));
|
||||
await checkedProcess(
|
||||
pnpm ? 'pnpm' : 'npm',
|
||||
pnpm
|
||||
? ['install', '--loglevel', 'error', '--production']
|
||||
: ['install', '--production', ...(termux ? ['--no-bin-links'] : [])],
|
||||
process.execPath,
|
||||
[
|
||||
path.join(context.root, 'scripts/install-panel-dependencies.cjs'),
|
||||
'--root',
|
||||
cwd,
|
||||
...(staticRoot ? ['--static-dir', staticRoot] : []),
|
||||
...(sourceCommit ? ['--source-commit', sourceCommit] : []),
|
||||
],
|
||||
{ cwd, env: context.env },
|
||||
);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
import path from 'node:path';
|
||||
import type { LocalContext } from '../runtime/context';
|
||||
import { checkedProcess } from '../runtime/process';
|
||||
|
||||
// Preserve the installation's configured global prefix, without resolving new
|
||||
// registry packages or replacing the user's persisted script dependencies.
|
||||
export async function installRuntimeTools(context: LocalContext): Promise<void> {
|
||||
const options = { cwd: context.root, env: context.env };
|
||||
const result = await checkedProcess('npm', ['prefix', '--global'], {
|
||||
...options,
|
||||
capture: true,
|
||||
});
|
||||
const prefix = result.stdout.trim();
|
||||
if (!path.isAbsolute(prefix) || prefix.includes('\n'))
|
||||
throw new Error('npm returned an invalid global prefix.');
|
||||
await checkedProcess(
|
||||
process.execPath,
|
||||
[
|
||||
path.join(context.root, 'scripts/install-runtime-tools.cjs'),
|
||||
'--archive',
|
||||
path.join(context.root, 'static/runtime-tools.tgz'),
|
||||
'--proof',
|
||||
path.join(context.root, 'static/runtime-tools-proof.json'),
|
||||
'--prefix',
|
||||
prefix,
|
||||
],
|
||||
options,
|
||||
);
|
||||
context.env.PATH = [path.join(prefix, 'bin'), context.env.PATH]
|
||||
.filter(Boolean)
|
||||
.join(path.delimiter);
|
||||
context.env.NODE_PATH = [
|
||||
path.join(prefix, 'lib/node_modules'),
|
||||
context.env.NODE_PATH,
|
||||
]
|
||||
.filter(Boolean)
|
||||
.join(path.delimiter);
|
||||
}
|
||||
@@ -25,7 +25,7 @@ const artifacts = require('./upgradeArtifacts.cjs') as {
|
||||
mirror: 'github' | 'gitee';
|
||||
signal?: AbortSignal;
|
||||
run(program: string, args: string[], capture?: boolean): Promise<unknown>;
|
||||
install(source: string): Promise<void>;
|
||||
install(source: string, staticRoot: string, sourceCommit: string): Promise<void>;
|
||||
}): Promise<{ source: string; static: string }>;
|
||||
selectedUpgrade(
|
||||
tmp: string,
|
||||
@@ -78,7 +78,8 @@ export async function stageUpgrade(
|
||||
signal: operationSignal(),
|
||||
run: (program, args, capture) =>
|
||||
checkedProcess(program, args, { env: context.env, capture }),
|
||||
install: (source) => installPanelDependencies(context, source),
|
||||
install: (source, staticRoot, sourceCommit) =>
|
||||
installPanelDependencies(context, source, staticRoot, sourceCommit),
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -220,7 +220,7 @@ async function stageUpgrade({ root, tmp, env, mirror, run, install, signal }) {
|
||||
)
|
||||
dependenciesChanged = true;
|
||||
if (dependenciesChanged) {
|
||||
await install(source);
|
||||
await install(source, staticRoot, manifest.sourceCommit);
|
||||
await fs.access(path.join(source, 'node_modules'));
|
||||
await verifyUpgrade(source, staticRoot);
|
||||
}
|
||||
@@ -553,7 +553,13 @@ async function withLifecycleHooks(root, tmp, env, operation) {
|
||||
case "$1" in
|
||||
stop) delete_pm2 ;;
|
||||
start) reload_pm2 ;;
|
||||
install) npm_install_2 "$2"; exit "$exit_status" ;;
|
||||
install)
|
||||
exit_status=0
|
||||
npm_install_2 "$2" "$3" "$4"
|
||||
hook_install_status=$?
|
||||
[[ "$hook_install_status" -eq 0 ]] || exit "$hook_install_status"
|
||||
exit "$exit_status"
|
||||
;;
|
||||
*) printf '%s\\n' 'Unknown lifecycle operation' >&2; exit 2 ;;
|
||||
esac
|
||||
`,
|
||||
@@ -617,7 +623,8 @@ async function main() {
|
||||
mirror: process.argv[3],
|
||||
run,
|
||||
signal: controller.signal,
|
||||
install: (source) => hook('install', [source], controller.signal),
|
||||
install: (source, staticRoot, sourceCommit) =>
|
||||
hook('install', [source, staticRoot, sourceCommit], controller.signal),
|
||||
});
|
||||
console.log('更新包下载及校验成功...');
|
||||
if (process.argv[4] !== 'true') return;
|
||||
|
||||
@@ -46,6 +46,7 @@ async function fixture(t) {
|
||||
const fs=require('node:fs'),p=require('node:path');
|
||||
const program=p.basename(process.argv[1]),args=process.argv.slice(2);
|
||||
fs.appendFileSync(process.env.CALLS,JSON.stringify({program,args,python:process.env.PYTHON_HOME,node:process.env.PNPM_HOME})+'\\n');
|
||||
if(program==='npm' && args[0]==='prefix') process.stdout.write(p.join(p.dirname(process.env.CALLS),'global-prefix')+'\\n');
|
||||
if(program==='python3') process.stdout.write('3.12\\n');
|
||||
if(program==='nginx' && args.includes('-s')) process.exit(1);
|
||||
`;
|
||||
@@ -64,6 +65,11 @@ async function fixture(t) {
|
||||
`#!${process.execPath}\n${program}`,
|
||||
{ mode: 0o755 },
|
||||
);
|
||||
await fs.mkdir(path.join(root, 'scripts'));
|
||||
await fs.writeFile(
|
||||
path.join(root, 'scripts/install-runtime-tools.cjs'),
|
||||
program,
|
||||
);
|
||||
const hooks = [];
|
||||
const registrations = [];
|
||||
const system = {
|
||||
@@ -82,7 +88,7 @@ async function fixture(t) {
|
||||
}
|
||||
|
||||
test('bootstrap installs prerequisites, uses the configured data directory and starts services in order', async (t) => {
|
||||
const { ctx, system, hooks, registrations } = await fixture(t);
|
||||
const { ctx, system, hooks, base, registrations } = await fixture(t);
|
||||
await fs.writeFile(path.join(ctx.root, '.env'), 'KEEP=true');
|
||||
const result = await bootstrapPanel(ctx, false, system);
|
||||
assert.equal(result.mode, 'install');
|
||||
@@ -107,6 +113,7 @@ test('bootstrap installs prerequisites, uses the configured data directory and s
|
||||
process.getuid() === 0 ? 'apt-get' : 'sudo',
|
||||
process.getuid() === 0 ? 'apt-get' : 'sudo',
|
||||
'npm',
|
||||
'install-runtime-tools.cjs',
|
||||
'python3',
|
||||
'pip3',
|
||||
'pm2',
|
||||
@@ -118,14 +125,23 @@ test('bootstrap installs prerequisites, uses the configured data directory and s
|
||||
'pm2',
|
||||
],
|
||||
);
|
||||
assert.deepEqual(calls[4].args, [
|
||||
assert.deepEqual(calls[2].args, ['prefix', '--global']);
|
||||
assert.deepEqual(calls[3].args, [
|
||||
'--archive',
|
||||
path.join(ctx.root, 'static/runtime-tools.tgz'),
|
||||
'--proof',
|
||||
path.join(ctx.root, 'static/runtime-tools-proof.json'),
|
||||
'--prefix',
|
||||
path.join(base, 'global-prefix'),
|
||||
]);
|
||||
assert.deepEqual(calls[5].args, [
|
||||
'install',
|
||||
'--prefix',
|
||||
path.join(ctx.data, 'dep_cache/python3'),
|
||||
'requests',
|
||||
]);
|
||||
assert.deepEqual(calls[6].args, ['-c', system.nginxConfig, '-s', 'reload']);
|
||||
assert.deepEqual(calls[7].args, ['-c', system.nginxConfig]);
|
||||
assert.deepEqual(calls[7].args, ['-c', system.nginxConfig, '-s', 'reload']);
|
||||
assert.deepEqual(calls[8].args, ['-c', system.nginxConfig]);
|
||||
assert.deepEqual(
|
||||
calls.slice(-4).map((c) => c.args),
|
||||
[
|
||||
@@ -137,7 +153,7 @@ test('bootstrap installs prerequisites, uses the configured data directory and s
|
||||
);
|
||||
assert.ok(
|
||||
calls
|
||||
.slice(4)
|
||||
.slice(5)
|
||||
.every(
|
||||
(c) =>
|
||||
c.python === path.join(ctx.data, 'dep_cache/python3') &&
|
||||
@@ -240,7 +256,9 @@ for (const language of ['zh', 'en', 'unsupported']) {
|
||||
return true;
|
||||
},
|
||||
);
|
||||
const { runtimeEnvironment } = require('../../dist/internal/maintenance/bootstrap');
|
||||
const {
|
||||
runtimeEnvironment,
|
||||
} = require('../../dist/internal/maintenance/bootstrap');
|
||||
assert.throws(
|
||||
() => runtimeEnvironment(ctx, 'bad'),
|
||||
(error) => {
|
||||
@@ -255,3 +273,55 @@ for (const language of ['zh', 'en', 'unsupported']) {
|
||||
assert.deepEqual(await fs.readdir(root), []);
|
||||
});
|
||||
}
|
||||
|
||||
test('failed reviewed runtime installation stops before service registration and startup', async (t) => {
|
||||
const { ctx, system, hooks, registrations } = await fixture(t);
|
||||
await fs.writeFile(path.join(ctx.root, '.env'), 'KEEP=true');
|
||||
await fs.writeFile(
|
||||
path.join(ctx.root, 'scripts/install-runtime-tools.cjs'),
|
||||
'process.exit(19);',
|
||||
);
|
||||
await assert.rejects(bootstrapPanel(ctx, false, system), /exit 19|退出码 19/);
|
||||
assert.deepEqual(registrations, []);
|
||||
assert.deepEqual(hooks, []);
|
||||
assert.equal(
|
||||
await fs.readFile(path.join(ctx.root, '.env'), 'utf8'),
|
||||
'KEEP=true',
|
||||
);
|
||||
const calls = (await fs.readFile(ctx.env.CALLS, 'utf8'))
|
||||
.trim()
|
||||
.split('\n')
|
||||
.map(JSON.parse);
|
||||
assert.ok(
|
||||
calls.every(
|
||||
(call) => !['python3', 'pip3', 'nginx', 'pm2'].includes(call.program),
|
||||
),
|
||||
);
|
||||
});
|
||||
|
||||
test('runtime environment preserves the controlled tools prefix and persistent global dependencies', async (t) => {
|
||||
const { ctx, base } = await fixture(t);
|
||||
const {
|
||||
runtimeEnvironment,
|
||||
} = require('../../dist/internal/maintenance/bootstrap');
|
||||
const toolsModules = path.join(base, 'global-prefix/lib/node_modules');
|
||||
const customModules = path.join(base, 'operator-modules');
|
||||
const env = runtimeEnvironment(
|
||||
{
|
||||
...ctx,
|
||||
env: {
|
||||
...ctx.env,
|
||||
NODE_PATH: [toolsModules, customModules].join(path.delimiter),
|
||||
},
|
||||
},
|
||||
'3.12',
|
||||
);
|
||||
const modules = env.NODE_PATH.split(path.delimiter);
|
||||
assert.deepEqual(modules.slice(0, 2), [toolsModules, customModules]);
|
||||
assert.ok(modules.includes('/usr/local/lib/node_modules'));
|
||||
assert.ok(
|
||||
modules.includes(
|
||||
path.join(ctx.data, 'dep_cache/node/global/5/node_modules'),
|
||||
),
|
||||
);
|
||||
});
|
||||
|
||||
@@ -59,6 +59,24 @@ async function stub(context, program, body) {
|
||||
context.env.PATH = `${bin}:/usr/bin:/bin`;
|
||||
}
|
||||
|
||||
async function stubRuntimeTools(context, record) {
|
||||
const prefix = path.join(context.root, 'global-prefix');
|
||||
await stub(
|
||||
context,
|
||||
'npm',
|
||||
`${record}
|
||||
if(process.argv[2]==='prefix') process.stdout.write(${JSON.stringify(
|
||||
prefix,
|
||||
)}+'\\n');`,
|
||||
);
|
||||
await fs.mkdir(path.join(context.root, 'scripts'), { recursive: true });
|
||||
await fs.writeFile(
|
||||
path.join(context.root, 'scripts/install-runtime-tools.cjs'),
|
||||
record,
|
||||
);
|
||||
return prefix;
|
||||
}
|
||||
|
||||
test('configuration repair preserves custom content and intentionally empty hooks', async (t) => {
|
||||
const ctx = await fixture(t);
|
||||
await fs.mkdir(ctx.paths.dir_config, { recursive: true });
|
||||
@@ -75,29 +93,51 @@ test('configuration repair preserves custom content and intentionally empty hook
|
||||
assert.deepEqual(await repairConfiguration(ctx), [ctx.paths.file_notify_py]);
|
||||
});
|
||||
|
||||
test('dependency installation selects pnpm or Termux npm argv without shell interpolation', async (t) => {
|
||||
async function stubPanelDependencies(context, body) {
|
||||
await fs.mkdir(path.join(context.root, 'scripts'), { recursive: true });
|
||||
await fs.writeFile(
|
||||
path.join(context.root, 'scripts/install-panel-dependencies.cjs'),
|
||||
body,
|
||||
);
|
||||
}
|
||||
|
||||
test('dependency installation uses the installed helper and preserves root, static and commit arguments', async (t) => {
|
||||
const ctx = await fixture(t);
|
||||
const output = path.join(ctx.root, 'calls.jsonl');
|
||||
ctx.env.CALLS = output;
|
||||
ctx.env.CALLS = path.join(ctx.root, 'dependencies.jsonl');
|
||||
const record =
|
||||
'require("node:fs").appendFileSync(process.env.CALLS, JSON.stringify({args:process.argv.slice(2),cwd:process.cwd()})+"\\n");';
|
||||
await stub(ctx, 'pnpm', record);
|
||||
await stub(ctx, 'npm', record);
|
||||
'require("node:fs").appendFileSync(process.env.CALLS,JSON.stringify({args:process.argv.slice(2),cwd:process.cwd(),termux:process.env.is_termux||""})+"\\n");';
|
||||
await stubPanelDependencies(ctx, record);
|
||||
await installPanelDependencies(ctx);
|
||||
ctx.env.is_termux = '1';
|
||||
await installPanelDependencies(ctx);
|
||||
const calls = (await fs.readFile(output, 'utf8'))
|
||||
const source = path.join(ctx.root, 'source ;$(touch SHOULD_NOT_RUN)');
|
||||
const staticRoot = path.join(ctx.root, 'static ;$(touch SHOULD_NOT_RUN)');
|
||||
await fs.mkdir(source);
|
||||
await installPanelDependencies(ctx, source, staticRoot, sourceCommit);
|
||||
const calls = (await fs.readFile(ctx.env.CALLS, 'utf8'))
|
||||
.trim()
|
||||
.split('\n')
|
||||
.map(JSON.parse);
|
||||
assert.deepEqual(
|
||||
calls.map((c) => c.args),
|
||||
[
|
||||
['install', '--loglevel', 'error', '--production'],
|
||||
['install', '--production', '--no-bin-links'],
|
||||
],
|
||||
);
|
||||
assert.ok(calls.every((c) => c.cwd === ctx.root));
|
||||
assert.deepEqual(calls, [
|
||||
{ args: ['--root', ctx.root], cwd: ctx.root, termux: '0' },
|
||||
{ args: ['--root', ctx.root], cwd: ctx.root, termux: '1' },
|
||||
{
|
||||
args: [
|
||||
'--root',
|
||||
source,
|
||||
'--static-dir',
|
||||
staticRoot,
|
||||
'--source-commit',
|
||||
sourceCommit,
|
||||
],
|
||||
cwd: source,
|
||||
termux: '1',
|
||||
},
|
||||
]);
|
||||
for (const directory of [ctx.root, source])
|
||||
await assert.rejects(fs.stat(path.join(directory, 'SHOULD_NOT_RUN')), {
|
||||
code: 'ENOENT',
|
||||
});
|
||||
});
|
||||
|
||||
test('PM2 reload uses installation cwd and exact process-manager arguments', async (t) => {
|
||||
@@ -278,6 +318,49 @@ test('upgrade staging extracts both archives before publishing readiness and rej
|
||||
),
|
||||
{ ...staged, sourceCommit, dependenciesChanged: false },
|
||||
);
|
||||
await fs.writeFile(
|
||||
path.join(ctx.root, 'package.json'),
|
||||
'{"name":"older-installed"}',
|
||||
);
|
||||
ctx.env.CALLS = path.join(ctx.root, 'upgrade-dependencies.jsonl');
|
||||
await stubPanelDependencies(ctx, 'process.exit(23);');
|
||||
await assert.rejects(stageUpgrade(ctx, 'github'), /exit 23/);
|
||||
const ready = path.join(ctx.paths.dir_tmp, 'upgrade-ready-master.json');
|
||||
assert.deepEqual(JSON.parse(await fs.readFile(ready, 'utf8')), {
|
||||
directory: path.basename(path.dirname(staged.source)),
|
||||
});
|
||||
assert.equal(
|
||||
await fs.readFile(path.join(ctx.root, 'package.json'), 'utf8'),
|
||||
'{"name":"older-installed"}',
|
||||
);
|
||||
await stubPanelDependencies(
|
||||
ctx,
|
||||
'const fs=require("node:fs"),p=require("node:path"),args=process.argv.slice(2);fs.appendFileSync(process.env.CALLS,JSON.stringify({args,cwd:process.cwd()})+"\\n");fs.mkdirSync(p.join(args[args.indexOf("--root")+1],"node_modules"),{recursive:true});',
|
||||
);
|
||||
const changedStage = await stageUpgrade(ctx, 'github');
|
||||
assert.deepEqual(
|
||||
JSON.parse((await fs.readFile(ctx.env.CALLS, 'utf8')).trim()),
|
||||
{
|
||||
args: [
|
||||
'--root',
|
||||
changedStage.source,
|
||||
'--static-dir',
|
||||
changedStage.static,
|
||||
'--source-commit',
|
||||
sourceCommit,
|
||||
],
|
||||
cwd: changedStage.source,
|
||||
},
|
||||
);
|
||||
assert.equal(
|
||||
JSON.parse(
|
||||
await fs.readFile(
|
||||
path.join(path.dirname(changedStage.source), 'ready.json'),
|
||||
'utf8',
|
||||
),
|
||||
).dependenciesChanged,
|
||||
true,
|
||||
);
|
||||
await fs.symlink(
|
||||
'../../outside',
|
||||
path.join(payloads, `qinglong-${sourceCommit}/escape`),
|
||||
@@ -291,7 +374,7 @@ test('upgrade staging extracts both archives before publishing readiness and rej
|
||||
assert.deepEqual(
|
||||
(await fs.readdir(ctx.paths.dir_tmp)).sort(),
|
||||
[
|
||||
path.basename(path.dirname(staged.source)),
|
||||
path.basename(path.dirname(changedStage.source)),
|
||||
'upgrade-ready-master.json',
|
||||
].sort(),
|
||||
);
|
||||
@@ -336,8 +419,9 @@ test('check repairs dependencies and notifications, probes loopback and reloads
|
||||
ctx.env.HTTP_PROXY = 'http://127.0.0.1:1';
|
||||
const record =
|
||||
'require("node:fs").appendFileSync(process.env.CALLS,JSON.stringify({program:require("node:path").basename(process.argv[1]),args:process.argv.slice(2)})+"\\n");';
|
||||
for (const executable of ['npm', 'pnpm', 'pm2'])
|
||||
await stub(ctx, executable, record);
|
||||
for (const executable of ['pnpm', 'pm2']) await stub(ctx, executable, record);
|
||||
const toolsPrefix = await stubRuntimeTools(ctx, record);
|
||||
await stubPanelDependencies(ctx, record);
|
||||
await repairConfiguration(ctx);
|
||||
await fs.writeFile(ctx.paths.file_notify_py, 'custom old notify');
|
||||
await fs.mkdir(path.join(ctx.env.PM2_HOME, 'logs'), { recursive: true });
|
||||
@@ -364,11 +448,22 @@ test('check repairs dependencies and notifications, probes loopback and reloads
|
||||
assert.deepEqual(calls, [
|
||||
{
|
||||
program: 'npm',
|
||||
args: ['i', '-g', 'pnpm@8.3.1', 'pm2', 'ts-node', 'typescript@5'],
|
||||
args: ['prefix', '--global'],
|
||||
},
|
||||
{
|
||||
program: 'pnpm',
|
||||
args: ['install', '--loglevel', 'error', '--production'],
|
||||
program: 'install-runtime-tools.cjs',
|
||||
args: [
|
||||
'--archive',
|
||||
path.join(ctx.root, 'static/runtime-tools.tgz'),
|
||||
'--proof',
|
||||
path.join(ctx.root, 'static/runtime-tools-proof.json'),
|
||||
'--prefix',
|
||||
toolsPrefix,
|
||||
],
|
||||
},
|
||||
{
|
||||
program: 'install-panel-dependencies.cjs',
|
||||
args: ['--root', ctx.root],
|
||||
},
|
||||
{ program: 'pm2', args: ['flush'] },
|
||||
{
|
||||
@@ -437,6 +532,28 @@ test('repair stops after installer failure before replacing user files or reload
|
||||
});
|
||||
});
|
||||
|
||||
test('dependency helper failure preserves notifications and prevents a service reload', async (t) => {
|
||||
const { checkAndRepair } = require('../../dist/internal/maintenance/check');
|
||||
const ctx = await fixture(t);
|
||||
await repairConfiguration(ctx);
|
||||
await fs.writeFile(ctx.paths.file_notify_js, 'keep installed notification');
|
||||
await stubRuntimeTools(ctx, '');
|
||||
await stubPanelDependencies(ctx, 'process.exit(19);');
|
||||
await stub(
|
||||
ctx,
|
||||
'pm2',
|
||||
'require("node:fs").writeFileSync(process.env.QL_DIR+"/unexpected-reload", "bad");',
|
||||
);
|
||||
await assert.rejects(checkAndRepair(ctx), /exit 19/);
|
||||
assert.equal(
|
||||
await fs.readFile(ctx.paths.file_notify_js, 'utf8'),
|
||||
'keep installed notification',
|
||||
);
|
||||
await assert.rejects(fs.stat(path.join(ctx.root, 'unexpected-reload')), {
|
||||
code: 'ENOENT',
|
||||
});
|
||||
});
|
||||
|
||||
test('legacy check workflow and TypeScript repair agree on dependency and notification operations', async (t) => {
|
||||
const { execFileSync } = require('node:child_process');
|
||||
const ctx = await fixture(t);
|
||||
@@ -444,7 +561,8 @@ test('legacy check workflow and TypeScript repair agree on dependency and notifi
|
||||
const trace = path.join(ctx.root, 'legacy-trace');
|
||||
const shell = `
|
||||
t() { :; }
|
||||
npm() { printf 'npm %s\\n' "$*" >> "$TRACE"; }
|
||||
npm() { printf 'npm %s\\n' "$*" >> "$TRACE"; printf '%s\\n' "$dir_root/global-prefix"; }
|
||||
node() { printf 'tools %s\\n' "$*" >> "$TRACE"; }
|
||||
fix_config() { printf 'repair-config\\n' >> "$TRACE"; }
|
||||
npm_install_2() { printf 'dependencies %s\\n' "$1" >> "$TRACE"; }
|
||||
cp() { printf 'copy %s\\n' "$*" >> "$TRACE"; command cp "$@"; }
|
||||
@@ -468,7 +586,8 @@ test('legacy check workflow and TypeScript repair agree on dependency and notifi
|
||||
});
|
||||
const operations = (await fs.readFile(trace, 'utf8')).trim().split('\n');
|
||||
assert.deepEqual(operations, [
|
||||
'npm i -g pnpm@8.3.1 pm2 ts-node typescript@5',
|
||||
'npm prefix --global',
|
||||
`tools ${ctx.root}/scripts/install-runtime-tools.cjs --archive ${ctx.root}/static/runtime-tools.tgz --proof ${ctx.root}/static/runtime-tools-proof.json --prefix ${ctx.root}/global-prefix`,
|
||||
'repair-config',
|
||||
`dependencies ${ctx.root}`,
|
||||
`copy -fv ${ctx.paths.file_notify_py_sample} ${ctx.paths.file_notify_py}`,
|
||||
@@ -485,6 +604,91 @@ test('legacy check workflow and TypeScript repair agree on dependency and notifi
|
||||
);
|
||||
});
|
||||
|
||||
test('legacy check aborts dependency failure before copying notifications or reloading', async (t) => {
|
||||
const { execFileSync } = require('node:child_process');
|
||||
const ctx = await fixture(t);
|
||||
await repairConfiguration(ctx);
|
||||
await fs.writeFile(ctx.paths.file_notify_js, 'keep legacy notification');
|
||||
const shell = `
|
||||
t() { :; }
|
||||
npm() { printf '%s\\n' "$dir_root/global-prefix"; }
|
||||
node() { :; }
|
||||
fix_config() { :; }
|
||||
npm_install_2() { return 19; }
|
||||
reload_pm2() { printf unexpected > "$dir_root/unexpected-reload"; }
|
||||
curl() { printf '<div id="root"></div>{"code":200,"status":"ok"}'; }
|
||||
. "$CHECK_SOURCE"
|
||||
`;
|
||||
assert.throws(
|
||||
() =>
|
||||
execFileSync('/bin/bash', ['--noprofile', '--norc', '-c', shell], {
|
||||
env: {
|
||||
...ctx.env,
|
||||
CHECK_SOURCE: path.resolve(__dirname, '../../../shell/check.sh'),
|
||||
},
|
||||
stdio: ['ignore', 'pipe', 'pipe'],
|
||||
}),
|
||||
(error) => error.status === 19,
|
||||
);
|
||||
assert.equal(
|
||||
await fs.readFile(ctx.paths.file_notify_js, 'utf8'),
|
||||
'keep legacy notification',
|
||||
);
|
||||
await assert.rejects(fs.stat(path.join(ctx.root, 'unexpected-reload')), {
|
||||
code: 'ENOENT',
|
||||
});
|
||||
});
|
||||
|
||||
test('installed upgrade hooks propagate returned failures and preserve legacy installer status', async (t) => {
|
||||
const {
|
||||
withLifecycleHooks,
|
||||
} = require('../../dist/internal/maintenance/upgradeArtifacts.cjs');
|
||||
for (const [returned, legacy, expected] of [
|
||||
[19, 0, 19],
|
||||
[0, 23, 23],
|
||||
[0, 0, 0],
|
||||
]) {
|
||||
const ctx = await fixture(t);
|
||||
const tmp = path.join(ctx.root, 'tmp');
|
||||
await fs.mkdir(tmp);
|
||||
await fs.mkdir(path.join(ctx.root, 'shell'));
|
||||
const trace = path.join(ctx.root, 'install-args');
|
||||
await fs.writeFile(path.join(ctx.root, 'shell/env.sh'), '');
|
||||
await fs.writeFile(
|
||||
path.join(ctx.root, 'shell/share.sh'),
|
||||
`import_config() { :; }
|
||||
npm_install_2() {
|
||||
printf '%s\\n' "$@" > "$QL_INSTALL_TRACE"
|
||||
exit_status=${legacy}
|
||||
return ${returned}
|
||||
}
|
||||
`,
|
||||
);
|
||||
const args = [
|
||||
path.join(ctx.root, 'staged source;$(touch SHOULD_NOT_RUN)'),
|
||||
path.join(ctx.root, 'staged static;$(touch SHOULD_NOT_RUN)'),
|
||||
sourceCommit,
|
||||
];
|
||||
const operation = withLifecycleHooks(
|
||||
ctx.root,
|
||||
tmp,
|
||||
{ ...ctx.env, QL_INSTALL_TRACE: trace },
|
||||
(hook) => hook('install', args),
|
||||
);
|
||||
if (expected)
|
||||
await assert.rejects(operation, new RegExp(`exit ${expected}`));
|
||||
else await operation;
|
||||
assert.deepEqual(
|
||||
(await fs.readFile(trace, 'utf8')).trim().split('\n'),
|
||||
args,
|
||||
);
|
||||
await assert.rejects(fs.stat(path.join(ctx.root, 'SHOULD_NOT_RUN')), {
|
||||
code: 'ENOENT',
|
||||
});
|
||||
assert.deepEqual(await fs.readdir(tmp), []);
|
||||
}
|
||||
});
|
||||
|
||||
test('interrupted reload restores old files and recovery subprocesses ignore the cancelled operation', async (t) => {
|
||||
const {
|
||||
cancellableOperation,
|
||||
|
||||
@@ -2,7 +2,9 @@ const assert = require('node:assert/strict');
|
||||
const fs = require('node:fs/promises');
|
||||
const { randomUUID } = require('node:crypto');
|
||||
const { createContext } = require('../../dist/internal/runtime/context');
|
||||
const { installPanelDependencies } = require('../../dist/internal/maintenance/operator');
|
||||
const {
|
||||
installPanelDependencies,
|
||||
} = require('../../dist/internal/maintenance/operator');
|
||||
const { reloadPanel } = require('../../dist/internal/maintenance/upgrade');
|
||||
assert.equal(process.env.QL_PANEL_INTEGRATION, '1');
|
||||
(async () => {
|
||||
@@ -65,7 +67,16 @@ assert.equal(process.env.QL_PANEL_INTEGRATION, '1');
|
||||
const config = await fs.readFile('/ql/data/config/config.sh');
|
||||
const dotenv = await fs.readFile('/ql/.env');
|
||||
const context = createContext({ root: '/ql' }, process.env);
|
||||
await installPanelDependencies(context, '/stage/source');
|
||||
const stagedManifest = JSON.parse(
|
||||
await fs.readFile('/stage/static/build-info.json', 'utf8'),
|
||||
);
|
||||
assert.match(stagedManifest.sourceCommit, /^[0-9a-f]{40}$/);
|
||||
await installPanelDependencies(
|
||||
context,
|
||||
'/stage/source',
|
||||
'/stage/static',
|
||||
stagedManifest.sourceCommit,
|
||||
);
|
||||
const replacement = await reloadPanel(context, 'system', {
|
||||
source: '/stage/source',
|
||||
static: '/stage/static',
|
||||
|
||||
+6
-5
@@ -1,15 +1,17 @@
|
||||
# Run Node package installation natively on the builder. Node/npm can spin at
|
||||
# 100% CPU when Alpine s390x is emulated through QEMU.
|
||||
FROM --platform=$BUILDPLATFORM node:18-alpine3.18 AS builder
|
||||
FROM --platform=$BUILDPLATFORM node:22.23.3-alpine3.24 AS builder
|
||||
|
||||
ARG TARGETARCH
|
||||
ENV NPM_CONFIG_PREFIX=/opt/node-global
|
||||
ENV PATH=/opt/node-global/bin:${PATH}
|
||||
|
||||
COPY package.json .npmrc pnpm-lock.yaml /tmp/build/
|
||||
COPY static/runtime-tools.tgz static/runtime-tools-proof.json /tmp/runtime-tools/
|
||||
COPY scripts/install-runtime-tools.cjs /tmp/install-runtime-tools.cjs
|
||||
RUN set -x \
|
||||
&& apk add --no-cache git \
|
||||
&& npm i -g pnpm@8.3.1 pm2 ts-node typescript@5 \
|
||||
&& node /tmp/install-runtime-tools.cjs --archive /tmp/runtime-tools/runtime-tools.tgz --proof /tmp/runtime-tools/runtime-tools-proof.json --prefix /opt/node-global \
|
||||
&& cd /tmp/build \
|
||||
&& case "${TARGETARCH}" in \
|
||||
amd64) NODE_ARCH=x64 ;; \
|
||||
@@ -22,7 +24,7 @@ RUN set -x \
|
||||
npm_config_target_libc=musl \
|
||||
pnpm install --prod --frozen-lockfile
|
||||
|
||||
FROM python:3.11-alpine
|
||||
FROM python:3.11.17-alpine3.24@sha256:d9368b3a5ac59afea7b5d4f2e2aea0941dbf9fdee9c369c5bec00b98244bc929
|
||||
|
||||
ARG QL_MAINTAINER="whyour"
|
||||
LABEL maintainer="${QL_MAINTAINER}"
|
||||
@@ -41,7 +43,7 @@ VOLUME /ql/data
|
||||
|
||||
EXPOSE 5700
|
||||
|
||||
COPY --from=builder /opt/node-global/lib/node_modules/. /usr/local/lib/node_modules/
|
||||
COPY --from=builder /opt/node-global/lib/. /usr/local/lib/
|
||||
COPY --from=builder /opt/node-global/bin/. /usr/local/bin/
|
||||
|
||||
RUN set -x \
|
||||
@@ -61,7 +63,6 @@ RUN set -x \
|
||||
procps \
|
||||
netcat-openbsd \
|
||||
unzip \
|
||||
npm \
|
||||
&& rm -rf /var/cache/apk/* \
|
||||
&& apk update \
|
||||
&& ln -sf /usr/share/zoneinfo/Asia/Shanghai /etc/localtime \
|
||||
|
||||
@@ -1,15 +1,17 @@
|
||||
# Run Node package installation natively on the builder. Node/npm can spin at
|
||||
# 100% CPU when Alpine s390x is emulated through QEMU.
|
||||
FROM --platform=$BUILDPLATFORM node:18-alpine3.18 AS builder
|
||||
FROM --platform=$BUILDPLATFORM node:22.23.3-alpine3.24 AS builder
|
||||
|
||||
ARG TARGETARCH
|
||||
ENV NPM_CONFIG_PREFIX=/opt/node-global
|
||||
ENV PATH=/opt/node-global/bin:${PATH}
|
||||
|
||||
COPY package.json .npmrc pnpm-lock.yaml /tmp/build/
|
||||
COPY static/runtime-tools.tgz static/runtime-tools-proof.json /tmp/runtime-tools/
|
||||
COPY scripts/install-runtime-tools.cjs /tmp/install-runtime-tools.cjs
|
||||
RUN set -x \
|
||||
&& apk add --no-cache git \
|
||||
&& npm i -g pnpm@8.3.1 pm2 ts-node typescript@5 \
|
||||
&& node /tmp/install-runtime-tools.cjs --archive /tmp/runtime-tools/runtime-tools.tgz --proof /tmp/runtime-tools/runtime-tools-proof.json --prefix /opt/node-global \
|
||||
&& cd /tmp/build \
|
||||
&& case "${TARGETARCH}" in \
|
||||
amd64) NODE_ARCH=x64 ;; \
|
||||
@@ -41,7 +43,7 @@ VOLUME /ql/data
|
||||
|
||||
EXPOSE 5700
|
||||
|
||||
COPY --from=builder /opt/node-global/lib/node_modules/. /usr/local/lib/node_modules/
|
||||
COPY --from=builder /opt/node-global/lib/. /usr/local/lib/
|
||||
COPY --from=builder /opt/node-global/bin/. /usr/local/bin/
|
||||
|
||||
RUN set -x \
|
||||
@@ -61,7 +63,6 @@ RUN set -x \
|
||||
procps \
|
||||
netcat-openbsd \
|
||||
unzip \
|
||||
npm \
|
||||
&& rm -rf /var/cache/apk/* \
|
||||
&& apk update \
|
||||
&& ln -sf /usr/share/zoneinfo/Asia/Shanghai /etc/localtime \
|
||||
|
||||
+19
-11
@@ -1,20 +1,26 @@
|
||||
# Node 20 Bookworm is the latest official Node image variant that covers the
|
||||
# full Debian build matrix, including arm/v7, ppc64le, and s390x.
|
||||
FROM node:20-bookworm-slim AS nodebuilder
|
||||
# Download authenticated target binaries natively; Node 22 Bookworm images do
|
||||
# not include s390x, while official Node tarballs retain all five targets.
|
||||
FROM --platform=$BUILDPLATFORM node:22.23.3-bookworm-slim AS nodebuilder
|
||||
ARG TARGETARCH
|
||||
ARG TARGETVARIANT
|
||||
COPY docker/install-node.sh docker/node-release-key.asc docker/node-v22.23.3-SHASUMS256.txt.asc /tmp/
|
||||
RUN apt-get update && apt-get install --no-install-recommends -y ca-certificates curl gnupg xz-utils \
|
||||
&& sh /tmp/install-node.sh
|
||||
|
||||
FROM python:3.11.14-slim-bookworm AS builder
|
||||
FROM python:3.11.17-slim-trixie@sha256:e88e9763f943ec1834f992a4b51e0f24500486803e8bc534e5767af9ea65f6ce AS builder
|
||||
COPY package.json .npmrc pnpm-lock.yaml /tmp/build/
|
||||
COPY --from=nodebuilder /usr/local/bin/node /usr/local/bin/
|
||||
COPY --from=nodebuilder /usr/local/lib/node_modules/. /usr/local/lib/node_modules/
|
||||
COPY static/runtime-tools.tgz static/runtime-tools-proof.json /tmp/runtime-tools/
|
||||
COPY scripts/install-runtime-tools.cjs /tmp/install-runtime-tools.cjs
|
||||
COPY --from=nodebuilder /opt/node-target/bin/node /usr/local/bin/
|
||||
RUN set -x && \
|
||||
ln -s /usr/local/lib/node_modules/npm/bin/npm-cli.js /usr/local/bin/npm && \
|
||||
apt-get update && \
|
||||
apt-get install --no-install-recommends -y libatomic1 && \
|
||||
npm i -g pnpm@8.3.1 && \
|
||||
node /tmp/install-runtime-tools.cjs --archive /tmp/runtime-tools/runtime-tools.tgz --proof /tmp/runtime-tools/runtime-tools-proof.json --prefix /usr/local && \
|
||||
cd /tmp/build && \
|
||||
pnpm install --prod --frozen-lockfile
|
||||
|
||||
FROM python:3.11.14-slim-bookworm
|
||||
FROM python:3.11.17-slim-trixie@sha256:e88e9763f943ec1834f992a4b51e0f24500486803e8bc534e5767af9ea65f6ce
|
||||
|
||||
ARG QL_MAINTAINER="whyour"
|
||||
LABEL maintainer="${QL_MAINTAINER}"
|
||||
@@ -42,8 +48,9 @@ RUN groupadd -g ${QL_GID} qinglong && \
|
||||
ENV QL_USER=qinglong
|
||||
ENV QL_HOME=/home/$QL_USER
|
||||
|
||||
COPY --from=nodebuilder /usr/local/bin/node /usr/local/bin/
|
||||
COPY --from=nodebuilder /usr/local/lib/node_modules/. /usr/local/lib/node_modules/
|
||||
COPY --from=nodebuilder /opt/node-target/bin/node /usr/local/bin/
|
||||
COPY static/runtime-tools.tgz static/runtime-tools-proof.json /tmp/runtime-tools/
|
||||
COPY scripts/install-runtime-tools.cjs /tmp/install-runtime-tools.cjs
|
||||
|
||||
RUN set -x && \
|
||||
ln -s /usr/local/lib/node_modules/npm/bin/npm-cli.js /usr/local/bin/npm && \
|
||||
@@ -69,7 +76,8 @@ RUN set -x && \
|
||||
git config --global user.email "qinglong@users.noreply.github.com" && \
|
||||
git config --global user.name "qinglong" && \
|
||||
git config --global http.postBuffer 524288000 && \
|
||||
npm install -g pnpm@8.3.1 pm2 ts-node typescript@5 && \
|
||||
node /tmp/install-runtime-tools.cjs --archive /tmp/runtime-tools/runtime-tools.tgz --proof /tmp/runtime-tools/runtime-tools-proof.json --prefix /usr/local && \
|
||||
rm -rf /tmp/runtime-tools /tmp/install-runtime-tools.cjs && \
|
||||
npm cache clean --force && \
|
||||
rm -rf /root/.cache && \
|
||||
rm -rf /root/.npm && \
|
||||
|
||||
@@ -1,18 +1,24 @@
|
||||
# Node 20 Bookworm is the latest official Node image variant that covers the
|
||||
# full Debian build matrix, including arm/v7, ppc64le, and s390x.
|
||||
FROM node:20-bookworm-slim AS nodebuilder
|
||||
# Download authenticated target binaries natively; Node 22 Bookworm images do
|
||||
# not include s390x, while official Node tarballs retain all five targets.
|
||||
FROM --platform=$BUILDPLATFORM node:22.23.3-bookworm-slim AS nodebuilder
|
||||
ARG TARGETARCH
|
||||
ARG TARGETVARIANT
|
||||
COPY docker/install-node.sh docker/node-release-key.asc docker/node-v22.23.3-SHASUMS256.txt.asc /tmp/
|
||||
RUN apt-get update && apt-get install --no-install-recommends -y ca-certificates curl gnupg xz-utils \
|
||||
&& sh /tmp/install-node.sh
|
||||
|
||||
# Keep the full release matrix, including s390x, on the same immutable base.
|
||||
# The floating 3.10-slim-bookworm tag no longer includes s390x.
|
||||
FROM python:3.10.19-slim-bookworm@sha256:23f63358922e79a794f71be8f3723c84e5ccca9638af3f74456dc73d6184499e AS builder
|
||||
COPY package.json .npmrc pnpm-lock.yaml /tmp/build/
|
||||
COPY --from=nodebuilder /usr/local/bin/node /usr/local/bin/
|
||||
COPY --from=nodebuilder /usr/local/lib/node_modules/. /usr/local/lib/node_modules/
|
||||
COPY static/runtime-tools.tgz static/runtime-tools-proof.json /tmp/runtime-tools/
|
||||
COPY scripts/install-runtime-tools.cjs /tmp/install-runtime-tools.cjs
|
||||
COPY --from=nodebuilder /opt/node-target/bin/node /usr/local/bin/
|
||||
RUN set -x && \
|
||||
ln -s /usr/local/lib/node_modules/npm/bin/npm-cli.js /usr/local/bin/npm && \
|
||||
apt-get update && \
|
||||
apt-get install --no-install-recommends -y libatomic1 && \
|
||||
npm i -g pnpm@8.3.1 && \
|
||||
node /tmp/install-runtime-tools.cjs --archive /tmp/runtime-tools/runtime-tools.tgz --proof /tmp/runtime-tools/runtime-tools-proof.json --prefix /usr/local && \
|
||||
cd /tmp/build && \
|
||||
pnpm install --prod --frozen-lockfile
|
||||
|
||||
@@ -44,8 +50,9 @@ RUN groupadd -g ${QL_GID} qinglong && \
|
||||
ENV QL_USER=qinglong
|
||||
ENV QL_HOME=/home/$QL_USER
|
||||
|
||||
COPY --from=nodebuilder /usr/local/bin/node /usr/local/bin/
|
||||
COPY --from=nodebuilder /usr/local/lib/node_modules/. /usr/local/lib/node_modules/
|
||||
COPY --from=nodebuilder /opt/node-target/bin/node /usr/local/bin/
|
||||
COPY static/runtime-tools.tgz static/runtime-tools-proof.json /tmp/runtime-tools/
|
||||
COPY scripts/install-runtime-tools.cjs /tmp/install-runtime-tools.cjs
|
||||
|
||||
RUN set -x && \
|
||||
ln -s /usr/local/lib/node_modules/npm/bin/npm-cli.js /usr/local/bin/npm && \
|
||||
@@ -70,7 +77,8 @@ RUN set -x && \
|
||||
git config --global user.email "qinglong@users.noreply.github.com" && \
|
||||
git config --global user.name "qinglong" && \
|
||||
git config --global http.postBuffer 524288000 && \
|
||||
npm install -g pnpm@8.3.1 pm2 ts-node typescript@5 && \
|
||||
node /tmp/install-runtime-tools.cjs --archive /tmp/runtime-tools/runtime-tools.tgz --proof /tmp/runtime-tools/runtime-tools-proof.json --prefix /usr/local && \
|
||||
rm -rf /tmp/runtime-tools /tmp/install-runtime-tools.cjs && \
|
||||
npm cache clean --force && \
|
||||
rm -rf /root/.cache && \
|
||||
rm -rf /root/.npm && \
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
#!/bin/sh
|
||||
set -eu
|
||||
|
||||
# These release files are committed together so every target architecture uses
|
||||
# the same official Node release and authenticated checksum list.
|
||||
NODE_VERSION=22.23.3
|
||||
NODE_SIGNER=5BE8A3F6C8A5C01D106C0AD820B1A390B168D356
|
||||
case "${TARGETARCH:?}/${TARGETVARIANT:-}" in
|
||||
amd64/) NODE_ARCH=x64 ;;
|
||||
arm/v7) NODE_ARCH=armv7l ;;
|
||||
arm64/|arm64/v8) NODE_ARCH=arm64 ;;
|
||||
ppc64le/) NODE_ARCH=ppc64le ;;
|
||||
s390x/) NODE_ARCH=s390x ;;
|
||||
*) printf 'Unsupported Debian Node architecture: %s/%s\n' "$TARGETARCH" "${TARGETVARIANT:-}" >&2; exit 1 ;;
|
||||
esac
|
||||
|
||||
DOWNLOAD_DIR=$(mktemp -d)
|
||||
KEY_DIR=$(mktemp -d)
|
||||
trap 'rm -rf "$DOWNLOAD_DIR" "$KEY_DIR"' EXIT HUP INT TERM
|
||||
chmod 700 "$KEY_DIR"
|
||||
gpg --homedir "$KEY_DIR" --batch --import /tmp/node-release-key.asc
|
||||
FINGERPRINT=$(gpg --homedir "$KEY_DIR" --batch --with-colons --fingerprint "$NODE_SIGNER" | awk -F: '$1 == "fpr" { print $10; exit }')
|
||||
test "$FINGERPRINT" = "$NODE_SIGNER"
|
||||
gpg --homedir "$KEY_DIR" --batch --status-fd 3 --output "$DOWNLOAD_DIR/SHASUMS256.txt" \
|
||||
--decrypt "/tmp/node-v${NODE_VERSION}-SHASUMS256.txt.asc" 3> "$DOWNLOAD_DIR/signature.status"
|
||||
grep -q "^\[GNUPG:\] VALIDSIG ${NODE_SIGNER} " "$DOWNLOAD_DIR/signature.status"
|
||||
|
||||
NODE_FILE="node-v${NODE_VERSION}-linux-${NODE_ARCH}.tar.xz"
|
||||
cd "$DOWNLOAD_DIR"
|
||||
grep " ${NODE_FILE}$" SHASUMS256.txt > expected.sha256
|
||||
test "$(wc -l < expected.sha256 | tr -d ' ')" = 1
|
||||
curl --fail --show-error --silent --location --proto '=https' --proto-redir '=https' --tlsv1.2 --retry 3 \
|
||||
"https://nodejs.org/dist/v${NODE_VERSION}/${NODE_FILE}" --output "$NODE_FILE"
|
||||
sha256sum --check --strict expected.sha256
|
||||
mkdir -p /opt/node-target
|
||||
tar --extract --xz --file "$NODE_FILE" --directory /opt/node-target --strip-components=1
|
||||
test -x /opt/node-target/bin/node
|
||||
test -f /opt/node-target/lib/node_modules/npm/package.json
|
||||
@@ -0,0 +1,17 @@
|
||||
-----BEGIN PGP PUBLIC KEY BLOCK-----
|
||||
|
||||
mDMEaGA63BYJKwYBBAHaRw8BAQdAo/yU+MutacFmmn0CEX495goNrBxR24235XLM
|
||||
cvHYjfq0L0FudG9pbmUgZHUgSGFtZWwgPGR1aGFtZWxhbnRvaW5lMTk5NUBnbWFp
|
||||
bC5jb20+iI4EExYKADYWIQRb6KP2yKXAHRBsCtggsaOQsWjTVgUCaGA63AIbAwQL
|
||||
CQgHBBUKCQgFFgIDAQACHgUCF4AACgkQILGjkLFo01afgwEA/sLHqsj7ml2vyDoT
|
||||
KDPE8n9a80ZOh14OfnlOe0cCZA8BAMEOOk7QFI69DIlV1nMiqcFCqQFoSzBU2LkI
|
||||
R17p/j4NtDNBbnRvaW5lIGR1IEhhbWVsIDxhbnRvaW5lLmR1aGFtZWxAcGxhdGZv
|
||||
cm1hdGljLmRldj6IjgQTFgoANhYhBFvoo/bIpcAdEGwK2CCxo5CxaNNWBQJpsCMx
|
||||
AhsDBAsJCAcEFQoJCAUWAgMBAAIeAQIXgAAKCRAgsaOQsWjTVr/sAPwIBsG8g6ND
|
||||
zoNRTX1wPKBvfZg1NP7tYCyM5sxQfrpuLAEA05AhG4xBILfhL/f0pqR5jXfxg6gz
|
||||
T6WfeVeS6zeHZwe4OARoYDrcEgorBgEEAZdVAQUBAQdAQVmtih8AO3ryBQMR/22x
|
||||
WHVKLjAbCiH2cMxNH+iy1RQDAQgHiHgEGBYKACAWIQRb6KP2yKXAHRBsCtggsaOQ
|
||||
sWjTVgUCaGA63AIbDAAKCRAgsaOQsWjTVu8oAP9Bc+QY+9FikX3YvMgWAqiDlVOy
|
||||
o0y6UIZGBMSQlF80wAD/d34LqtVIVe9oe5NO3xA75+6Ew8tGeAjUq/ovagr5dAU=
|
||||
=JsVv
|
||||
-----END PGP PUBLIC KEY BLOCK-----
|
||||
@@ -0,0 +1,54 @@
|
||||
-----BEGIN PGP SIGNED MESSAGE-----
|
||||
Hash: SHA256
|
||||
|
||||
b85dc5f0d7a9be8038c4e73450d9125b241c792ec7ec495229345f86d1e3f6fa node-v22.23.3-aix-ppc64.tar.gz
|
||||
a6fcbecafce9c8fbf86ffcb5fb12a66af5f465c449ef5e2fcf7c4e651c1a9b14 node-v22.23.3-arm64.msi
|
||||
23b25245dcfb9af7262f8ff142e9e2e0af025368117329e7a7458a51e5922f53 node-v22.23.3-darwin-arm64.tar.gz
|
||||
72d5d8832b41c9d9646197af614ffd751406ea4d215060eb91b98864e1919a3e node-v22.23.3-darwin-arm64.tar.xz
|
||||
8a677b0219178efd6eb0e475457c4afb452b521a92f6e67845a73bd85727f2a8 node-v22.23.3-darwin-x64.tar.gz
|
||||
ac41874c3352937119cfec39e1a98c578fe58ce8a86d7e785b89a4706015f305 node-v22.23.3-darwin-x64.tar.xz
|
||||
eff1a7e67736bdf25db421ddaa613ed605f751a7b9fe07fb473ea880d1348987 node-v22.23.3-headers.tar.gz
|
||||
8448fe9f94d65aece4df26f266358644d6f41da4ce23aab09675e206edcff1d4 node-v22.23.3-headers.tar.xz
|
||||
5ced2d48d1d7198739b7f86804de0171aefb6823b684b12341d3321afc3cb0b2 node-v22.23.3-linux-arm64.tar.gz
|
||||
a44aeb94849a299b22df10b9e622ec2f605c2183501bc40590705131de7c740f node-v22.23.3-linux-arm64.tar.xz
|
||||
8b99058ec6f4e372b9887f8ecb577a292adb132d59e5104356428a758fdb126c node-v22.23.3-linux-armv7l.tar.gz
|
||||
590a2768199bdd0b848648a81cd7f07f88a5ceb27949f0c70d5e6652c3eaca69 node-v22.23.3-linux-armv7l.tar.xz
|
||||
236712ddef73f56c0e0590a2c49f69c8393a2c2d82c3d62c7c165605b472b38d node-v22.23.3-linux-ppc64le.tar.gz
|
||||
0214e542c73686134856784c0cbd2762b5414097640762b4afc2e33e6e499207 node-v22.23.3-linux-ppc64le.tar.xz
|
||||
51f64794f5f61bdfc80aa186c8273021473997605cf90ed31ed148bee650a554 node-v22.23.3-linux-s390x.tar.gz
|
||||
a311c8f0b352e7dedc13958b612ac190a56456fc837dd26cd5e5845713a1eb1d node-v22.23.3-linux-s390x.tar.xz
|
||||
633f49a4f388c5f9564d56d9c0f5ffa394346d92ca0076fe21cf776ece1988bc node-v22.23.3-linux-x64-musl.tar.gz
|
||||
0564c678817e6b6fd7860689283185cecdf42e644135b45636094757b9a7922b node-v22.23.3-linux-x64-musl.tar.xz
|
||||
1084aa36196bba4c3a5e69a1ee388a6e4ff729dad09445fbcd434b28fe3c24af node-v22.23.3-linux-x64.tar.gz
|
||||
df450af89261115ef9f9e3830c3eeb2cc9213b63c720b1af623cb5dcbe2e02de node-v22.23.3-linux-x64.tar.xz
|
||||
55c853a53b4bf88c1d8fd70e6add0b22b49c0e76352d4044caaa90512734ab7e node-v22.23.3.pkg
|
||||
9436c81b284889303d39f5b8bd629bf19e790c665ccb94b4994aa87220d9799a node-v22.23.3.tar.gz
|
||||
bd97093e1a1e9243338950c174a693a64d4e0926a9c6ce259962bc58d5e96909 node-v22.23.3.tar.xz
|
||||
33fbc3458ce5cddd694f319bcb47490fbce720c492e25573b594e205c189c89a node-v22.23.3-win-arm64.7z
|
||||
33dad22e4cef5ee8f9fbb1b0d037fdacd0e56d12a4580f0d63f68b894deab535 node-v22.23.3-win-arm64.zip
|
||||
fa7a98510f2edf181733dee15a200a5039a0459511366150e4c64da72a53ab94 node-v22.23.3-win-x64.7z
|
||||
2b0ff57b049cda1bbcea2240eec20467018713c1efe1f7360c2681859b90ed71 node-v22.23.3-win-x64.zip
|
||||
ff5ffeb0cc42d76b4bfe453638dbe9a69738fdaec967a14cd84f3b11225bb523 node-v22.23.3-win-x86.7z
|
||||
1e07ac00e61b41b7ecf242de8e38883d237d5ad3fdcad7c16f2dd3a88670e2b6 node-v22.23.3-win-x86.zip
|
||||
1c0efc8449987e7da5d184786a0a96da83ffa11d334421201e5c09b93017cb8d node-v22.23.3-x64.msi
|
||||
c71bbe31029c99d6f9cffa4841056ecc49dcd1fb09ef739b7e85dc7b207242b4 node-v22.23.3-x86.msi
|
||||
b5a3165ec6f24c0b1fe2a10fed58e03f6c1d7d141beb3b225ff030ceac6c5eb5 win-arm64/node.exe
|
||||
5378f5bad777b4929d1cd4f58974e0ef03815ce5c159863f1875c4b434460c27 win-arm64/node.lib
|
||||
3f21d9cee58b7168ee46827e04d381dcdeb2d1cc5974bc226ba3a1bcd6bf8988 win-arm64/node_pdb.7z
|
||||
bb155f2ecad21c98cd909831cb26320e123dd010a654f3649e8ba54a7de9f433 win-arm64/node_pdb.zip
|
||||
9c9245166b4a8e182e0b797da9c20136117ff24368eaff1fec8343a123c8db0e win-x64/node.exe
|
||||
c243185f2347870cadb637c909bf123fc703dc65dbfa769b17ec45a4b661d782 win-x64/node.lib
|
||||
1babb45798cf3a2d4e70574ba926e7ff26b71487ed940a98cdd4971f32af4cae win-x64/node_pdb.7z
|
||||
ab9fe384f04575fab7b2915b02d109a1d415b184cd6b28239fdb67e12c260321 win-x64/node_pdb.zip
|
||||
a8aa72dda43af5357d0a502548a54e402d4f7643be221662624a2cc876cd37bb win-x86/node.exe
|
||||
eff47cc18fe2d3e40886cd49f04a85134681657d8b0f2afc55f363743cf6ddd7 win-x86/node.lib
|
||||
18963f55b0ebda23c72ab023990e68bce5302645f9643b6f0ac52dc809e3c501 win-x86/node_pdb.7z
|
||||
95d75dce4db28836ef7ff2ddada21c1df67cc07f7d9a727ac38b021ee3afdeaa win-x86/node_pdb.zip
|
||||
|
||||
-----BEGIN PGP SIGNATURE-----
|
||||
|
||||
iHUEARYIAB0WIQRb6KP2yKXAHRBsCtggsaOQsWjTVgUCarQJ5gAKCRAgsaOQsWjT
|
||||
Vi6XAQD18cb42T8Iokdj3jgAfoB41fqECt7DeN43VsgOoSNDZwEAxqn3W+nDSkFo
|
||||
aQyV6A9gETWcFz27zSzMVA+vDrzbvQM=
|
||||
=2yJE
|
||||
-----END PGP SIGNATURE-----
|
||||
@@ -0,0 +1,35 @@
|
||||
# Production dependency validation
|
||||
|
||||
Date: 2026-10-10. Source baseline: `68ffa106f80a2080ddd9b48affe0d7a7489e4d05` on `develop`.
|
||||
|
||||
The baseline frozen production lockfile reported 4 Critical, 48 High, 56 Moderate and 14 Low advisory matches across 410 dependencies. After the targeted updates, `pnpm audit --prod --json` reports zero matches at every severity across 368 dependencies. These counts describe the dependency advisory database at the time of verification; they are not a replacement for application security review.
|
||||
|
||||
## Changes
|
||||
|
||||
- Update the HTTP parser, compression, upload, YAML, Lodash, Sequelize, gRPC and Undici dependencies to patched versions while retaining their supported release lines. Use Undici 7 for the existing Node 20 runtime, rather than Undici 8's newer Node requirement.
|
||||
- Upgrade Nodemailer to 10.0.16, including the address parser and OAuth2 TLS fixes. Nodemailer 10 requires Node 20; backend type checking and local message generation are included in validation.
|
||||
- Pin patched indirect dependencies with scoped pnpm overrides. Keep old path-to-regexp, Joi, brace-expansion, picomatch and Undici consumers on compatible release lines.
|
||||
- Remove the unused direct `http-proxy-middleware` production dependency. There are no imports or calls in the backend, frontend, CLI or shell sources. This removes its `micromatch -> braces` chain from the production graph; braces 3.0.3 has no upstream patch. Umi's development proxy remains managed by Umi's own development dependencies.
|
||||
- Upgrade the SQLite installation chain to `@mapbox/node-pre-gyp@2.0.3` and `tar@7.5.22`. Retain the existing `@whyour/sqlite3@1.1.2` binary package and N-API ABI. The SQLite source extractor uses the supported synchronous `tar.extract` API.
|
||||
- Replace nested UUID 8 with UUID 11.1.1 for Sequelize and SockJS. Database transactions and a real SockJS/WebSocket exchange verify their production use of the UUID API.
|
||||
|
||||
No advisory ignore list or risk acceptance exception was added. Development dependencies are outside the production audit claim.
|
||||
|
||||
## Local verification
|
||||
|
||||
- Independent Node 20.20.2 installation using the frozen lockfile; no shared node_modules.
|
||||
- SQLite precompiled download and extraction through the upgraded node-pre-gyp/tar chain succeeded on macOS arm64.
|
||||
- Forced SQLite source compilation using node-gyp 12.4.0 succeeded on macOS arm64, followed by the same production protocol checks. The local SDK required its C++ include directory to be supplied explicitly; no global toolchain setting was changed.
|
||||
- Complete regression: 497 passed, 7 platform-dependent skips, zero failures.
|
||||
- Backend TypeScript and frontend production builds passed. The existing frontend bundle-size advisory remains.
|
||||
- `scripts/verify-production-dependencies.cjs` exercises real SQLite reads, Sequelize transactions and updates, protobuf serialization, a local gRPC request, a local SockJS/WebSocket exchange, the Undici ProxyAgent API, JWT signature verification and Nodemailer message generation. Mail uses an in-memory transport; it does not contact recipients or an SMTP service.
|
||||
|
||||
## Release verification
|
||||
|
||||
The `Release readiness` workflow builds the same four Dockerfile variants and all 24 variant/architecture combinations used by the release workflow, including both Python 3.10 variants. It uses the candidate source SHA and the candidate static artifact, then loads each amd64 image and runs the production dependency check inside it with external networking disabled.
|
||||
|
||||
A separate Linux job tests both precompiled SQLite installation and forced source compilation using the SQLite package's declared node-gyp, rather than pnpm 8's bundled older node-gyp. This distinction matters on Python 3.12 and newer.
|
||||
|
||||
The workflow does not push images, tags, npm packages, CDN metadata or static mirrors. It runs when a pull request introduces or changes the readiness workflow; subsequent candidates can invoke it manually. A successful run is required evidence for this dependency update, not evidence that a formal version has already been published.
|
||||
|
||||
Linux source compilation and the complete image matrix still require CI confirmation; results will be recorded after the candidate run completes.
|
||||
@@ -0,0 +1,31 @@
|
||||
# 发布运行时支持
|
||||
|
||||
2026-10-10 的依赖验收区分 npm 锁文件审计与镜像运行时维护。生产 npm 审计零公告命中不能证明 Node、Python、操作系统或用户脚本依赖没有漏洞。
|
||||
|
||||
默认 Debian 镜像使用 Node 22.23.3、Python 3.11.17 和 Debian 13(Trixie),保留 amd64、arm/v7、arm64、ppc64le、s390x 五种架构。Node 22 官方 Bookworm 镜像缺少 s390x,因此在原生构建阶段下载官方目标架构发布包,只将目标 Node 二进制复制到 Python 阶段;npm 等工具来自同源码 CI 构建的受控归档。不要将下载阶段自身的原生 Node 或附带的旧工具树复制到目标镜像。
|
||||
|
||||
`docker/install-node.sh` 使用仓库中固定的 Node 22.23.3 签名摘要清单和官方发布者公钥,通过 GPG 检查签名、发布者指纹和下载包 SHA256。升级 Node 时一起更新版本、签名清单和发布者公钥,并重新核验全部架构。arm/v7 需要 `libatomic1`。已读取五种实际 Node ELF:GLIBC 需求不超过 2.28,arm/v7 的 GLIBCXX 需求不超过 3.4.26;Trixie 满足这些要求,实际构建仍由完整镜像矩阵验证。
|
||||
|
||||
默认 Alpine 镜像固定 Python 3.11.17 / Alpine 3.24 基础镜像摘要,保留原有七种架构;运行时 Node 由 Alpine APK 提供。2026-10-10 的七架构 APK 索引均为 Node 24.18.1-r0。原生包安装阶段使用 Node 22.23.3,替换已结束维护的 Node 18。源码安装要求 Node 22.x >=22.22.2 或 Node 24.x >=24.15.0,CI 在 Node 22 和 24 上验证回归;镜像验证同时输出实际 Node、Python 和机器架构。
|
||||
|
||||
Python 3.10 已于 2026-10-01 结束维护。两个兼容 Dockerfile 仍单独验证,继续发布还是停止新增兼容标签需在正式发版前明确;不能将这些兼容镜像描述为受支持的默认运行时。已有历史镜像不会因这次准备工作删除。
|
||||
|
||||
运行时变更后必须重新执行镜像矩阵,并在使用实际新 Node 的独立 dev 容器中重新做至少两小时功能验证。原 Node 20 环境中的依赖长跑单独保留,不能替代新运行时结果。镜像准备与 CI 构建不会自动执行正式发布。
|
||||
|
||||
镜像中的全局 pnpm、PM2、npm 等工具不属于应用 `pnpm-lock.yaml` 的生产树。a70ec66 候选的实际全局工具补审发现 High/Critical 公告匹配。本轮收尾使用固定来源重构 npm 12.2.0 和 pnpm 10.34.6,更新实际 node-gyp 12.4.0、tar 7.5.22、PM2 的 js-yaml 4.3.2 等间接依赖;源码锁迁移到 PNPM10 格式,不改变已锁定的版本、依赖边或 peer 上下文。只供前端构建的 plots 移到开发依赖,应用生产树从 367 个减到 282 个(按 PNPM10 审计口径)。
|
||||
|
||||
受控工具归档是 Linux CI 生成的纯 JavaScript 产物。静态构建清单覆盖归档及 proof,proof 记录源码提交、所有构建输入 Git blob 与 SHA256、实际工具文件和安全相对链接。安装器只用 Node 标准库,先验收再原子替换已知工具,失败回退;不解析浮动 registry 版本,也不改变持久化 `global/5` 脚本依赖。启动、修复、源码升级共用这套安装路径。普通项目安装保留 PNPM10 的脚本批准规则,全局用户脚本依赖维持原有原生安装行为。
|
||||
|
||||
实际工具树的本机审计仍有 **一项 High:braces 3.0.3,GHSA-vfj7-8cjw-p6xm**,当前没有已发布修复版本。默认 PM2 watch 关闭;四个正常安装、冻结离线安装、workspace 和 pack 场景的 V8 覆盖未执行其 parse/compile/expand,正向控制确认覆盖有效。这仅限定这些已测场景,不证明所有命令不可达;用户启用 watch、维护 glob 或调用 Shell 模式仍属于相关输入面。[公告](https://github.com/advisories/GHSA-vfj7-8cjw-p6xm)。最终 Linux CI、镜像实际树和 dev 维护行为仍须验收,不宣称全局依赖漏洞为零。
|
||||
|
||||
主 npm 包按单一冻结 PNPM 图生成 `bundleDependencies`,记录实际发货源文件、每个依赖 manifest 和 Node 解析边。消费者修复只校验随包树并处理 SQLite,不重新解析 npm 依赖。npm 12 默认阻止依赖安装脚本;全局安装需精确批准 `npm install -g @whyour/qinglong@2.23.0 --allow-scripts=@whyour/qinglong@2.23.0`,项目安装在自己的 `package.json` 设置 `allowScripts`。实际 SQLite 编译由主包安装脚本转发。CI 必须独立验证 npm10 本地、npm12 本地/全局的预编译和源码编译六种组合,正式发布只消费通过验收的同一 TGZ。[npm12 脚本配置](https://docs.npmjs.com/cli/v12/using-npm/config/#allow-scripts)。
|
||||
|
||||
源码安装固定 pnpm 10.34.6 和 frozen lock。若升级暂存源码遇到旧 manager,只有同提交、同锁和全部输入哈希匹配的静态工具归档才可在临时 prefix 完成安装;无受控归档时明确失败,不再回退到 npm 无锁解析。旧 Node20/PNPM8 容器应拉取新镜像并重建;宿主机先升级 Node,再重新安装完整 npm 包。Termux 的无锁源码回退也不再支持。CLI 的远程 API 模式仍仅需 Node >=22.12。
|
||||
|
||||
依据:[Node 发布计划](https://github.com/nodejs/Release/blob/main/schedule.json)、[Node 22.23.3 发布与下载](https://nodejs.org/en/blog/release/v22.23.3)、[官方 Node 发布者公钥](https://github.com/nodejs/release-keys)、[Python 支持周期](https://devguide.python.org/versions/)、[Python 3.11.17 Trixie 镜像元数据](https://github.com/docker-library/repo-info/blob/master/repos/python/remote/3.11.17-slim-trixie.md)、[Python 3.11.17 Alpine 镜像元数据](https://github.com/docker-library/repo-info/blob/master/repos/python/remote/3.11.17-alpine3.24.md)。
|
||||
|
||||
## 前端构建运行时
|
||||
|
||||
后端继续在 Node 22 和 24 上验收。现有 Umi 4.4.4 构建依赖加载 spdy/http-deceiver,仍使用 Node 24 已移除的 `http_parser`。因此 `build:front` 通过 pnpm 单次命令参数固定使用 Node 22.23.3;这不会改变后端、全局 Node 或其他脚本的运行时。使用 `pnpm build:front` 或 `npm run build:front`,不要直接在 Node 24 下调用 `max build`。pnpm 会从官方 Node 发布站下载构建运行时,首次前端构建需要网络访问;前端构建不属于容器运行时离线验收阶段。Node 22/24 CI 均执行该构建入口,覆盖宿主 Node 24 的场景。此范围待前端工具链升级后再放宽。
|
||||
|
||||
相关设置:[pnpm 10 Node.js 运行时设置](https://github.com/pnpm/pnpm.io/blob/main/versioned_docs/version-10.x/settings.md#usenodeversion)。
|
||||
@@ -0,0 +1,97 @@
|
||||
# 青龙生产依赖候选 dev 验收
|
||||
|
||||
> 此文档记录先前依赖迁移候选构建的验收。2.23.0 已采用最小生产依赖修复方案发布;完整 Node/pnpm 和依赖迁移仍留在草稿中,等待后续版本。历史提交与构建证据不能作为当前合并后提交的验收结果,需重新运行 CI。
|
||||
|
||||
**结论:全部完成审计通过。**
|
||||
|
||||
完成审计:57/57 项通过,0 项未通过。
|
||||
|
||||
## 候选与打包来源
|
||||
|
||||
- 源码:`5038d2d9a046b629dfe01e8162d48fe7143f5ef4`。
|
||||
- dev 测试镜像:`sha256:141bdc3e3281cc22cd4bf407feb782fe969b92cde48f724c5e9d5bdc7078e8e1`;容器:`ql-dependency-soak-20261010`。
|
||||
- 打包方法:`native-debian-runtime-repack`。本轮在既有原生 Debian runtime `sha256:9326cfda470960877b537463fc9158964356521586b58ebb1373a270fcfcd533`(基础源码 `68ffa106f80a2080ddd9b48affe0d7a7489e4d05`)上重新打包候选源码、全新冻结锁文件生产依赖包树及同 SHA 的 CI 静态产物。
|
||||
- dev 镜像是本轮依赖功能验收用重打包产物;正式发行 Dockerfile 的完整四种镜像与多架构构建由下面的 CI 独立验证。
|
||||
- 镜像内 Git HEAD、源/镜像 package.json 与锁文件哈希、全部静态文件哈希及实际依赖版本在开始、长跑期间、重启后和结束时复核。
|
||||
|
||||
运行环境:Node `20.20.2`,`Python 3.11.14`,`linux/x64`。静态产物复核 729 个文件;来源复核 7 次。
|
||||
|
||||
| 运行依赖 | 版本 |
|
||||
|---|---|
|
||||
| nodemailer | `10.0.16` |
|
||||
| sequelize | `6.37.8` |
|
||||
| @grpc/grpc-js | `1.14.6` |
|
||||
| undici | `7.30.0` |
|
||||
| multer | `2.4.0` |
|
||||
| lodash | `4.18.1` |
|
||||
| sqlite3 | `1.1.2` |
|
||||
|
||||
## 完整发布 CI
|
||||
|
||||
[Release readiness run 37965143236](https://github.com/whyour/qinglong/actions/runs/37965143236);源码 `5038d2d9a046b629dfe01e8162d48fe7143f5ef4`;结论 `success`。
|
||||
|
||||
| 镜像 | 架构数 | 构建 | 原生 amd64 协议检查 |
|
||||
|---|---:|---|---|
|
||||
| alpine | 7 | 通过 | 通过 |
|
||||
| debian | 5 | 通过 | 通过 |
|
||||
| alpine-python3.10 | 7 | 通过 | 通过 |
|
||||
| debian-python3.10 | 5 | 通过 | 通过 |
|
||||
|
||||
留存 CI 证明记录 4 种镜像、24 个镜像/架构组合及 4 个通过的原生协议检查;该 readiness 流水线未推送正式镜像、版本标签、npm 包或 CDN。
|
||||
|
||||
## 至少两小时的实际功能运行
|
||||
|
||||
开始:2026-10-10 02:33:01 CST;结束:2026-10-10 04:35:30 CST。
|
||||
有效运行 7324.418 秒;总墙钟 7348.53 秒;计划重启停机合计 24.112 秒已扣除。
|
||||
健康/CLI 采样 578 次,失败 0 次;功能阶段 64 项,失败 0 项。
|
||||
|
||||
| 脚本语言 | 实际执行次数 | 执行时间跨度(秒) | 最大间隔(秒) |
|
||||
|---|---:|---:|---:|
|
||||
| JS | 733 | 7338.495 | 21.054 |
|
||||
| PY | 727 | 7338.195 | 70.081 |
|
||||
| SH | 469 | 6884.0 | 26.0 |
|
||||
|
||||
数据库实例记录:成功 1929 次,计划重启中断 0 次;中断的结束时间和未知退出码另行断言。
|
||||
|
||||
覆盖秒级任务调度、三种语言执行、禁用/恢复/修改/删除、并发与排队失效、任务停止/失败、环境变量更新;Git/SSH/HTTP/raw 订阅、过滤/自动任务、间隔与定时拉取、共享目录删除保护;脚本上传/编辑/历史/hash 冲突/回滚/路径限制;Node/Python/Linux 依赖安装、执行、重装与卸载;认证、应用 scope/secret、TOTP 和备份导入。
|
||||
|
||||
| 预检套件 | 真实断言 | 失败 |
|
||||
|---|---:|---:|
|
||||
| full | 18 | 0 |
|
||||
| other | 15 | 0 |
|
||||
| advanced-retry | 3 | 0 |
|
||||
| queue | 4 | 0 |
|
||||
|
||||
初始化/安全/历史/CLI 断言:26 项。API 依赖专项:8 轮、129 项,失败 0 项;包括真实请求解析、gzip、反向代理 IP、上传临时文件和授权 SockJS XHR/WebSocket。
|
||||
|
||||
## 通知与真实邮件收件
|
||||
|
||||
Nodemailer `10.0.16`:通知 8 轮、224 项;真实收到邮件 164 封,其中 TLS 124 封。
|
||||
传输模式:`{"tls": 82, "plain": 40, "starttls": 42}`;TLS 协议:`{"TLSv1.3": 124}`;认证机制:`{"PLAIN": 148, "LOGIN": 16}`。
|
||||
预期错误反馈:认证拒绝 8 次,收件人拒绝 8 次;夹具内部错误 0 次。
|
||||
本地 HTTP 通知接收 112 次;认证 CONNECT 代理 16 次。
|
||||
|
||||
覆盖面板 Plain/LOGIN/STARTTLS/SSL、保存配置、错误配置不覆盖原设置、空收件人回退、官方 JS/Python 样例、gRPC、UTF-8 正文/附件、四种 webhook body、gotify、ntfy 和 Telegram HTTP 代理。全部收件及 HTTP 请求由容器 localhost 夹具接收。报告只保留计数和协议摘要。
|
||||
|
||||
真实任务队列通知:1 轮;JS/Python 各执行官方 sample 和 QLAPI,SMTP 收件证据 4 封;实例退出码、任务日志 SHA、TLS/认证及原始收件 SHA 单独核对。任务和脚本清理错误 0 项。
|
||||
|
||||
## 资源与原有服务
|
||||
|
||||
资源采样 104 次;最高内存比例 23.73%(486.1MiB / 2GiB);最高 CPU 126.66%。
|
||||
|
||||
| 原有容器 | ID保持 | StartedAt保持 | 运行状态保持 |
|
||||
|---|---|---|---|
|
||||
| demo | 是 | 是 | 是 |
|
||||
| ql | 是 | 是 | 是 |
|
||||
|
||||
## 完成审计与剩余事项
|
||||
|
||||
所有明确验收项及来源/时长/失败/服务保护检查通过。正式发版和公告仍按发布流程执行。
|
||||
|
||||
早期预检记录:fixture connection reuse failure; targeted fresh-agent export+upload passed。备份夹具首次连接失败记录保存于 `failed-preflight-1`;随后使用独立新 Undici Agent 的真实导出和上传均通过,没有自动重试或修改候选产品代码。首次异常未采集 cause;服务保持健康时闭合连接的 UND_ERR_SOCKET 复现提供时序佐证。
|
||||
夹具修正状态:`True`;原始失败断言 1 项;留存证明文件 3 份。
|
||||
|
||||
原长跑窗口 `hour-attempt1` 独立留存:失败阶段 `[{"phase": "dependency-integration", "status": "fail", "errorType": "RuntimeError"}]`,DI 失败断言 `["actual SockJS XHR polling opens and exchanges authenticated messages"]`;归档原文件哈希复核 21/21。旧 SMTP journal 记录 43 封邮件,仅计入旧窗口。
|
||||
首次 SockJS XHR 接收帧同时包含订阅广播和正确的本次 echo,原夹具对整个帧作单消息比较而失败。修正后只识别合法 a 帧内的唯一测试 marker,同时继续拒绝 marker 缺失、改变或重复。旧周期记录与邮件 journal 已归档,新窗口重新执行 DI/通知预检和任务通知;最终计时与当前周期统计不合并旧窗口。
|
||||
|
||||
原始 owner 凭据、私钥、数据库、邮件正文及原始日志没有进入本报告或安全汇总。仅在主代理保存本报告、完成审计和安全证据至另一目录并审阅 cleanup-owned.py 后,可决定清理本轮容器/镜像和测试私密数据。
|
||||
@@ -0,0 +1,81 @@
|
||||
# 青龙 2.23.0 发布验证草稿
|
||||
|
||||
> 此文档记录先前依赖迁移候选构建的验收。2.23.0 已采用最小生产依赖修复方案发布;完整 Node/pnpm 和依赖迁移仍留在草稿中,等待后续版本。历史提交与构建证据不能作为当前合并后提交的验收结果,需重新运行 CI。
|
||||
|
||||
记录日期:2026-10-10(Asia/Shanghai)。此前运行时、依赖和版本候选源码:`a70ec66fe3c8395c3edfe3543c039f682c716b5b`。本轮受控工具和 npm 归档收尾改变了源码、锁格式及安装路径,须以新提交重新执行 CI 和独立 dev 窗口;下方旧提交结果不代表最终工件验收。原依赖候选 `5038d2d9a046b629dfe01e8162d48fe7143f5ef4` 的验证单独保留。正式发布提交如继续变化,应更新记录并验证受影响的部分。
|
||||
|
||||
## 此前运行时候选已完成(a70ec66fe)
|
||||
|
||||
| 验证 | 结果 | 证据及范围 |
|
||||
| --- | --- | --- |
|
||||
| Linux Node 22 / 24 回归 | 各 504 通过,0 失败,0 跳过 | [Release readiness CI](https://github.com/whyour/qinglong/actions/runs/37979273057),实际 Node 22.23.3 / 24.21.0 |
|
||||
| 静态产物 | 729/729 文件摘要匹配,无额外或缺失文件 | 同次 CI,源码 SHA 和生产锁文件 SHA 一致,dirty=false |
|
||||
| SQLite Linux 安装 | 预编译和强制源码编译均通过 | Node 22,分别执行四项实际数据库/网络/邮件协议验证 |
|
||||
| 四种 release Dockerfile | 24 个变体/架构组合构建通过 | 默认和 Python 3.10 兼容变体,架构覆盖保持不变 |
|
||||
| 原生运行协议 | 四个 amd64 镜像各 4 项通过 | 网络 none;其他 20 个架构组合在本项没有实际执行证明 |
|
||||
| Debian Node 官方包 | 两个变体共十次验签和 SHA256 核验通过 | 每个变体五种架构,签名发布者及目标包摘要强制检查 |
|
||||
| CLI 0.1.2 | Node 22.12 / 24 类型、测试、打包和离线安装通过 | [CLI package CI](https://github.com/whyour/qinglong/actions/runs/37979272601),发布 job 跳过 |
|
||||
| 本机 Node 22 构建及协议 | 前后端和完整内部 CLI 构建通过,四项生产协议通过 | macOS arm64;SQLite 下载连接中断后源码编译,显式指定本机 SDK C++ 头文件路径后通过,未修改产品代码 |
|
||||
|
||||
实际 amd64 运行时为:默认 Debian Node 22.23.3 / Python 3.11.17;Debian 3.10 Node 22.23.3 / Python 3.10.19;默认 Alpine Node 24.18.1 / Python 3.11.17;Alpine 3.10 Node 24.18.1 / Python 3.10.22。兼容镜像构建通过不表示 Python 3.10 仍受维护。
|
||||
|
||||
本次 CI 导出的原生 Debian 镜像已下载并核验归档 SHA256、config digest、源码 label 与静态产物来源。镜像 ID 为 `sha256:203b5c5ae3c357e75c29f82491f6aff20d4e0e387ac1a237c36286bfb949b482`,归档 SHA256 为 `8d9dea1c15640c3953ab546734cf267e13c4aabdfad7fa88f39712d616387813`。独立 Node 22 dev 窗口使用这个镜像及全新数据,预备阶段的测试工具安装不计入时长,完整预检通过后才启动至少 7320 有效秒的时钟;不合并原 Node 20 窗口。
|
||||
|
||||
## 原依赖候选已完成(5038d2d9a)
|
||||
|
||||
| 验证 | 已验证结果 | 证据及范围 |
|
||||
| --- | --- | --- |
|
||||
| Linux 回归 | 504 通过,0 失败,0 跳过 | [Release readiness CI](https://github.com/whyour/qinglong/actions/runs/37965143236) |
|
||||
| 静态产物 | 前端/后端构建与产物清单通过 | 同一候选来源提交、锁文件、全部文件哈希 |
|
||||
| SQLite Linux 安装 | 预编译下载/解压及强制源码编译均通过 | 分别执行实际 SQLite、Sequelize、gRPC、SockJS/WebSocket、JWT、ProxyAgent 与内存邮件生成检查 |
|
||||
| 四种 release Dockerfile | 24 个变体/架构组合构建通过 | Alpine 默认和 3.10 各 7 架构;Debian 默认和 3.10 各 5 架构 |
|
||||
| 镜像运行协议 | 四个 amd64 镜像均通过 | 使用正式 Dockerfile 构建候选,以 `--network none` 执行生产依赖验证器;其他架构在此项没有实际运行证明 |
|
||||
| CLI 类型/测试/打包 | Node 22.12.0 与 24 均通过,离线包安装通过 | [CLI package CI](https://github.com/whyour/qinglong/actions/runs/37965142712);发布 job 跳过,当时包版本仍为 0.1.1 |
|
||||
| 本地 Node 22.23.3 / 24.18.0 回归 | 各 497 通过、7 平台相关跳过、0 失败 | macOS arm64,新运行时完整回归;Node 24 首次沙箱运行的端口/进程 EPERM 失败日志保留,开放测试所需回环和进程权限后通过 |
|
||||
| 本地 Node 20 回归 | 497 通过,7 平台相关跳过,0 失败 | macOS arm64;前后端构建通过,SQLite 预编译与源码编译通过;不能代替 Linux 结果 |
|
||||
| 前端依赖定向 smoke | 7 项通过,0 控制台错误/警告 | 实际 CI bundle、全部 729 个文件哈希匹配;独立 Chrome + localhost 模拟 API;覆盖 Moment 日期选择和 Lodash 表单/搜索/默认值,不属于 dev 后端长跑证据 |
|
||||
|
||||
生产锁文件审计:开发基线 `68ffa106f80a2080ddd9b48affe0d7a7489e4d05` 的 410 个生产依赖报告 Critical 4、High 48、Moderate 56、Low 14;本次冻结锁文件的 368 个生产依赖各级匹配均为 0。没有新增 advisory 忽略名单或风险豁免。统计对应核验时的漏洞数据库,不表示没有未知漏洞,不覆盖开发依赖、用户脚本依赖、系统软件或运行时。
|
||||
|
||||
四种镜像矩阵为:
|
||||
|
||||
- Alpine 默认/Python 3.10:amd64、arm/v6、arm/v7、arm64、ppc64le、s390x、386。
|
||||
- Debian 默认/Python 3.10:amd64、arm/v7、arm64、ppc64le、s390x。
|
||||
|
||||
## dev 验证:原依赖窗口完成,新运行时窗口进行中
|
||||
|
||||
原依赖候选 `5038d2d9a` 的独立 dev 容器采用已有原生 Debian 运行时重新打包全新冻结生产依赖、已核验 Linux SQLite 二进制与同候选 CI 静态构建;它不是从 CI 导出的同一镜像。实际 Node 20.20.2 / Python 3.11.14,完整 release Dockerfile 和矩阵结果来自上方独立 CI。
|
||||
|
||||
该窗口于 2026-10-10 02:33:01–04:35:30 CST 完成,有效运行 **7324.418 秒**,两次计划重启的 24.112 秒停机已扣除。完成审计 **57/57**,健康/CLI 采样 578 次、功能阶段 64 项,均无失败;数据库记录 1929 次成功任务实例(JS 733、Python 727、Shell 469)。JS/Python 执行跨度分别为 7338.495 / 7338.195 秒。API 依赖专项 8 轮 / 129 项,通知 8 轮 / 224 项均通过。受控 SMTP 实际收件 164 封,其中 TLS 124 封,包含真实 JS/Python 队列任务生成的四封通知。原 `demo` / `ql` 容器的 ID、StartedAt 和运行状态保持不变。详见[完整依赖 dev 验收报告](./2.23.0-dev-dependencies.md)。
|
||||
|
||||
邮件检查使用受控 SMTP 服务、测试收件域名和隔离网络,核对认证、STARTTLS/SSL、中文内容/附件以及官方 JS/Python 示例与队列任务的实际收件。这不是外部真实邮箱的送达率证明。
|
||||
|
||||
此前测试夹具出现备份 fetch 复用已关闭连接和 SockJS 合法广播混合帧两项问题;原始失败断言和证据哈希在报告及脱敏汇总中保留,修正仅涉及独立夹具,失败窗口不计入上述 7324.418 秒。完成报告、57 项审计及安全证据已在本地和远端独立归档并逐文件复核 SHA256;本轮自建容器、重打包镜像、凭据、数据库、邮件正文及原始私有日志已按清理清单移除,保留脱敏断言、原始证据哈希和完成报告;原 dev/demo 及 Node 22 测试未改变。
|
||||
|
||||
此前 `a70ec66fe` 的 **实际 CI 导出 Node 22 镜像**使用全新数据另行验证。有效时钟从 2026-10-10 03:51:39 CST 开始,SSH 订阅失败后,该窗口及后续关联失败均不计入成功长跑。驱动已按 PID/开始时间和证据哈希保护停止,原镜像层、容器及失败证据保留。
|
||||
|
||||
定位发现原 dev Docker 存储中的 `libcrypto.so.3` 字节损坏:读出 SHA256 为 `6379f4254678d92169c3fc02d5d6005cb09da06e6a21b206f6d9fb0f4f5d6b7b`,SSH、curl、OpenSSL、Python SSL 在只读、无网络、无测试数据挂载的同镜像容器中均崩溃。原 CI gzip 的摘要正确,全部 18 个层的原始 diffID 与镜像配置匹配,归档层内该库为 `8ae56f0a9481280a68c19f59809cd242a07a91ad7142944d21a878221992244b`,与 Debian 签名索引指定的同版本官方包一致。仅一份自建候选引用该损坏基础层;原 demo/ql 不引用它。
|
||||
|
||||
在新的独立 Docker/containerd 数据目录导入原 CI gzip 后,镜像 ID 和源码 revision 不变,该库恢复为正确 SHA256,四项系统程序检查全部 exit 0。原 daemon、demo/ql 身份和启动时间未改变。已证明 CI 归档与 dev 原存储的差异,尚未证明损坏写入者或机制;没有为此改发布源码或删除共享 Docker 层。新的最终工件将使用这个独立存储、全新数据和计数,完整预检后另起至少 7320 有效秒的窗口。
|
||||
|
||||
## 运行时维护与待确认变体
|
||||
|
||||
此前补审实际 CI 镜像 `/usr/local/lib/node_modules` 发现全局 pnpm/PM2/npm 的 High/Critical 匹配;不在旧应用 368 个生产依赖的范围内。本轮已实现受控纯 JavaScript 工具归档:固定并核验官方 npm12/PNPM10 来源、更新实际嵌套安装链、记录源码 Git blob/文件哈希、安装前验收、并发 owner 锁及失败原子回退。三个内部 CLI 测试文件的安装/修复/升级适配通过完整回归:293 通过、5 平台跳过、0 失败;最终 Linux CI 和镜像尚待运行。
|
||||
|
||||
工具本机实际树审计覆盖 4167 个 JS 文件、425 个内联版本和 292 个实际 package records,仍有一项无已发布补丁的 High `braces@3.0.3`。默认 watch 关闭,四项正常安装/离线/workspace/pack 的 V8 覆盖没有调用其解析函数;不证明其他 glob/Shell 命令不可达。详见[工具风险与运行时支持](../development/runtime-support.md)。不宣称全局工具、系统软件或用户依赖审计为零。
|
||||
|
||||
主 npm 包的 shrinkwrap 试验曾让消费者重新装入有公告的旧间接依赖,npm12 也不再采用 shrinkwrap。因此新 producer 从同一冻结 PNPM 图展开普通 bundled 依赖树,保留各版本、peer 上下文及来源,单独记录必需的 manifest 边变换、实际发货清单和解析边。不补丁编译 JavaScript,也不产生另一份浮动 npm 锁。
|
||||
|
||||
本机研究归档的 npm10 本地、npm12 本地和隔离 global 消费全部通过:实际 SQLite 源码编译及 binding 哈希、完整依赖清单、四协议、Keyv、内部 CLI 和重复 bundled repair;两个本地生产审计各级为零。精确 registry 版本脚本批准的公开 fixture 与主包回环 registry 试验均通过,未发布 2.23.0。研究包仍来自 a70 源码加显式 metadata/helper preview,没有最终 Linux runtime-tools 静态产物,不能当作发行工件。
|
||||
|
||||
新 CI 将验证最终同提交静态归档、Git 输入和生产锁;npm10 本地/npm12 本地/全局各执行预编译和强制源码六种实际消费。预编译下载失败后源码 fallback 明确不能算预编译通过。旧 manager 的同源临时工具 source fallback 另有 Linux 正例。未来 npm 发布只消费这份已验证 TGZ,不从工作树再次解析构建。当前均未执行正式发布。[npm overrides 规则](https://docs.npmjs.com/cli/v11/configuring-npm/package-json/#overrides)、[npm12 脚本批准](https://docs.npmjs.com/cli/v12/using-npm/config/#allow-scripts)。
|
||||
|
||||
原 Debian 候选和原 dev 验证使用 Node 20;该发布线于 2026-04-30 结束官方维护,EOL 后不再提供官方安全修复。生产 npm 审计为零不能消除这个风险。当前候选已改为受支持的 Node 22.23.3、Python 3.11.17 和 Debian 13,全五种架构的官方 Node 发布包已核验签名、摘要与 ELF 依赖,新镜像矩阵已通过。独立 Node 22 长跑仍待完成;上方 Node 20 长跑不会代替它。详见 [运行时支持](../development/runtime-support.md)。[Node 官方维护日程](https://github.com/nodejs/Release/blob/main/schedule.json)、[Node EOL 说明](https://nodejs.org/en/about/eol)
|
||||
|
||||
Python 3.10 于 2026-10-01 结束维护;它的兼容 tag 是否继续发布及如何披露风险仍待最终 review。默认 Python 3.11 仍处于安全维护期,计划到 2027-10 结束。不能把 Python 3.10 镜像构建成功写成运行时仍有安全支持。[Python 官方支持表](https://devguide.python.org/versions/)
|
||||
|
||||
Alpine 的运行时和构建阶段分开记录:当前 CI 实际运行时为受支持的 Node 24.18.1,builder 已由 Node 18 改为 Node 22.23.3。仍未进行整个操作系统镜像的漏洞扫描,不宣称 OS 软件或所有原生组件的漏洞匹配为零。
|
||||
|
||||
## 尚未正式发布
|
||||
|
||||
尚未执行 `shell/pub.sh`,未更新正式 master/tag、CDN、镜像仓库、主 npm 包或 CLI npm 包,也没有发送 Telegram 公告。已通过的准备流水线不等于正式发布产物存在。版本草稿已经统一为主包 2.23.0、CLI 0.1.2;版本时间当前为准备时间,正式发布前应更新实际发布时间。
|
||||
+46
-15
@@ -1,7 +1,10 @@
|
||||
{
|
||||
"name": "@whyour/qinglong",
|
||||
"packageManager": "pnpm@8.3.1",
|
||||
"packageManager": "pnpm@10.34.6",
|
||||
"version": "2.23.0",
|
||||
"engines": {
|
||||
"node": "^22.22.2 || ^24.15.0 || >=26.0.0"
|
||||
},
|
||||
"description": "Timed task management platform supporting Python3, JavaScript, Shell, Typescript",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
@@ -20,7 +23,7 @@
|
||||
"start": "concurrently -n w: npm:start:*",
|
||||
"start:back": "nodemon ./back/app.ts",
|
||||
"start:front": "max dev",
|
||||
"build:front": "max build",
|
||||
"build:front": "pnpm --use-node-version=22.23.3 exec max build",
|
||||
"build:back": "tsc -p back/tsconfig.json",
|
||||
"test": "TS_NODE_PROJECT=back/tsconfig.json node -r ts-node/register/transpile-only --test test/back/*.test.cjs test/front/*.test.cjs",
|
||||
"benchmark:execution": "TS_NODE_PROJECT=back/tsconfig.json node -r ts-node/register/transpile-only scripts/benchmark-execution.cjs",
|
||||
@@ -75,21 +78,49 @@
|
||||
"sqlite3": "npm:@whyour/sqlite3@1.1.2",
|
||||
"@codemirror/state": "6.5.4",
|
||||
"@codemirror/view": "6.39.16",
|
||||
"@mapbox/node-pre-gyp": "2.0.3",
|
||||
"tar": "7.5.22",
|
||||
"protobufjs": "7.6.6",
|
||||
"websocket-driver": "0.7.5",
|
||||
"proxy-addr": "2.0.8",
|
||||
"websocket-driver": "0.7.5"
|
||||
"validator": "13.15.35",
|
||||
"jws@3": "3.2.3",
|
||||
"path-to-regexp@<1": "0.1.13",
|
||||
"brace-expansion@1": "1.1.21",
|
||||
"picomatch@2": "2.3.2",
|
||||
"undici@6": "6.29.0",
|
||||
"joi@17": "17.13.8",
|
||||
"qs": "6.16.0",
|
||||
"dottie": "2.0.7",
|
||||
"@protobufjs/utf8": "1.1.2",
|
||||
"moment": "2.31.0",
|
||||
"on-headers": "1.1.0",
|
||||
"follow-redirects": "1.16.0",
|
||||
"lodash": "4.18.1",
|
||||
"body-parser@1": "1.20.6",
|
||||
"uuid@8": "11.1.1"
|
||||
},
|
||||
"allowBuilds": {
|
||||
"@whyour/sqlite3@1.1.2": true,
|
||||
"core-js@3.34.0": true,
|
||||
"core-js-pure@3.39.0": true,
|
||||
"es5-ext@0.10.64": true,
|
||||
"esbuild@0.18.20 || 0.21.4": true,
|
||||
"fsevents@2.3.3": true,
|
||||
"protobufjs@7.6.6": true,
|
||||
"react-intl-universal@2.12.0": true
|
||||
}
|
||||
},
|
||||
"dependencies": {
|
||||
"@ant-design/plots": "^2.6.8",
|
||||
"@bufbuild/protobuf": "^2.10.0",
|
||||
"@grpc/grpc-js": "1.14.5",
|
||||
"@grpc/grpc-js": "^1.14.6",
|
||||
"@grpc/proto-loader": "^0.8.0",
|
||||
"@keyv/sqlite": "^4.0.1",
|
||||
"@otplib/preset-default": "^12.0.1",
|
||||
"body-parser": "^1.20.3",
|
||||
"body-parser": "^1.20.6",
|
||||
"celebrate": "^15.0.3",
|
||||
"chokidar": "^4.0.1",
|
||||
"compression": "1.8.2",
|
||||
"compression": "^1.8.2",
|
||||
"cors": "^2.8.5",
|
||||
"cron-parser": "^5.4.0",
|
||||
"cross-spawn": "^7.0.6",
|
||||
@@ -101,33 +132,33 @@
|
||||
"express-urlrewrite": "^2.0.3",
|
||||
"helmet": "^8.1.0",
|
||||
"hpagent": "^1.2.0",
|
||||
"http-proxy-middleware": "^3.0.3",
|
||||
"iconv-lite": "^0.6.3",
|
||||
"ip2region": "2.3.0",
|
||||
"js-yaml": "^4.1.0",
|
||||
"js-yaml": "^4.3.2",
|
||||
"jsonwebtoken": "^9.0.2",
|
||||
"keyv": "^5.2.3",
|
||||
"lodash": "^4.17.21",
|
||||
"lodash": "^4.18.1",
|
||||
"multer": "2.4.0",
|
||||
"node-cron": "4.6.0",
|
||||
"nodemailer": "^10.0.9",
|
||||
"nodemailer": "^10.0.16",
|
||||
"p-queue-cjs": "7.3.4",
|
||||
"proper-lockfile": "^4.1.2",
|
||||
"ps-tree": "^1.2.0",
|
||||
"reflect-metadata": "^0.2.2",
|
||||
"request-ip": "3.3.0",
|
||||
"sequelize": "6.37.8",
|
||||
"sequelize": "^6.37.8",
|
||||
"sockjs": "^0.3.24",
|
||||
"sqlite3": "npm:@whyour/sqlite3@1.1.2",
|
||||
"toad-scheduler": "^3.0.1",
|
||||
"typedi": "^0.10.0",
|
||||
"undici": "^7.9.0",
|
||||
"uuid": "^11.0.3",
|
||||
"undici": "^7.30.0",
|
||||
"uuid": "^11.1.1",
|
||||
"winston": "^3.17.0",
|
||||
"winston-daily-rotate-file": "^5.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@ant-design/icons": "^5.0.1",
|
||||
"@ant-design/plots": "^2.6.8",
|
||||
"@ant-design/pro-layout": "6.38.22",
|
||||
"@codemirror/state": "6.5.4",
|
||||
"@codemirror/view": "6.39.16",
|
||||
@@ -171,7 +202,7 @@
|
||||
"concurrently": "^7.0.0",
|
||||
"file-saver": "2.0.2",
|
||||
"lint-staged": "^13.0.3",
|
||||
"moment": "2.30.1",
|
||||
"moment": "2.31.0",
|
||||
"monaco-editor": "0.33.0",
|
||||
"nodemon": "^3.0.1",
|
||||
"prettier": "^2.5.1",
|
||||
|
||||
Generated
+10562
-9122
File diff suppressed because it is too large
Load Diff
Executable
+405
@@ -0,0 +1,405 @@
|
||||
#!/usr/bin/env node
|
||||
'use strict';
|
||||
const fs = require('node:fs');
|
||||
const path = require('node:path');
|
||||
const os = require('node:os');
|
||||
const crypto = require('node:crypto');
|
||||
const { createRequire } = require('node:module');
|
||||
const { spawnSync } = require('node:child_process');
|
||||
const assert = require('node:assert/strict');
|
||||
|
||||
const options = {};
|
||||
const allowedOptions = new Set(['--repo', '--source-ref', '--artifact-dir', '--pnpm-cli', '--npm-cli', '--output', '--metadata-dir', '--allow-metadata-preview']);
|
||||
for (let i = 2; i < process.argv.length; i++) {
|
||||
const key = process.argv[i];
|
||||
assert(allowedOptions.has(key), `Unknown option ${key}`);
|
||||
if (key === '--allow-metadata-preview') options[key] = true;
|
||||
else if (key.startsWith('--') && process.argv[i + 1] && !process.argv[i + 1].startsWith('--')) options[key] = process.argv[++i];
|
||||
else throw new Error(`Unexpected argument ${key}`);
|
||||
}
|
||||
const required = ['--repo', '--source-ref', '--artifact-dir', '--pnpm-cli', '--npm-cli', '--output'];
|
||||
for (const key of required) assert(options[key], `Missing ${key}`);
|
||||
assert.match(options['--source-ref'], /^[a-f0-9]{40}$/);
|
||||
const sha = (value) => crypto.createHash('sha256').update(value).digest('hex');
|
||||
const exporterSHA256 = sha(fs.readFileSync(__filename));
|
||||
const installerSHA256 = sha(fs.readFileSync(path.join(__dirname, 'install-bundled-sqlite.cjs')));
|
||||
const verifierSHA256 = sha(fs.readFileSync(path.join(__dirname, 'verify-npm-package.cjs')));
|
||||
const helperNames = ['build-npm-package.cjs', 'install-bundled-sqlite.cjs', 'verify-npm-package.cjs', 'verify-bundled-dependencies.cjs', 'install-panel-dependencies.cjs'];
|
||||
const helperHashes = Object.fromEntries(helperNames.map((name) => [name, sha(fs.readFileSync(path.join(__dirname, name)))]));
|
||||
const output = path.resolve(options['--output']);
|
||||
const parent = fs.realpathSync(path.dirname(output));
|
||||
const temporary = [os.tmpdir(), '/tmp', process.env.RUNNER_TEMP].filter(Boolean).map((folder) => fs.realpathSync(folder));
|
||||
assert(temporary.some((root) => parent === root || parent.startsWith(`${root}${path.sep}`)), 'Output must be inside the temporary directory');
|
||||
assert(!fs.existsSync(output), 'Output must not already exist');
|
||||
fs.mkdirSync(output);
|
||||
fs.writeFileSync(path.join(output, '.qinglong-npm-build-owner.json'), JSON.stringify({ version: 1, id: crypto.randomUUID(), sourceRef: options['--source-ref'] }));
|
||||
const logs = path.join(output, 'logs');
|
||||
fs.mkdirSync(logs);
|
||||
const env = {
|
||||
...process.env,
|
||||
NPM_CONFIG_USERCONFIG: path.join(output, 'public.npmrc'),
|
||||
NPM_CONFIG_GLOBALCONFIG: path.join(output, 'global.npmrc'),
|
||||
NPM_CONFIG_CACHE: path.join(output, 'npm-cache'),
|
||||
PNPM_HOME: path.join(output, 'pnpm-home'),
|
||||
XDG_DATA_HOME: path.join(output, 'xdg-data'),
|
||||
XDG_CACHE_HOME: path.join(output, 'xdg-cache'),
|
||||
XDG_STATE_HOME: path.join(output, 'xdg-state'),
|
||||
};
|
||||
fs.writeFileSync(env.NPM_CONFIG_USERCONFIG, 'registry=https://registry.npmjs.org/\n');
|
||||
fs.writeFileSync(env.NPM_CONFIG_GLOBALCONFIG, '');
|
||||
const run = (command, args, name, cwd = output, input) => {
|
||||
const result = spawnSync(command, args, { cwd, env, input, encoding: input ? undefined : 'utf8', maxBuffer: 128 * 1024 * 1024 });
|
||||
fs.writeFileSync(path.join(logs, `${name}.log`), Buffer.concat([Buffer.from(result.stdout || ''), Buffer.from(result.stderr || '')]));
|
||||
assert(!result.error && result.status === 0, `${name} failed; inspect ${path.join(logs, `${name}.log`)}`);
|
||||
return result.stdout;
|
||||
};
|
||||
const sourceRef = String(run('git', ['-C', path.resolve(options['--repo']), 'rev-parse', `${options['--source-ref']}^{commit}`], 'source-ref')).trim();
|
||||
assert.equal(sourceRef, options['--source-ref']);
|
||||
const treeText = String(run('git', ['-C', path.resolve(options['--repo']), 'ls-tree', '-r', '-z', sourceRef], 'source-tree'));
|
||||
const sourceFiles = [];
|
||||
for (const record of treeText.split('\0').filter(Boolean)) {
|
||||
const [header, relative] = record.split('\t');
|
||||
const [mode, type, object] = header.split(' ');
|
||||
assert(type === 'blob' && ['100644', '100755'].includes(mode), `Unsupported tracked item ${relative}`);
|
||||
assert(relative && !path.isAbsolute(relative) && !relative.split('/').includes('..'));
|
||||
sourceFiles.push({ path: relative, mode, gitBlob: object });
|
||||
}
|
||||
const checkout = path.join(output, 'source');
|
||||
fs.mkdirSync(checkout);
|
||||
const archive = spawnSync('git', ['-C', path.resolve(options['--repo']), 'archive', '--format=tar', sourceRef], { maxBuffer: 128 * 1024 * 1024 });
|
||||
assert(!archive.error && archive.status === 0);
|
||||
run('tar', ['-xf', '-', '-C', checkout], 'source-archive-extract', output, archive.stdout);
|
||||
for (const item of sourceFiles) {
|
||||
const bytes = fs.readFileSync(path.join(checkout, item.path));
|
||||
const blob = crypto.createHash('sha1').update(Buffer.concat([Buffer.from(`blob ${bytes.length}\0`), bytes])).digest('hex');
|
||||
assert.equal(blob, item.gitBlob, `Source blob changed ${item.path}`);
|
||||
}
|
||||
const originalPackage = JSON.parse(fs.readFileSync(path.join(checkout, 'package.json')));
|
||||
const helperProvenance = [];
|
||||
for (const name of helperNames) {
|
||||
const actual = fs.readFileSync(path.join(__dirname, name));
|
||||
const tracked = path.join(checkout, 'scripts', name);
|
||||
const sameAsGit = fs.existsSync(tracked) && fs.readFileSync(tracked).equals(actual);
|
||||
assert(sameAsGit || options['--allow-metadata-preview'], `Helper differs from Git source: ${name}`);
|
||||
helperProvenance.push({ path: `scripts/${name}`, gitSourceSHA256: fs.existsSync(tracked) ? sha(fs.readFileSync(tracked)) : null, archiveHelperSHA256: sha(actual), matchesGitSource: sameAsGit });
|
||||
}
|
||||
const metadata = options['--metadata-dir'] ? fs.realpathSync(options['--metadata-dir']) : checkout;
|
||||
const production = path.join(output, 'production');
|
||||
fs.mkdirSync(production);
|
||||
const metadataChanges = [];
|
||||
for (const name of ['package.json', 'pnpm-lock.yaml', '.npmrc']) {
|
||||
const baseFile = path.join(checkout, name);
|
||||
const inputFile = path.join(metadata, name);
|
||||
const next = fs.readFileSync(inputFile);
|
||||
const before = fs.existsSync(baseFile) ? fs.readFileSync(baseFile) : null;
|
||||
if (!before || !before.equals(next)) {
|
||||
assert(options['--allow-metadata-preview'], `Metadata differs from Git source: ${name}`);
|
||||
metadataChanges.push({ path: name, gitSourceSHA256: before ? sha(before) : null, previewSHA256: sha(next) });
|
||||
}
|
||||
fs.writeFileSync(path.join(production, name), next);
|
||||
}
|
||||
const packageInput = JSON.parse(fs.readFileSync(path.join(production, 'package.json')));
|
||||
assert.equal(packageInput.name, '@whyour/qinglong');
|
||||
assert.equal(packageInput.name, originalPackage.name);
|
||||
assert.equal(packageInput.version, originalPackage.version);
|
||||
assert(!packageInput.dependencies['@ant-design/plots'], 'Frontend plots must be development-only');
|
||||
assert(!packageInput.overrides, 'Use the single pnpm override source');
|
||||
assert.deepEqual(packageInput.pnpm.overrides, originalPackage.pnpm.overrides, 'Preview must retain the reviewed source override map');
|
||||
for (const line of fs.readFileSync(path.join(production, '.npmrc'), 'utf8').split(/\r?\n/).filter(Boolean)) {
|
||||
assert(/^(strict-peer-dependencies|strict-dep-builds)=(true|false)$/.test(line), 'Unexpected project npm configuration');
|
||||
}
|
||||
const artifact = fs.realpathSync(options['--artifact-dir']);
|
||||
const buildInfoBytes = fs.readFileSync(path.join(artifact, 'build-info.json'));
|
||||
const buildInfo = JSON.parse(buildInfoBytes);
|
||||
assert.equal(buildInfo.sourceCommit, sourceRef);
|
||||
assert.equal(buildInfo.dirty, false);
|
||||
assert.equal(buildInfo.lockfileSha256, sha(fs.readFileSync(path.join(checkout, 'pnpm-lock.yaml'))));
|
||||
assert(buildInfo.files && Object.keys(buildInfo.files).length > 0);
|
||||
const walkFiles = (folder, relative = '') => {
|
||||
const result = [];
|
||||
for (const item of fs.readdirSync(folder, { withFileTypes: true })) {
|
||||
const next = path.join(relative, item.name);
|
||||
assert(!item.isSymbolicLink(), `Unexpected symlink ${next}`);
|
||||
if (item.isDirectory()) result.push(...walkFiles(path.join(folder, item.name), next));
|
||||
else { assert(item.isFile()); result.push(next.split(path.sep).join('/')); }
|
||||
}
|
||||
return result;
|
||||
};
|
||||
const actualArtifactFiles = walkFiles(artifact).filter((name) => name !== 'build-info.json').sort();
|
||||
assert.deepEqual(actualArtifactFiles, Object.keys(buildInfo.files).sort(), 'Static artifact inventory mismatch');
|
||||
for (const [relative, expected] of Object.entries(buildInfo.files)) {
|
||||
assert(relative && !path.isAbsolute(relative) && !relative.split('/').includes('..'));
|
||||
assert.equal(sha(fs.readFileSync(path.join(artifact, relative))), expected, `Static artifact hash mismatch ${relative}`);
|
||||
}
|
||||
const pnpmCLI = fs.realpathSync(options['--pnpm-cli']);
|
||||
const npmCLI = fs.realpathSync(options['--npm-cli']);
|
||||
const pnpmVersion = String(run(process.execPath, [pnpmCLI, '--config.manage-package-manager-versions=false', '--version'], 'pnpm-version', production)).trim();
|
||||
assert(packageInput.packageManager && packageInput.packageManager.split('+')[0] === `pnpm@${pnpmVersion}`, 'pnpm version must match packageManager');
|
||||
const npmVersion = String(run(process.execPath, [npmCLI, '--version'], 'npm-version', production)).trim();
|
||||
assert(Number(npmVersion.split('.')[0]) >= 12, 'Use npm 12+ to retain publication guard checks');
|
||||
const lockSHA256 = sha(fs.readFileSync(path.join(production, 'pnpm-lock.yaml')));
|
||||
run(process.execPath, [pnpmCLI, '--config.manage-package-manager-versions=false', 'install', '--prod', '--frozen-lockfile', '--ignore-scripts', '--store-dir', path.join(output, 'pnpm-store')], 'frozen-production-install', production);
|
||||
assert.equal(sha(fs.readFileSync(path.join(production, 'pnpm-lock.yaml'))), lockSHA256, 'Frozen lock was changed');
|
||||
const requireProduction = createRequire(path.join(production, 'package.json'));
|
||||
const yaml = requireProduction('js-yaml');
|
||||
const preGypRequire = createRequire(requireProduction.resolve('sqlite3/package.json'));
|
||||
const semver = preGypRequire('semver');
|
||||
assert(packageInput.engines?.node && semver.satisfies(process.version, packageInput.engines.node), 'Build Node must satisfy the source package engine');
|
||||
const lock = yaml.load(fs.readFileSync(path.join(production, 'pnpm-lock.yaml'), 'utf8'));
|
||||
assert.equal(Number(lock.lockfileVersion), 9);
|
||||
// Build the full internal CLI from the same verified source. Its tracked npm
|
||||
// lock is an existing compiler lock, not a second production dependency lock.
|
||||
const cliRoot = path.join(checkout, 'cli');
|
||||
run(process.execPath, [npmCLI, 'ci', '--ignore-scripts', '--no-audit', '--no-fund'], 'cli-frozen-compiler-install', cliRoot);
|
||||
run(process.execPath, [path.join(cliRoot, 'scripts', 'build.cjs')], 'cli-full-build', checkout);
|
||||
const cliBuildFiles = walkFiles(path.join(cliRoot, 'dist'));
|
||||
assert(cliBuildFiles.includes('internal/maintenance/upgradeArtifacts.cjs'), 'Dynamic internal CLI CJS artifact missing');
|
||||
const within = (root, target) => target === root || target.startsWith(`${root}${path.sep}`);
|
||||
const resolveInstalled = (from, name, placements) => {
|
||||
for (let folder = from; within(placements ? stage : production, folder); folder = path.dirname(folder)) {
|
||||
const file = path.join(folder, 'node_modules', name, 'package.json');
|
||||
if (placements ? placements.has(path.dirname(file)) : fs.existsSync(file)) return path.dirname(file);
|
||||
if (folder === (placements ? stage : production)) break;
|
||||
}
|
||||
return null;
|
||||
};
|
||||
const fileInventory = (folder) => {
|
||||
const entries = [];
|
||||
const visit = (current, relative = '') => {
|
||||
for (const item of fs.readdirSync(current, { withFileTypes: true }).sort((a, b) => a.name.localeCompare(b.name))) {
|
||||
if (item.name === 'node_modules') continue;
|
||||
const next = path.join(relative, item.name);
|
||||
assert(!['.npmrc', '.git', '.pnpm', '_cacache'].includes(item.name), `Unexpected package payload ${next}`);
|
||||
const target = fs.realpathSync(path.join(current, item.name));
|
||||
assert(within(folder, target), `Package symlink escapes source ${next}`);
|
||||
const stat = fs.statSync(target);
|
||||
if (stat.isDirectory()) visit(target, next);
|
||||
else {
|
||||
assert(stat.isFile() && !next.endsWith('.node'), `Native or unsupported payload ${next}`);
|
||||
entries.push({ path: next.split(path.sep).join('/'), sha256: sha(fs.readFileSync(target)), mode: stat.mode & 0o777 });
|
||||
}
|
||||
}
|
||||
};
|
||||
visit(folder);
|
||||
return entries;
|
||||
};
|
||||
const nodes = new Map();
|
||||
const pending = [];
|
||||
const discover = (folder) => {
|
||||
const real = fs.realpathSync(folder);
|
||||
assert(within(path.join(production, 'node_modules'), real), 'Dependency escapes owned production tree');
|
||||
if (!nodes.has(real)) {
|
||||
const manifestBytes = fs.readFileSync(path.join(real, 'package.json'));
|
||||
const pkg = JSON.parse(manifestBytes);
|
||||
const resolution = lock.packages[`${pkg.name}@${pkg.version}`]?.resolution;
|
||||
assert(resolution && resolution.integrity, `Missing frozen registry integrity: ${pkg.name}@${pkg.version}`);
|
||||
const inventory = fileInventory(real);
|
||||
const node = { key: real, pkg, manifestBytes, inventory, resolution, edges: [], class: 0 };
|
||||
node.base = `${pkg.name}@${pkg.version}:${sha(JSON.stringify(inventory))}`;
|
||||
nodes.set(real, node); pending.push(node);
|
||||
}
|
||||
return nodes.get(real);
|
||||
};
|
||||
const direct = [];
|
||||
for (const [name] of Object.entries(packageInput.dependencies)) {
|
||||
const folder = resolveInstalled(production, name);
|
||||
assert(folder, `Missing production dependency ${name}`);
|
||||
direct.push({ name, target: discover(folder) });
|
||||
}
|
||||
while (pending.length) {
|
||||
const node = pending.shift();
|
||||
for (const category of ['dependencies', 'optionalDependencies', 'peerDependencies']) {
|
||||
for (const [name, spec] of Object.entries(node.pkg[category] || {})) {
|
||||
const folder = resolveInstalled(node.key, name);
|
||||
if (!folder) {
|
||||
const optional = category === 'optionalDependencies' || (category === 'peerDependencies' && node.pkg.peerDependenciesMeta?.[name]?.optional);
|
||||
assert(optional, `Missing required ${category}: ${node.pkg.name} -> ${name}`);
|
||||
continue;
|
||||
}
|
||||
node.edges.push({ category, name, spec, target: discover(folder) });
|
||||
}
|
||||
}
|
||||
}
|
||||
// Partition equivalent package contexts, including their dependency and peer graphs.
|
||||
let groups = new Map();
|
||||
for (const node of nodes.values()) {
|
||||
if (!groups.has(node.base)) groups.set(node.base, groups.size);
|
||||
node.class = groups.get(node.base);
|
||||
}
|
||||
for (let round = 0; round <= nodes.size; round++) {
|
||||
const next = new Map(); const assigned = new Map();
|
||||
for (const node of nodes.values()) {
|
||||
const identity = JSON.stringify([node.base, node.edges.map((edge) => [edge.category, edge.name, edge.target.class])]);
|
||||
if (!next.has(identity)) next.set(identity, next.size);
|
||||
assigned.set(node, next.get(identity));
|
||||
}
|
||||
const stable = [...nodes.values()].every((node) => node.class === assigned.get(node));
|
||||
for (const node of nodes.values()) node.class = assigned.get(node);
|
||||
if (stable) break;
|
||||
assert(round < nodes.size, 'Package context partition did not converge');
|
||||
}
|
||||
const classes = new Map();
|
||||
for (const node of nodes.values()) if (!classes.has(node.class)) classes.set(node.class, node);
|
||||
const stage = path.join(output, 'package');
|
||||
fs.mkdirSync(stage);
|
||||
const stageFiles = [];
|
||||
const copyFile = (from, relative, expected) => {
|
||||
const bytes = fs.readFileSync(from);
|
||||
if (expected) assert.equal(sha(bytes), expected);
|
||||
const to = path.join(stage, relative); fs.mkdirSync(path.dirname(to), { recursive: true }); fs.writeFileSync(to, bytes); fs.chmodSync(to, fs.statSync(from).mode & 0o777);
|
||||
stageFiles.push({ path: relative.split(path.sep).join('/'), sha256: sha(bytes) });
|
||||
};
|
||||
for (const item of sourceFiles) {
|
||||
const relative = item.path;
|
||||
if (['.env.example', 'LICENSE', 'README.md', 'README-en.md', 'SECURITY.md', 'ecosystem.config.js', 'version.yaml'].includes(relative) || relative.startsWith('shell/') || relative.startsWith('sample/') || (relative.startsWith('back/protos/') && relative.endsWith('.proto')) || ['scripts/verify-production-dependencies.cjs', 'scripts/install-runtime-tools.cjs'].includes(relative)) copyFile(path.join(checkout, relative), relative);
|
||||
}
|
||||
for (const [relative, expected] of Object.entries(buildInfo.files)) {
|
||||
copyFile(path.join(artifact, relative), path.join('static', relative), expected);
|
||||
}
|
||||
copyFile(path.join(artifact, 'build-info.json'), 'static/build-info.json', sha(buildInfoBytes));
|
||||
for (const name of helperNames.filter((name) => name !== 'build-npm-package.cjs')) copyFile(path.join(__dirname, name), path.join('scripts', name));
|
||||
for (const relative of cliBuildFiles) if (!relative.endsWith('.js.map')) copyFile(path.join(cliRoot, 'dist', relative), path.join('cli', 'dist', relative));
|
||||
for (const relative of ['package.json', 'LOCAL.md', 'LOCAL.en.md', 'README.md', 'README.en.md', 'src/internal/maintenance/upgradeArtifacts.cjs']) copyFile(path.join(cliRoot, relative), path.join('cli', relative));
|
||||
const rootPackage = structuredClone(packageInput);
|
||||
if (fs.existsSync(path.join(checkout, 'scripts', 'install-runtime-tools.cjs'))) assert(['runtime-tools.tgz', 'runtime-tools-proof.json'].every((name) => Object.hasOwn(buildInfo.files, name)), 'Runtime tools must be covered by the same CI static manifest');
|
||||
let runtimeTools = null;
|
||||
if (Object.hasOwn(buildInfo.files, 'runtime-tools-proof.json')) {
|
||||
const toolProof = JSON.parse(fs.readFileSync(path.join(artifact, 'runtime-tools-proof.json')));
|
||||
const toolArchive = fs.readFileSync(path.join(artifact, 'runtime-tools.tgz'));
|
||||
assert.equal(toolProof.sourceCommit, sourceRef, 'Runtime tools came from another source commit');
|
||||
assert.equal(toolProof.archive.sha256, sha(toolArchive), 'Runtime tools archive differs from its proof');
|
||||
for (const input of toolProof.inputs) assert.equal(sha(fs.readFileSync(path.join(checkout, input.path))), input.sha256, `Runtime tools source input differs from Git: ${input.path}`);
|
||||
const toolFiles = new Map(toolProof.files.map((item) => [item.path, item]));
|
||||
assert.equal(sha(fs.readFileSync(pnpmCLI)), toolFiles.get('tools/node_modules/pnpm/bin/pnpm.cjs')?.sha256, 'Producer pnpm CLI differs from the reviewed runtime artifact');
|
||||
assert.equal(sha(fs.readFileSync(npmCLI)), toolFiles.get('npm/bin/npm-cli.js')?.sha256, 'Producer npm CLI differs from the reviewed runtime artifact');
|
||||
runtimeTools = { proofSHA256: sha(fs.readFileSync(path.join(artifact, 'runtime-tools-proof.json'))), archiveSHA256: sha(toolArchive), sourceCommit: toolProof.sourceCommit, builder: toolProof.builder, components: toolProof.components, residualAdvisories: toolProof.residualAdvisories };
|
||||
}
|
||||
delete rootPackage.pnpm; delete rootPackage.packageManager; delete rootPackage.devDependencies;
|
||||
const rootMetadataTransforms = [];
|
||||
for (const edge of direct) {
|
||||
const target = edge.target.pkg;
|
||||
const exact = target.name === edge.name ? target.version : `npm:${target.name}@${target.version}`;
|
||||
if (rootPackage.dependencies[edge.name] !== exact) rootMetadataTransforms.push({ category: 'dependencies', dependency: edge.name, from: rootPackage.dependencies[edge.name], to: exact, source: 'frozen direct pnpm resolution' });
|
||||
rootPackage.dependencies[edge.name] = exact;
|
||||
}
|
||||
rootPackage.bundleDependencies = Object.keys(rootPackage.dependencies);
|
||||
rootPackage.scripts.install = 'node scripts/install-bundled-sqlite.cjs';
|
||||
fs.writeFileSync(path.join(stage, 'package.json'), `${JSON.stringify(rootPackage, null, 2)}\n`);
|
||||
const rootMap = new Map();
|
||||
for (const edge of direct) rootMap.set(edge.name, edge.target.class);
|
||||
for (const node of nodes.values()) for (const edge of node.edges) if (!rootMap.has(edge.name)) rootMap.set(edge.name, edge.target.class);
|
||||
const placements = new Map(); const placementQueue = []; const transforms = []; const shipped = [];
|
||||
const permittedOverride = (edge) => {
|
||||
for (const [key, replacement] of Object.entries(packageInput.pnpm.overrides)) {
|
||||
const index = key.lastIndexOf('@'); const hasSelector = index > 0;
|
||||
const name = hasSelector ? key.slice(0, index) : key;
|
||||
const selector = hasSelector ? key.slice(index + 1) : null;
|
||||
if (name !== edge.name) continue;
|
||||
if (selector && (!semver.validRange(edge.spec) || !semver.intersects(edge.spec, selector))) continue;
|
||||
const target = edge.target.pkg;
|
||||
const expected = target.name === edge.name ? target.version : `npm:${target.name}@${target.version}`;
|
||||
if (replacement === expected) return { key, replacement };
|
||||
}
|
||||
return null;
|
||||
};
|
||||
const matches = (name, spec, target) => {
|
||||
if (spec.startsWith('npm:')) return spec === `npm:${target.name}@${target.version}`;
|
||||
return name === target.name && !!semver.validRange(spec) && semver.satisfies(target.version, spec);
|
||||
};
|
||||
const place = (folder, classID) => {
|
||||
assert(!placements.has(folder));
|
||||
const node = classes.get(classID); placements.set(folder, node); placementQueue.push(folder);
|
||||
fs.mkdirSync(folder, { recursive: true });
|
||||
for (const item of node.inventory) {
|
||||
const from = path.join(node.key, item.path); const to = path.join(folder, item.path);
|
||||
fs.mkdirSync(path.dirname(to), { recursive: true });fs.copyFileSync(from, to);fs.chmodSync(to, item.mode);
|
||||
assert.equal(sha(fs.readFileSync(to)), item.sha256);
|
||||
}
|
||||
const pkg = structuredClone(node.pkg); const edits = [];
|
||||
for (const edge of node.edges) if (!matches(edge.name, edge.spec, edge.target.pkg)) {
|
||||
const override = permittedOverride(edge);
|
||||
assert(override, `Unverified dependency mismatch ${pkg.name}@${pkg.version} ${edge.name}: ${edge.spec} -> ${edge.target.pkg.name}@${edge.target.pkg.version}`);
|
||||
const next = edge.target.pkg.name === edge.name ? edge.target.pkg.version : `npm:${edge.target.pkg.name}@${edge.target.pkg.version}`;
|
||||
pkg[edge.category][edge.name] = next;edits.push({ category: edge.category, dependency: edge.name, from: edge.spec, to: next, pnpmOverride: override });
|
||||
}
|
||||
const manifestPath = path.join(folder, 'package.json');
|
||||
if (edits.length) {
|
||||
const after = `${JSON.stringify(pkg, null, 2)}\n`;fs.writeFileSync(manifestPath, after);
|
||||
transforms.push({ path: path.relative(stage, manifestPath), package: pkg.name, version: pkg.version, beforeSHA256: sha(node.manifestBytes), afterSHA256: sha(after), edits });
|
||||
}
|
||||
shipped.push({ path: path.relative(stage, folder), package: pkg.name, version: pkg.version, context: classID, sourcePath: path.relative(production, node.key), registryIntegrity: node.resolution.integrity, sourceFiles: node.inventory, publishedManifestSHA256: sha(fs.readFileSync(manifestPath)) });
|
||||
};
|
||||
for (const [name, classID] of rootMap) place(path.join(stage, 'node_modules', name), classID);
|
||||
while (placementQueue.length) {
|
||||
const folder = placementQueue.shift(); const node = placements.get(folder);
|
||||
for (const edge of node.edges) {
|
||||
const found = resolveInstalled(folder, edge.name, placements);
|
||||
if (!found || placements.get(found).class !== edge.target.class) place(path.join(folder, 'node_modules', edge.name), edge.target.class);
|
||||
}
|
||||
}
|
||||
for (const [folder, node] of placements) for (const edge of node.edges) {
|
||||
const resolved = resolveInstalled(folder, edge.name, placements);
|
||||
assert(resolved && placements.get(resolved).class === edge.target.class, 'Exported dependency resolution differs from frozen source');
|
||||
const pkg = JSON.parse(fs.readFileSync(path.join(folder, 'package.json')));
|
||||
assert(matches(edge.name, pkg[edge.category][edge.name], edge.target.pkg), 'Exported manifest dependency remains invalid');
|
||||
}
|
||||
for (const item of shipped) {
|
||||
const folder = path.join(stage, item.path);
|
||||
const node = placements.get(folder);
|
||||
const pkg = JSON.parse(fs.readFileSync(path.join(folder, 'package.json')));
|
||||
item.edges = node.edges.map((edge) => ({ category: edge.category, name: edge.name, publishedSpec: pkg[edge.category][edge.name], context: edge.target.class, resolvedPath: path.relative(stage, resolveInstalled(folder, edge.name, placements)) }));
|
||||
}
|
||||
const receipt = {
|
||||
version: 1, package: rootPackage.name, packageVersion: rootPackage.version, sourceCommit: sourceRef,
|
||||
sourceArchiveSHA256: sha(archive.stdout), sourceFiles, sourceMetadataChanges: metadataChanges,
|
||||
previewMetadata: !!metadataChanges.length || helperProvenance.some((item) => !item.matchesGitSource), helperProvenance, productionManifestSHA256: sha(fs.readFileSync(path.join(production, 'package.json'))), productionLockSHA256: lockSHA256,
|
||||
ciBuildInfoSHA256: sha(buildInfoBytes), ciStaticSourceCommit: buildInfo.sourceCommit, ciStaticFilesVerified: actualArtifactFiles.length,
|
||||
fullInternalCli: { sourceCommit: sourceRef, compilerLockSHA256: sha(fs.readFileSync(path.join(cliRoot, 'package-lock.json'))), fullBuiltFiles: cliBuildFiles.length },
|
||||
tools: { node: process.version, pnpm: pnpmVersion, npm: npmVersion, pnpmCliSHA256: sha(fs.readFileSync(pnpmCLI)), npmCliSHA256: sha(fs.readFileSync(npmCLI)) }, runtimeTools,
|
||||
exporterSHA256, nativeInstallerSHA256: installerSHA256, verifierSHA256, helperHashes,
|
||||
publishedRootManifestSHA256: sha(fs.readFileSync(path.join(stage, 'package.json'))), rootMetadataTransforms,
|
||||
directDependencies: direct.map((edge) => ({ name: edge.name, publishedSpec: rootPackage.dependencies[edge.name], context: edge.target.class, resolvedPath: path.relative(stage, resolveInstalled(stage, edge.name, placements)) })),
|
||||
runtimeToolsIncluded: ['static/runtime-tools.tgz', 'static/runtime-tools-proof.json', 'scripts/install-runtime-tools.cjs'].every((name) => stageFiles.some((file) => file.path === name)),
|
||||
packageContexts: classes.size, shippedPackagePlacements: shipped.length, originalRegistryTreeUnmodified: !transforms.length,
|
||||
transforms, shipped, applicationFiles: stageFiles,
|
||||
};
|
||||
fs.writeFileSync(path.join(stage, 'qinglong-npm-dependency-proof.json'), `${JSON.stringify(receipt, null, 2)}\n`);
|
||||
const artifacts = path.join(output, 'artifacts');fs.mkdirSync(artifacts);
|
||||
const unpackJSON = (text) => {
|
||||
const value = JSON.parse(text);
|
||||
return Array.isArray(value) ? value : Object.values(value);
|
||||
};
|
||||
// npm 12 sanitizes UUID-looking filenames in JSON (for example dotenv's .tap
|
||||
// processinfo). The tar itself is authoritative, never the display-only list.
|
||||
const planDirectory = path.join(output, 'plan-artifact');fs.mkdirSync(planDirectory);
|
||||
const planned = unpackJSON(String(run(process.execPath, [npmCLI, 'pack', '--ignore-scripts', '--json', '--pack-destination', planDirectory], 'npm-pack-plan', stage)));
|
||||
assert.equal(planned.length, 1);
|
||||
const archivePaths = (file, label) => String(run('tar', ['-tzf', file], label)).split('\n').filter(Boolean).map((name) => {
|
||||
assert(name.startsWith('package/') && !name.split('/').includes('..'), 'Unsafe archive path');
|
||||
return name.slice('package/'.length);
|
||||
});
|
||||
const plannedPaths = new Set(archivePaths(path.join(planDirectory, planned[0].filename), 'npm-plan-tar-inventory'));
|
||||
for (const item of shipped) {
|
||||
item.shippedFiles = item.sourceFiles.filter((file) => plannedPaths.has(`${item.path}/${file.path}`));
|
||||
item.omittedByNpmPack = item.sourceFiles.filter((file) => !plannedPaths.has(`${item.path}/${file.path}`));
|
||||
assert(plannedPaths.has(`${item.path}/package.json`), `Bundled manifest missing ${item.path}`);
|
||||
}
|
||||
for (const item of stageFiles) assert(plannedPaths.has(item.path), `Application file omitted by npm pack: ${item.path}`);
|
||||
receipt.publishedFilePaths = [...plannedPaths].sort();
|
||||
fs.writeFileSync(path.join(stage, 'qinglong-npm-dependency-proof.json'), `${JSON.stringify(receipt, null, 2)}\n`);
|
||||
const packText = String(run(process.execPath, [npmCLI, 'pack', '--ignore-scripts', '--json', '--pack-destination', artifacts], 'npm-pack', stage));
|
||||
const packed = unpackJSON(packText);
|
||||
assert.equal(packed.length, 1);
|
||||
assert.equal(packed[0].name, rootPackage.name);assert.equal(packed[0].version, rootPackage.version);
|
||||
const tarball = path.join(artifacts, packed[0].filename);
|
||||
const actualPublishedPaths = archivePaths(tarball, 'npm-final-tar-inventory');
|
||||
assert.equal(actualPublishedPaths.length, new Set(actualPublishedPaths).size, 'Duplicate archive members');
|
||||
assert.deepEqual(actualPublishedPaths.sort(), receipt.publishedFilePaths, 'Actual archive differs from planned archive inventory');
|
||||
for (const relative of actualPublishedPaths) assert(!relative.endsWith('.node') && !relative.split('/').some((part) => ['.npmrc', '.git', '.pnpm', '_cacache', '.qinglong-npm-build-owner.json'].includes(part)), `Forbidden archive payload ${relative}`);
|
||||
assert.equal(sha(fs.readFileSync(__filename)), exporterSHA256, 'Exporter source changed during build');
|
||||
assert.equal(sha(fs.readFileSync(path.join(__dirname, 'install-bundled-sqlite.cjs'))), installerSHA256, 'Installer source changed during build');
|
||||
assert.equal(sha(fs.readFileSync(path.join(__dirname, 'verify-npm-package.cjs'))), verifierSHA256, 'Verifier source changed during build');
|
||||
for (const name of helperNames) assert.equal(sha(fs.readFileSync(path.join(__dirname, name))), helperHashes[name], `Helper changed during build: ${name}`);
|
||||
fs.writeFileSync(path.join(output, 'npm-pack.json'), `${JSON.stringify(packed, null, 2)}\n`);
|
||||
fs.writeFileSync(path.join(output, 'package-build-summary.json'), `${JSON.stringify({ ...receipt, tarball: { filename: packed[0].filename, sha256: sha(fs.readFileSync(tarball)), bytes: fs.statSync(tarball).size, files: packed[0].entryCount } }, null, 2)}\n`);
|
||||
console.log(JSON.stringify({ output, tarball, sourceCommit: sourceRef, lockSHA256, ciStaticFilesVerified: actualArtifactFiles.length, shippedPackages: shipped.length, transforms: transforms.length }));
|
||||
@@ -0,0 +1,34 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
scripts_dir=$(cd -- "$(dirname -- "$0")" && pwd)
|
||||
payload_dir="$scripts_dir/runtime-tools"
|
||||
output_dir=${1:?Pass a fresh output directory}
|
||||
case "$output_dir" in /*) ;; *) echo 'Output directory must be absolute' >&2; exit 1 ;; esac
|
||||
if [[ -e "$output_dir" ]]; then echo 'Output directory already exists' >&2; exit 1; fi
|
||||
if [[ "$(node --version)" != v22.23.3 ]]; then echo 'Build requires Node22.23.3' >&2; exit 1; fi
|
||||
if [[ "$(uname -s)" != Linux && "${QL_TOOLS_ALLOW_HOST_SMOKE:-0}" != 1 ]]; then echo 'Release artifact must be built on Linux' >&2; exit 1; fi
|
||||
source_commit=${QL_TOOLS_SOURCE_COMMIT:?Set the Qinglong source commit}
|
||||
if [[ ! "$source_commit" =~ ^[0-9a-f]{40}$ ]]; then echo 'Source commit must be a full Git SHA' >&2; exit 1; fi
|
||||
node "$scripts_dir/verify-runtime-tools-source.cjs" "$payload_dir" "$source_commit"
|
||||
build_dir=$(mktemp -d /tmp/qinglong-runtime-build.XXXXXX)
|
||||
build_dir=$(cd -- "$build_dir" && pwd -P)
|
||||
cleanup() { rm -rf -- "$build_dir"; }
|
||||
trap cleanup EXIT
|
||||
bootstrap_npm=${QL_TOOLS_BOOTSTRAP_NPM:-"$(dirname -- "$(command -v node)")/../lib/node_modules/npm/bin/npm-cli.js"}
|
||||
export QL_TOOLS_BOOTSTRAP_NPM="$bootstrap_npm"
|
||||
export QL_TOOLS_NPM_CACHE=${QL_TOOLS_NPM_CACHE:-"$build_dir/npm-cache"}
|
||||
export NODE_COMPILE_CACHE="$build_dir/compile-cache"
|
||||
export NPM_CONFIG_GLOBALCONFIG=/dev/null
|
||||
bash "$payload_dir/npm/install-preview.sh" "$build_dir/npm"
|
||||
export QL_TOOLS_BOOTSTRAP_NPM="$build_dir/npm/bin/npm-cli.js"
|
||||
bash "$payload_dir/pnpm-source/build-preview.sh" "$build_dir/pnpm-build"
|
||||
mkdir "$build_dir/tools-payload"
|
||||
cp -- "$payload_dir/tools/package.json" "$payload_dir/tools/package-lock.json" \
|
||||
"$payload_dir/tools/public.npmrc" "$payload_dir/tools/install-preview.sh" "$build_dir/tools-payload/"
|
||||
cp -- "$build_dir/pnpm-build/pnpm-10.34.6-qinglong-security-linux.tgz" "$build_dir/tools-payload/"
|
||||
bash "$build_dir/tools-payload/install-preview.sh" "$build_dir/tools"
|
||||
mkdir "$build_dir/tools/bin"
|
||||
cp -- "$payload_dir/pnpm-wrapper.cjs" "$build_dir/tools/bin/pnpm.cjs"
|
||||
chmod 755 "$build_dir/tools/bin/pnpm.cjs"
|
||||
node "$scripts_dir/pack-runtime-tools.cjs" --npm "$build_dir/npm" --tools "$build_dir/tools" \
|
||||
--payload "$payload_dir" --output "$output_dir" --source-commit "$source_commit"
|
||||
Executable
+32
@@ -0,0 +1,32 @@
|
||||
#!/usr/bin/env node
|
||||
'use strict';
|
||||
const fs = require('node:fs');
|
||||
const path = require('node:path');
|
||||
const { createRequire } = require('node:module');
|
||||
const { spawnSync } = require('node:child_process');
|
||||
const rootManifest = path.join(__dirname, '..', 'package.json');
|
||||
const rootPackage = JSON.parse(fs.readFileSync(rootManifest));
|
||||
const rootRequire = createRequire(rootManifest);
|
||||
const sqliteManifest = rootRequire.resolve('sqlite3/package.json');
|
||||
const sqlitePackage = JSON.parse(fs.readFileSync(sqliteManifest));
|
||||
if (sqlitePackage.name !== '@whyour/sqlite3' || rootPackage.dependencies.sqlite3 !== `npm:${sqlitePackage.name}@${sqlitePackage.version}`) throw new Error('Unexpected bundled SQLite identity');
|
||||
const forceSource = /^(true|1)$/i.test(String(process.env.npm_config_build_from_source || process.env.NPM_CONFIG_BUILD_FROM_SOURCE || ''));
|
||||
let needsInstall = forceSource;
|
||||
try { rootRequire('sqlite3'); }
|
||||
catch (error) {
|
||||
if (!/Cannot find module|Could not locate the bindings file|dlopen|invalid ELF|not a valid Win32|wrong architecture/.test(String(error))) throw error;
|
||||
needsInstall = true;
|
||||
}
|
||||
if (needsInstall) {
|
||||
const sqliteRequire = createRequire(sqliteManifest);
|
||||
const installer = sqliteRequire.resolve('@mapbox/node-pre-gyp/bin/node-pre-gyp');
|
||||
const nodeGyp = sqliteRequire.resolve('node-gyp/bin/node-gyp.js');
|
||||
const installed = spawnSync(process.execPath, [installer, 'install', '--fallback-to-build'], { cwd: path.dirname(sqliteManifest), stdio: 'inherit', env: { ...process.env, npm_config_node_gyp: nodeGyp } });
|
||||
if (installed.error) throw installed.error;
|
||||
if (installed.status !== 0) process.exit(installed.status || 1);
|
||||
rootRequire('sqlite3');
|
||||
}
|
||||
const binding = Object.keys(require.cache).find((file) => file.endsWith('.node') && file.startsWith(`${path.dirname(sqliteManifest)}${path.sep}`));
|
||||
if (!binding) throw new Error('SQLite native binding did not load');
|
||||
const sha256 = require('node:crypto').createHash('sha256').update(fs.readFileSync(binding)).digest('hex');
|
||||
console.log(JSON.stringify({ bundledSqlite: sqlitePackage.version, platform: process.platform, arch: process.arch, forceSource, installation: forceSource ? 'source-built' : needsInstall ? 'installer-ran' : 'existing-binding', binding: path.relative(path.join(__dirname, '..'), binding), bindingSHA256: sha256 }));
|
||||
Executable
+94
@@ -0,0 +1,94 @@
|
||||
#!/usr/bin/env node
|
||||
'use strict';
|
||||
const fs = require('node:fs');
|
||||
const path = require('node:path');
|
||||
const assert = require('node:assert/strict');
|
||||
const crypto = require('node:crypto');
|
||||
const os = require('node:os');
|
||||
const { spawnSync } = require('node:child_process');
|
||||
const { verifyBundledDependencies } = require('./verify-bundled-dependencies.cjs');
|
||||
const options = {};
|
||||
for (let i = 2; i < process.argv.length; i += 2) {
|
||||
assert(['--root', '--pnpm-cli', '--static-dir', '--source-commit'].includes(process.argv[i]) && process.argv[i + 1], 'Expected a supported option with a value');
|
||||
options[process.argv[i]] = process.argv[i + 1];
|
||||
}
|
||||
const root = fs.realpathSync(options['--root'] || path.join(__dirname, '..'));
|
||||
let runEnvironment = { ...process.env };
|
||||
const run = (command, args, capture = false) => {
|
||||
const result = spawnSync(command, args, { cwd: root, env: runEnvironment, stdio: capture ? 'pipe' : 'inherit', encoding: capture ? 'utf8' : undefined });
|
||||
if (result.error) throw result.error;
|
||||
assert.equal(result.status, 0, `Panel dependency command failed: ${command}`);
|
||||
return capture ? result.stdout.trim() : undefined;
|
||||
};
|
||||
if (fs.existsSync(path.join(root, 'qinglong-npm-dependency-proof.json'))) {
|
||||
const verified = verifyBundledDependencies(root);
|
||||
run(process.execPath, [path.join(root, 'scripts', 'install-bundled-sqlite.cjs')]);
|
||||
verifyBundledDependencies(root);
|
||||
console.log(JSON.stringify({ mode: 'bundled', resolverInstallSkipped: true, verifiedPlacements: verified.verifiedPlacements, verifiedEdges: verified.verifiedEdges }));
|
||||
} else {
|
||||
const lock = path.join(root, 'pnpm-lock.yaml');
|
||||
assert(fs.existsSync(lock), 'Panel source has no frozen pnpm lock; refusing an npm re-resolution');
|
||||
const pkg = JSON.parse(fs.readFileSync(path.join(root, 'package.json')));
|
||||
assert(pkg.engines?.node, 'Panel source must declare its supported Node engine');
|
||||
assert.equal(pkg.packageManager?.split('+')[0], 'pnpm@10.34.6', 'Panel source requires pinned pnpm@10.34.6; install its reviewed runtime archive');
|
||||
const hash = (file) => crypto.createHash('sha256').update(fs.readFileSync(file)).digest('hex');
|
||||
const before = hash(lock);
|
||||
let cli = options['--pnpm-cli'] || process.env.QINGLONG_PNPM_CLI;
|
||||
let command = cli ? process.execPath : 'pnpm';
|
||||
let prefix = cli ? [fs.realpathSync(cli)] : [];
|
||||
const version = spawnSync(command, [...prefix, '--config.manage-package-manager-versions=false', '--version'], { cwd: root, env: process.env, encoding: 'utf8' });
|
||||
let temporary;
|
||||
let owner;
|
||||
let runtimeArchive = null;
|
||||
try {
|
||||
if (version.error || version.status !== 0 || version.stdout.trim() !== '10.34.6') {
|
||||
const candidates = [options['--static-dir'], path.join(root, 'static'), path.basename(root) === 'source' ? path.join(path.dirname(root), 'static') : null].filter(Boolean);
|
||||
const selected = candidates.find((directory) => ['build-info.json', 'runtime-tools.tgz', 'runtime-tools-proof.json'].every((name) => fs.existsSync(path.join(directory, name))));
|
||||
assert(selected, 'Active pnpm is not 10.34.6 and no controlled runtime artifact exists; refusing an unfrozen npm fallback');
|
||||
const staticRoot = fs.realpathSync(selected);
|
||||
const build = JSON.parse(fs.readFileSync(path.join(staticRoot, 'build-info.json')));
|
||||
const proof = JSON.parse(fs.readFileSync(path.join(staticRoot, 'runtime-tools-proof.json')));
|
||||
assert.equal(build.dirty, false); assert.match(build.sourceCommit, /^[a-f0-9]{40}$/);
|
||||
assert.equal(build.sourceCommit, proof.sourceCommit, 'Source/static runtime commits differ');
|
||||
assert.equal(build.lockfileSha256, before, 'Source and static frozen lock hashes differ');
|
||||
let sourceCommit = options['--source-commit'];
|
||||
if (fs.existsSync(path.join(root, '.git'))) {
|
||||
const head = run('git', ['rev-parse', 'HEAD'], true);
|
||||
if (sourceCommit) assert.equal(sourceCommit, head, 'Explicit source commit differs from Git HEAD');
|
||||
sourceCommit = head;
|
||||
}
|
||||
assert(sourceCommit, 'Archive source requires --source-commit from the verified upgrade download');
|
||||
assert.equal(build.sourceCommit, sourceCommit, 'Runtime artifact is not from the verified source commit');
|
||||
for (const name of ['runtime-tools.tgz', 'runtime-tools-proof.json']) assert.equal(hash(path.join(staticRoot, name)), build.files[name], `Static runtime tool hash differs: ${name}`);
|
||||
for (const input of proof.inputs) {
|
||||
assert(input.path && !path.isAbsolute(input.path) && !input.path.split(/[\\/]/).includes('..'), 'Unsafe runtime input path');
|
||||
const file = path.join(root, input.path);
|
||||
assert(fs.realpathSync(file).startsWith(`${root}${path.sep}`), 'Runtime source input escapes source root');
|
||||
assert.equal(hash(file), input.sha256, `Runtime tool input differs from verified source: ${input.path}`);
|
||||
}
|
||||
const installer = path.join(__dirname, 'install-runtime-tools.cjs');
|
||||
const installerInput = proof.inputs.find((input) => input.path === 'scripts/install-runtime-tools.cjs');
|
||||
assert(installerInput && hash(installer) === installerInput.sha256, 'Use the installed, reviewed runtime archive verifier');
|
||||
temporary = fs.mkdtempSync(path.join(os.tmpdir(), 'qinglong-source-tools-'));
|
||||
owner = Buffer.from(JSON.stringify({ id: crypto.randomUUID(), sourceCommit }));
|
||||
fs.writeFileSync(path.join(temporary, '.qinglong-source-tools-owner.json'), owner);
|
||||
const toolPrefix = path.join(temporary, 'prefix');
|
||||
runEnvironment = { ...runEnvironment, NPM_CONFIG_USERCONFIG: path.join(temporary, 'public.npmrc'), npm_config_userconfig: path.join(temporary, 'public.npmrc'), NPM_CONFIG_GLOBALCONFIG: path.join(temporary, 'global.npmrc'), npm_config_globalconfig: path.join(temporary, 'global.npmrc'), NPM_CONFIG_CACHE: path.join(temporary, 'npm-cache'), npm_config_cache: path.join(temporary, 'npm-cache'), PNPM_HOME: path.join(temporary, 'pnpm-home'), XDG_CACHE_HOME: path.join(temporary, 'xdg-cache'), XDG_STATE_HOME: path.join(temporary, 'xdg-state'), XDG_DATA_HOME: path.join(temporary, 'xdg-data') };
|
||||
fs.writeFileSync(runEnvironment.NPM_CONFIG_USERCONFIG, 'registry=https://registry.npmjs.org/\n'); fs.writeFileSync(runEnvironment.NPM_CONFIG_GLOBALCONFIG, '');
|
||||
run(process.execPath, [installer, '--archive', path.join(staticRoot, 'runtime-tools.tgz'), '--proof', path.join(staticRoot, 'runtime-tools-proof.json'), '--prefix', toolPrefix]);
|
||||
const gyp = path.join(toolPrefix, 'lib', 'node_modules', 'qinglong-runtime-tools', 'node_modules', 'node-gyp', 'bin', 'node-gyp.js');
|
||||
runEnvironment = { ...runEnvironment, PATH: [path.join(toolPrefix, 'bin'), path.dirname(process.execPath), runEnvironment.PATH || ''].join(path.delimiter), npm_config_node_gyp: gyp, NPM_CONFIG_NODE_GYP: gyp };
|
||||
cli = path.join(toolPrefix, 'lib', 'node_modules', 'qinglong-runtime-tools', 'node_modules', 'pnpm', 'bin', 'pnpm.cjs');
|
||||
command = process.execPath; prefix = [cli];
|
||||
assert.equal(run(command, [...prefix, '--config.manage-package-manager-versions=false', '--version'], true), '10.34.6');
|
||||
runtimeArchive = { sourceCommit, sha256: proof.archive.sha256, prefixScope: 'fresh temporary only' };
|
||||
}
|
||||
const install = [...prefix, '--config.manage-package-manager-versions=false', '--config.engine-strict=true', 'install', '--prod', '--frozen-lockfile'];
|
||||
if (temporary) install.push('--store-dir', path.join(temporary, 'pnpm-store'));
|
||||
run(command, install);
|
||||
assert.equal(hash(lock), before, 'Frozen production installation changed the lock');
|
||||
console.log(JSON.stringify({ mode: 'source', frozen: true, productionLockSHA256: before, pnpm: '10.34.6', runtimeArchive }));
|
||||
} finally {
|
||||
if (temporary) { assert(fs.readFileSync(path.join(temporary, '.qinglong-source-tools-owner.json')).equals(owner), 'Temporary runtime prefix ownership changed'); fs.rmSync(temporary, { recursive: true }); }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,198 @@
|
||||
#!/usr/bin/env node
|
||||
'use strict';
|
||||
const fs = require('node:fs');
|
||||
const path = require('node:path');
|
||||
const crypto = require('node:crypto');
|
||||
const zlib = require('node:zlib');
|
||||
const { createRequire } = require('node:module');
|
||||
const { spawnSync } = require('node:child_process');
|
||||
const options = {};
|
||||
for (let index = 2; index < process.argv.length; index++) {
|
||||
const name = process.argv[index];
|
||||
if (['--verify-only', '--allow-host-smoke'].includes(name)) options[name.slice(2)] = true;
|
||||
else if (['--archive', '--proof', '--prefix'].includes(name) && process.argv[index + 1]) options[name.slice(2)] = process.argv[++index];
|
||||
else throw new Error('Unknown or incomplete argument: ' + name);
|
||||
}
|
||||
const prefix = options.prefix || '/usr/local';
|
||||
const archivePath = options.archive || path.resolve(__dirname, '../static/runtime-tools.tgz');
|
||||
const proofPath = options.proof || path.resolve(__dirname, '../static/runtime-tools-proof.json');
|
||||
for (const [name, value] of Object.entries({ prefix, archive: archivePath, proof: proofPath })) if (!path.isAbsolute(value)) throw new Error(name + ' must be absolute');
|
||||
if (prefix === '/') throw new Error('A filesystem root is not an installation prefix');
|
||||
const [major, minor, patch] = process.versions.node.split('.').map(Number);
|
||||
if (!((major === 22 && (minor > 22 || minor === 22 && patch >= 2)) || major === 24 && minor >= 15 || major >= 26)) throw new Error('Node ^22.22.2 || ^24.15.0 || >=26.0.0 is required');
|
||||
const proof = JSON.parse(fs.readFileSync(proofPath, 'utf8'));
|
||||
if (proof.schemaVersion !== 1 || !/^[0-9a-f]{40}$/.test(proof.sourceCommit) || !Array.isArray(proof.files)) throw new Error('Invalid runtime tools proof');
|
||||
if (!options['allow-host-smoke'] && (process.platform !== 'linux' || !proof.builder.releaseBuild || !proof.sourceAttestation?.verified || proof.sourceAttestation.head !== proof.sourceCommit || !proof.supportedPlatforms.includes('linux'))) throw new Error('This archive is a Linux release artifact; host smoke must be explicit');
|
||||
const hash = (buffer, algorithm = 'sha256', encoding = 'hex') => crypto.createHash(algorithm).update(buffer).digest(encoding);
|
||||
const archive = fs.readFileSync(archivePath);
|
||||
if (archive.length !== proof.archive.bytes || hash(archive) !== proof.archive.sha256 || 'sha512-' + hash(archive, 'sha512', 'base64') !== proof.archive.integrity) throw new Error('Runtime archive integrity mismatch');
|
||||
const expectedEmbedded = { ...proof }; delete expectedEmbedded.archive;
|
||||
const embeddedBytes = Buffer.from(JSON.stringify(expectedEmbedded, null, 2) + '\n');
|
||||
function safeRelative(value) {
|
||||
return typeof value === 'string' && value.length > 0 && !value.includes('\0') && !value.includes('\n') && !value.includes('\\') && !path.posix.isAbsolute(value) && value.split('/').every(part => part && part !== '.' && part !== '..');
|
||||
}
|
||||
const records = new Map();
|
||||
for (const record of proof.files) {
|
||||
if (!safeRelative(record.path) || !/^(npm|tools)\//.test(record.path) || records.has(record.path)) throw new Error('Unsafe or duplicate proof path');
|
||||
if (record.type === 'file') {
|
||||
if (!Number.isSafeInteger(record.size) || record.size < 0 || !/^[0-9a-f]{64}$/.test(record.sha256) || ![0o644, 0o755].includes(record.mode)) throw new Error('Invalid file proof: ' + record.path);
|
||||
} else if (record.type === 'symlink') {
|
||||
if (typeof record.target !== 'string' || path.posix.isAbsolute(record.target) || record.target.includes('\0') || record.target.includes('\n')) throw new Error('Unsafe symlink proof');
|
||||
const resolved = path.posix.normalize(path.posix.join(path.posix.dirname(record.path), record.target));
|
||||
if (!resolved.startsWith(record.path.split('/')[0] + '/')) throw new Error('Escaping symlink proof');
|
||||
} else throw new Error('Unsupported proof type');
|
||||
records.set(record.path, record);
|
||||
}
|
||||
records.set('proof.json', { path: 'proof.json', type: 'file', size: embeddedBytes.length, mode: 0o644, sha256: hash(embeddedBytes) });
|
||||
for (const record of records.values()) {
|
||||
let parent = path.posix.dirname(record.path);
|
||||
while (parent !== '.') {
|
||||
if (records.get(parent)?.type === 'symlink') throw new Error('Archive child beneath symlink');
|
||||
parent = path.posix.dirname(parent);
|
||||
}
|
||||
}
|
||||
const maximum = proof.archive.unpackedBytes + records.size * 1024 + 2048;
|
||||
if (!Number.isSafeInteger(maximum) || maximum <= 0) throw new Error('Invalid unpacked size');
|
||||
const tar = zlib.gunzipSync(archive, { maxOutputLength: maximum });
|
||||
const decoded = [];
|
||||
const seen = new Set();
|
||||
const string = (buffer, start, length) => buffer.subarray(start, start + length).toString('utf8').split('\0', 1)[0];
|
||||
const number = (buffer, start, length) => {
|
||||
const text = string(buffer, start, length).trim();
|
||||
if (!/^[0-7]+$/.test(text)) throw new Error('Invalid normalized USTAR numeric field');
|
||||
return Number.parseInt(text, 8);
|
||||
};
|
||||
for (let offset = 0; offset + 512 <= tar.length;) {
|
||||
const header = tar.subarray(offset, offset + 512);
|
||||
if (header.every(byte => byte === 0)) {
|
||||
if (tar.subarray(offset).some(byte => byte !== 0)) throw new Error('Data after archive terminator');
|
||||
break;
|
||||
}
|
||||
if (string(header, 257, 6) !== 'ustar' || string(header, 263, 2) !== '00') throw new Error('Only normalized USTAR is supported');
|
||||
let checksum = 0; for (let index = 0; index < 512; index++) checksum += index >= 148 && index < 156 ? 32 : header[index];
|
||||
if (number(header, 148, 8) !== checksum) throw new Error('USTAR checksum mismatch');
|
||||
const leaf = string(header, 0, 100), parent = string(header, 345, 155);
|
||||
const name = parent ? parent + '/' + leaf : leaf;
|
||||
if (!name.startsWith('runtime-tools/')) throw new Error('Unexpected archive root');
|
||||
const relative = name.slice('runtime-tools/'.length);
|
||||
const record = records.get(relative);
|
||||
if (!safeRelative(relative) || !record || seen.has(relative)) throw new Error('Unknown, unsafe or duplicate archive entry');
|
||||
seen.add(relative);
|
||||
const size = number(header, 124, 12), mode = number(header, 100, 8), type = string(header, 156, 1), target = string(header, 157, 100);
|
||||
offset += 512;
|
||||
if (size > tar.length - offset) throw new Error('Truncated runtime archive');
|
||||
const bytes = tar.subarray(offset, offset + size);
|
||||
if (record.type === 'file') {
|
||||
if (type !== '0' || size !== record.size || mode !== record.mode || target || hash(bytes) !== record.sha256) throw new Error('File checksum or metadata mismatch: ' + relative);
|
||||
} else if (type !== '2' || size !== 0 || target !== record.target) throw new Error('Symlink mismatch: ' + relative);
|
||||
decoded.push({ ...record, bytes });
|
||||
offset += Math.ceil(size / 512) * 512;
|
||||
}
|
||||
if (seen.size !== records.size) throw new Error('Missing runtime archive files');
|
||||
if (options['verify-only']) {
|
||||
console.log(JSON.stringify({ verified: true, sourceCommit: proof.sourceCommit, archiveSha256: proof.archive.sha256, entries: seen.size }));
|
||||
process.exit(0);
|
||||
}
|
||||
const existed = file => { try { fs.lstatSync(file); return true; } catch (error) { if (error.code === 'ENOENT') return false; throw error; } };
|
||||
const createdDirectories = [];
|
||||
function ensureDirectory(directory) {
|
||||
if (existed(directory)) { if (!fs.statSync(directory).isDirectory()) throw new Error('Expected directory: ' + directory); return; }
|
||||
ensureDirectory(path.dirname(directory)); fs.mkdirSync(directory); fs.chmodSync(directory, 0o755); createdDirectories.push(directory);
|
||||
}
|
||||
let transaction;
|
||||
let installLock;
|
||||
let lockOwner;
|
||||
const swapped = [];
|
||||
function replace(source, destination) {
|
||||
ensureDirectory(path.dirname(destination));
|
||||
const backup = path.join(transaction, 'backup-' + swapped.length);
|
||||
const previous = existed(destination);
|
||||
if (previous) fs.renameSync(destination, backup);
|
||||
const operation = { destination, backup, previous, installed: false };
|
||||
swapped.push(operation);
|
||||
fs.renameSync(source, destination); operation.installed = true;
|
||||
}
|
||||
function assertTree(npmRoot, toolsRoot) {
|
||||
const readVersion = file => JSON.parse(fs.readFileSync(file, 'utf8')).version;
|
||||
if (readVersion(path.join(npmRoot, 'package.json')) !== '12.2.0') throw new Error('Wrong npm version');
|
||||
for (const [name, version] of Object.entries({ pnpm: '10.34.6', pm2: '7.0.4', 'node-gyp': '12.4.0', 'ts-node': '10.9.2', typescript: '5.9.3' })) if (readVersion(path.join(toolsRoot, 'node_modules', name, 'package.json')) !== version) throw new Error('Wrong tool version: ' + name);
|
||||
for (const [root, expected] of [
|
||||
[npmRoot, { 'brace-expansion': '5.0.12', 'http-cache-semantics': '4.3.0', undici: '6.29.0', tar: '7.5.22' }],
|
||||
[path.join(toolsRoot, 'node_modules/pm2'), { 'js-yaml': '4.3.2', 'basic-ftp': '6.2.1' }],
|
||||
[path.join(toolsRoot, 'node_modules/pnpm/dist'), { 'node-gyp': '12.4.0', tar: '7.5.22' }],
|
||||
]) {
|
||||
const requireFrom = createRequire(path.join(root, 'package.json'));
|
||||
for (const [name, version] of Object.entries(expected)) if (readVersion(requireFrom.resolve(name + '/package.json')) !== version) throw new Error('Wrong actual runtime dependency: ' + name);
|
||||
}
|
||||
}
|
||||
try {
|
||||
ensureDirectory(prefix);
|
||||
installLock = path.join(prefix, '.qinglong-runtime-install.lock');
|
||||
try { fs.mkdirSync(installLock, { mode: 0o700 }); } catch (error) { if (error.code === 'EEXIST') throw new Error('Runtime tools install is busy: existing prefix lock; no stale lock is automatically removed'); throw error; }
|
||||
lockOwner = JSON.stringify({ pid: process.pid, nonce: crypto.randomBytes(16).toString('hex') });
|
||||
fs.writeFileSync(path.join(installLock, 'owner.json'), lockOwner, { flag: 'wx', mode: 0o600 });
|
||||
transaction = fs.mkdtempSync(path.join(prefix, '.qinglong-runtime-'));
|
||||
const stage = path.join(transaction, 'stage'); fs.mkdirSync(stage);
|
||||
for (const entry of decoded.filter(record => record.type === 'file')) {
|
||||
const destination = path.join(stage, entry.path); fs.mkdirSync(path.dirname(destination), { recursive: true });
|
||||
fs.writeFileSync(destination, entry.bytes, { flag: 'wx', mode: entry.mode }); fs.chmodSync(destination, entry.mode);
|
||||
}
|
||||
for (const entry of decoded.filter(record => record.type === 'symlink')) {
|
||||
const destination = path.join(stage, entry.path); fs.mkdirSync(path.dirname(destination), { recursive: true }); fs.symlinkSync(entry.target, destination);
|
||||
}
|
||||
// Artifact directories need stable permissions even when the installing user has umask 077.
|
||||
const chmodArchiveDirectories = directory => { fs.chmodSync(directory, 0o755); for (const item of fs.readdirSync(directory, { withFileTypes: true })) if (item.isDirectory()) chmodArchiveDirectories(path.join(directory, item.name)); };
|
||||
chmodArchiveDirectories(stage);
|
||||
assertTree(path.join(stage, 'npm'), path.join(stage, 'tools'));
|
||||
const modules = path.join(prefix, 'lib/node_modules');
|
||||
const npmRoot = path.join(modules, 'npm'), toolsRoot = path.join(modules, 'qinglong-runtime-tools');
|
||||
replace(path.join(stage, 'npm'), npmRoot);
|
||||
replace(path.join(stage, 'tools'), toolsRoot);
|
||||
const links = path.join(transaction, 'links'); fs.mkdirSync(links);
|
||||
const toolNames = ['pnpm', 'pm2', 'node-gyp', 'ts-node', 'typescript'];
|
||||
for (const name of toolNames) {
|
||||
const source = path.join(links, 'module-' + name); fs.symlinkSync('qinglong-runtime-tools/node_modules/' + name, source);
|
||||
replace(source, path.join(modules, name));
|
||||
}
|
||||
const bins = new Map();
|
||||
for (const [name, root] of [['npm', npmRoot], ...toolNames.map(name => [name, path.join(toolsRoot, 'node_modules', name)])]) {
|
||||
const manifest = JSON.parse(fs.readFileSync(path.join(root, 'package.json'), 'utf8'));
|
||||
const declarations = typeof manifest.bin === 'string' ? { [name]: manifest.bin } : manifest.bin;
|
||||
for (const [command, relative] of Object.entries(declarations || {})) {
|
||||
if (!/^[A-Za-z0-9_.-]+$/.test(command) || !safeRelative(relative.replace(/^\.\//, ''))) throw new Error('Unsafe tool bin');
|
||||
const executable = command === 'pnpm' ? path.join(toolsRoot, 'bin/pnpm.cjs') : path.join(root, relative);
|
||||
if (!fs.statSync(executable).isFile()) throw new Error('Missing executable: ' + command);
|
||||
bins.set(command, executable);
|
||||
}
|
||||
}
|
||||
for (const [command, executable] of bins) {
|
||||
const destination = path.join(prefix, 'bin', command), source = path.join(links, 'bin-' + command);
|
||||
fs.symlinkSync(path.relative(path.dirname(destination), executable), source); replace(source, destination);
|
||||
}
|
||||
assertTree(npmRoot, toolsRoot);
|
||||
const emptyUserConfig = path.join(transaction, 'empty-user.npmrc'); fs.writeFileSync(emptyUserConfig, '', { flag: 'wx', mode: 0o600 });
|
||||
const safeEnvironment = { ...process.env, NPM_CONFIG_USERCONFIG: emptyUserConfig, NPM_CONFIG_GLOBALCONFIG: '/dev/null', NPM_CONFIG_CACHE: path.join(transaction, 'npm-cache'), PNPM_HOME: path.join(transaction, 'pnpm-home'), NODE_COMPILE_CACHE: path.join(transaction, 'compile-cache') };
|
||||
for (const [file, arguments_, expected] of [
|
||||
[path.join(npmRoot, 'bin/npm-cli.js'), ['--version'], '12.2.0'],
|
||||
[path.join(toolsRoot, 'node_modules/pnpm/bin/pnpm.cjs'), ['--config.manage-package-manager-versions=false', '--config.cache-dir=' + path.join(transaction, 'pnpm-cache'), '--config.state-dir=' + path.join(transaction, 'pnpm-state'), '--version'], '10.34.6'],
|
||||
[path.join(toolsRoot, 'node_modules/node-gyp/bin/node-gyp.js'), ['--version'], 'v12.4.0'],
|
||||
]) {
|
||||
const child = spawnSync(process.execPath, [file, ...arguments_], { cwd: transaction, env: safeEnvironment, encoding: 'utf8' });
|
||||
if (child.status !== 0 || child.stdout.trim() !== expected) throw new Error('Installed tool smoke failed: ' + path.basename(file));
|
||||
}
|
||||
if (fs.readFileSync(path.join(installLock, 'owner.json'), 'utf8') !== lockOwner) throw new Error('Installation lock ownership changed');
|
||||
fs.rmSync(transaction, { recursive: true, force: true }); transaction = undefined;
|
||||
fs.rmSync(installLock, { recursive: true }); installLock = undefined;
|
||||
console.log(JSON.stringify({ installed: true, prefix, npmRoot, toolsRoot, sourceCommit: proof.sourceCommit, archiveSha256: proof.archive.sha256, components: proof.components, persistedGlobalDirectoryChanged: false }));
|
||||
} catch (error) {
|
||||
const failures = [];
|
||||
for (const operation of swapped.reverse()) {
|
||||
try { if (operation.installed) fs.rmSync(operation.destination, { recursive: true, force: true }); if (operation.previous) fs.renameSync(operation.backup, operation.destination); }
|
||||
catch (rollbackError) { failures.push({ path: operation.destination, error: rollbackError.message }); }
|
||||
}
|
||||
if (installLock && lockOwner) { try { if (fs.readFileSync(path.join(installLock, 'owner.json'), 'utf8') === lockOwner) { fs.rmSync(installLock, { recursive: true }); installLock = undefined; } else failures.push({ path: installLock, error: 'Installation lock ownership changed' }); } catch (lockError) { failures.push({ path: installLock, error: lockError.message }); } }
|
||||
if (transaction && !failures.length) fs.rmSync(transaction, { recursive: true, force: true });
|
||||
if (!failures.length) for (const directory of createdDirectories.reverse()) { try { fs.rmdirSync(directory); } catch (cleanupError) { if (!['ENOTEMPTY', 'ENOENT'].includes(cleanupError.code)) failures.push({ path: directory, error: cleanupError.message }); } }
|
||||
console.error(JSON.stringify({ installed: false, error: error.message, rollbackComplete: failures.length === 0, rollbackFailures: failures, recoveryDirectory: failures.length ? transaction : undefined }));
|
||||
process.exitCode = 1;
|
||||
}
|
||||
@@ -0,0 +1,81 @@
|
||||
'use strict';
|
||||
const fs = require('node:fs');
|
||||
const path = require('node:path');
|
||||
const crypto = require('node:crypto');
|
||||
const zlib = require('node:zlib');
|
||||
const args = Object.fromEntries(Array.from({ length: (process.argv.length - 2) / 2 }, (_, i) => [process.argv[2 + i * 2].slice(2), process.argv[3 + i * 2]]));
|
||||
for (const key of ['npm', 'tools', 'payload', 'output', 'source-commit']) if (!args[key]) throw new Error('Missing --' + key);
|
||||
const sha = buffer => crypto.createHash('sha256').update(buffer).digest('hex');
|
||||
const entries = [];
|
||||
function scan(directory, prefix, records) {
|
||||
for (const name of fs.readdirSync(directory).sort()) {
|
||||
const file = path.join(directory, name), relative = prefix + '/' + name;
|
||||
if (name.includes('\n') || name.includes('\r') || name.includes('\0')) throw new Error('Unsupported archive name');
|
||||
const stat = fs.lstatSync(file);
|
||||
if (stat.isDirectory()) scan(file, relative, records);
|
||||
else if (stat.isSymbolicLink()) {
|
||||
const target = fs.readlinkSync(file);
|
||||
if (path.posix.isAbsolute(target)) throw new Error('Absolute symlink: ' + relative);
|
||||
const resolved = path.posix.normalize(path.posix.join(path.posix.dirname(relative), target));
|
||||
if (!resolved.startsWith(prefix.split('/')[0] + '/')) throw new Error('Escaping symlink: ' + relative);
|
||||
records.push({ path: relative, type: 'symlink', target });
|
||||
} else if (stat.isFile()) {
|
||||
if (name === 'pnpm-10.34.6-qinglong-security-linux.tgz' || name.startsWith('qinglong-runtime-') && name.endsWith('-proof.json')) continue;
|
||||
const bytes = fs.readFileSync(file);
|
||||
const magic = bytes.subarray(0, 4).toString('hex');
|
||||
if (/\.(node|exe|dll|so|dylib)$/.test(name) || ['7f454c46', 'cffaedfe', 'feedfacf', 'cafebabe'].includes(magic) || bytes.subarray(0, 2).toString() === 'MZ') throw new Error('Native/platform binary: ' + relative);
|
||||
records.push({ path: relative, type: 'file', size: bytes.length, mode: stat.mode & 0o111 ? 0o755 : 0o644, sha256: sha(bytes), bytes });
|
||||
} else throw new Error('Unsupported filesystem entry: ' + relative);
|
||||
}
|
||||
}
|
||||
scan(args.npm, 'npm', entries); scan(args.tools, 'tools', entries);
|
||||
const inputs = [];
|
||||
function inputScan(directory, prefix = '') {
|
||||
for (const name of fs.readdirSync(directory).sort()) {
|
||||
const file = path.join(directory, name), relative = prefix ? prefix + '/' + name : name;
|
||||
if (fs.statSync(file).isDirectory()) inputScan(file, relative);
|
||||
else inputs.push({ path: 'scripts/runtime-tools/' + relative, sha256: sha(fs.readFileSync(file)) });
|
||||
}
|
||||
}
|
||||
inputScan(args.payload);
|
||||
for (const name of ['build-runtime-tools.sh', 'pack-runtime-tools.cjs', 'install-runtime-tools.cjs', 'verify-runtime-tools-source.cjs']) {
|
||||
const file = path.join(__dirname, name); if (fs.existsSync(file)) inputs.push({ path: 'scripts/' + name, sha256: sha(fs.readFileSync(file)) });
|
||||
}
|
||||
const sourceAttestation = require('./verify-runtime-tools-source.cjs')(args.payload, args['source-commit']);
|
||||
const proof = {
|
||||
schemaVersion: 1, sourceCommit: args['source-commit'], supportedPlatforms: ['linux'],
|
||||
nodeEngine: '^22.22.2 || ^24.15.0 || >=26.0.0',
|
||||
builder: { node: process.version, platform: process.platform, releaseBuild: process.platform === 'linux' && sourceAttestation.verified, typescript: '5.5.4', esbuild: '0.25.12', bootstrapPnpm: '10.34.6', runtimeNpm: '12.2.0' },
|
||||
components: { npm: '12.2.0', pnpm: '10.34.6', pm2: '7.0.4', nodeGyp: '12.4.0', tsNode: '10.9.2', typescript: '5.9.3', pnpmUpstreamCommit: '9287c31cea69206c4eeca7f21cd21df81d0b93e7', pnpmSecurityTgzSha256: 'de1266186669300c0729f7f5671c97108379ec36afe17b6ec7caaf47091dd7d7' },
|
||||
inputs, sourceAttestation,
|
||||
residualAdvisories: [{ id: 'GHSA-vfj7-8cjw-p6xm', package: 'braces', version: '3.0.3', severity: 'high', patchedVersion: null }],
|
||||
files: entries.map(({ bytes, ...record }) => record),
|
||||
};
|
||||
const proofBytes = Buffer.from(JSON.stringify(proof, null, 2) + '\n');
|
||||
entries.push({ path: 'proof.json', type: 'file', mode: 0o644, size: proofBytes.length, bytes: proofBytes });
|
||||
function header(name, mode, size, type, link = '') {
|
||||
const buffer = Buffer.alloc(512), octal = (value, width) => value.toString(8).padStart(width - 1, '0') + '\0';
|
||||
let leaf = name, prefix = '';
|
||||
if (Buffer.byteLength(name) > 100) {
|
||||
for (let index = name.length - 1; index >= 0; index--) if (name[index] === '/' && Buffer.byteLength(name.slice(0, index)) <= 155 && Buffer.byteLength(name.slice(index + 1)) <= 100) { prefix = name.slice(0, index); leaf = name.slice(index + 1); break; }
|
||||
if (!prefix) throw new Error('Path too long for normalized USTAR: ' + name);
|
||||
}
|
||||
if (Buffer.byteLength(link) > 100) throw new Error('Symlink target too long: ' + name);
|
||||
buffer.write(leaf, 0, 100); buffer.write(octal(mode, 8), 100, 8); buffer.write(octal(0, 8), 108, 8); buffer.write(octal(0, 8), 116, 8);
|
||||
buffer.write(octal(size, 12), 124, 12); buffer.write(octal(0, 12), 136, 12); buffer.fill(32, 148, 156); buffer.write(type, 156, 1); buffer.write(link, 157, 100);
|
||||
buffer.write('ustar\0', 257, 6); buffer.write('00', 263, 2); buffer.write(prefix, 345, 155);
|
||||
const checksum = buffer.reduce((sum, byte) => sum + byte, 0); buffer.write(checksum.toString(8).padStart(6, '0') + '\0 ', 148, 8);
|
||||
return buffer;
|
||||
}
|
||||
const chunks = [];
|
||||
for (const entry of entries.sort((a, b) => a.path.localeCompare(b.path, 'en'))) {
|
||||
const file = entry.type === 'file'; chunks.push(header('runtime-tools/' + entry.path, entry.mode || 0o777, file ? entry.size : 0, file ? '0' : '2', entry.target || ''));
|
||||
if (file) { chunks.push(entry.bytes); const padding = (512 - entry.size % 512) % 512; if (padding) chunks.push(Buffer.alloc(padding)); }
|
||||
}
|
||||
chunks.push(Buffer.alloc(1024));
|
||||
const archive = zlib.gzipSync(Buffer.concat(chunks), { level: 9 });
|
||||
fs.mkdirSync(args.output);
|
||||
fs.writeFileSync(path.join(args.output, 'runtime-tools.tgz'), archive);
|
||||
const published = { ...proof, archive: { name: 'runtime-tools.tgz', sha256: sha(archive), integrity: 'sha512-' + crypto.createHash('sha512').update(archive).digest('base64'), bytes: archive.length, unpackedBytes: proof.files.filter(x => x.type === 'file').reduce((sum, x) => sum + x.size, 0) + proofBytes.length } };
|
||||
fs.writeFileSync(path.join(args.output, 'runtime-tools-proof.json'), JSON.stringify(published, null, 2) + '\n');
|
||||
console.log(JSON.stringify({ archive: published.archive, sourceCommit: proof.sourceCommit, fileCount: proof.files.length, releaseBuild: proof.builder.releaseBuild }));
|
||||
@@ -0,0 +1,68 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
payload_dir=$(cd -- "$(dirname -- "$0")" && pwd)
|
||||
node <<'NODE'
|
||||
const [major,minor,patch]=process.versions.node.split('.').map(Number);
|
||||
const supported=(major===22&&(minor>22||(minor===22&&patch>=2)))||(major===24&&minor>=15)||major>=26;
|
||||
if(!supported)throw new Error('npm12 requires Node ^22.22.2 || ^24.15.0 || >=26.0.0');
|
||||
NODE
|
||||
target_dir=${1:?Pass a new /tmp/qinglong-* or /private/tmp/qinglong-* directory}
|
||||
case "$target_dir" in
|
||||
/tmp/qinglong-*|/private/tmp/qinglong-*) ;;
|
||||
*) echo 'Refusing a target outside the preview prefix' >&2; exit 1 ;;
|
||||
esac
|
||||
if [[ -e "$target_dir" ]]; then
|
||||
echo 'Target already exists; use a fresh directory' >&2
|
||||
exit 1
|
||||
fi
|
||||
mkdir -p "$target_dir"
|
||||
archive_file="$target_dir/.npm-source.tgz"
|
||||
if [[ -n "${QL_TOOLS_NPM_ARCHIVE:-}" ]]; then
|
||||
cp -- "$QL_TOOLS_NPM_ARCHIVE" "$archive_file"
|
||||
else
|
||||
curl --fail --location --proto '=https' --proto-redir '=https' \
|
||||
https://registry.npmjs.org/npm/-/npm-12.2.0.tgz --output "$archive_file"
|
||||
fi
|
||||
node - "$payload_dir/source-integrity.json" "$archive_file" <<'NODE'
|
||||
const fs=require('node:fs');
|
||||
const crypto=require('node:crypto');
|
||||
const [manifest,archive]=process.argv.slice(2);
|
||||
const expected=JSON.parse(fs.readFileSync(manifest,'utf8'));
|
||||
const actual=crypto.createHash('sha512').update(fs.readFileSync(archive)).digest('base64');
|
||||
if(actual!==expected.sha512Base64)throw new Error('Official npm source SHA512 mismatch');
|
||||
NODE
|
||||
tar -xzf "$archive_file" --strip-components=1 -C "$target_dir"
|
||||
rm -- "$archive_file"
|
||||
# This directory was created above, solely from the authenticated archive.
|
||||
# Re-resolve from the recorded lock instead of preserving its stale bundle.
|
||||
rm -rf -- "$target_dir/node_modules"
|
||||
cp -- "$payload_dir/package.json" "$target_dir/package.json"
|
||||
cp -- "$payload_dir/package-lock.json" "$target_dir/package-lock.json"
|
||||
offline_args=()
|
||||
if [[ "${QL_TOOLS_OFFLINE:-0}" == 1 ]]; then offline_args+=(--offline); fi
|
||||
build_cache=${QL_TOOLS_NPM_CACHE:-"$target_dir/.build-cache"}
|
||||
(
|
||||
cd -- "$target_dir"
|
||||
NPM_CONFIG_USERCONFIG="$payload_dir/public.npmrc" \
|
||||
NPM_CONFIG_CACHE="$build_cache" \
|
||||
node "${QL_TOOLS_BOOTSTRAP_NPM:-$(command -v npm)}" ci --os=linux --ignore-scripts --omit=dev --no-audit --no-fund "${offline_args[@]}"
|
||||
)
|
||||
if [[ -z "${QL_TOOLS_NPM_CACHE:-}" ]]; then rm -rf -- "$target_dir/.build-cache"; fi
|
||||
node - "$target_dir" <<'NODE'
|
||||
const fs=require('node:fs');
|
||||
const path=require('node:path');
|
||||
const {createRequire}=require('node:module');
|
||||
const target=process.argv[2];
|
||||
const req=createRequire(path.join(target,'package.json'));
|
||||
const expected={'brace-expansion':'5.0.12','http-cache-semantics':'4.3.0','undici':'6.29.0','tar':'7.5.22'};
|
||||
const proof={};
|
||||
for(const [name,version] of Object.entries(expected)){
|
||||
const file=req.resolve(name+'/package.json');
|
||||
const actual=JSON.parse(fs.readFileSync(file,'utf8')).version;
|
||||
if(actual!==version)throw new Error(`${name}: expected ${version}, got ${actual}`);
|
||||
proof[name]={version:actual,path:file};
|
||||
}
|
||||
fs.writeFileSync(path.join(target,'qinglong-runtime-dependency-proof.json'),JSON.stringify(proof,null,2)+'\n');
|
||||
console.log(JSON.stringify(proof));
|
||||
NODE
|
||||
node "$target_dir/bin/npm-cli.js" --version
|
||||
+2126
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,248 @@
|
||||
{
|
||||
"version": "12.2.0",
|
||||
"name": "npm",
|
||||
"description": "a package manager for JavaScript",
|
||||
"workspaces": [
|
||||
"docs",
|
||||
"smoke-tests",
|
||||
"mock-globals",
|
||||
"mock-registry",
|
||||
"workspaces/*"
|
||||
],
|
||||
"files": [
|
||||
"bin/",
|
||||
"lib/",
|
||||
"index.js",
|
||||
"docs/content/",
|
||||
"docs/output/",
|
||||
"man/"
|
||||
],
|
||||
"keywords": [
|
||||
"install",
|
||||
"modules",
|
||||
"package manager",
|
||||
"package.json"
|
||||
],
|
||||
"homepage": "https://docs.npmjs.com/",
|
||||
"author": "GitHub Inc.",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/npm/cli.git"
|
||||
},
|
||||
"bugs": {
|
||||
"url": "https://github.com/npm/cli/issues"
|
||||
},
|
||||
"main": "./index.js",
|
||||
"bin": {
|
||||
"npm": "bin/npm-cli.js",
|
||||
"npx": "bin/npx-cli.js"
|
||||
},
|
||||
"exports": {
|
||||
".": [
|
||||
{
|
||||
"default": "./index.js"
|
||||
},
|
||||
"./index.js"
|
||||
],
|
||||
"./package.json": "./package.json"
|
||||
},
|
||||
"dependencies": {
|
||||
"@isaacs/string-locale-compare": "^1.1.0",
|
||||
"@npmcli/arborist": "^10.0.3",
|
||||
"@npmcli/config": "^11.2.0",
|
||||
"@npmcli/fs": "^6.0.0",
|
||||
"@npmcli/git": "^8.0.0",
|
||||
"@npmcli/map-workspaces": "^6.0.0",
|
||||
"@npmcli/metavuln-calculator": "^10.0.0",
|
||||
"@npmcli/package-json": "^8.0.0",
|
||||
"@npmcli/promise-spawn": "^10.0.0",
|
||||
"@npmcli/redact": "^5.0.0",
|
||||
"@npmcli/run-script": "^11.0.0",
|
||||
"@sigstore/tuf": "^5.0.0",
|
||||
"abbrev": "^5.0.0",
|
||||
"archy": "~1.0.0",
|
||||
"bin-links": "^7.0.0",
|
||||
"cacache": "^21.0.1",
|
||||
"chalk": "^5.6.2",
|
||||
"ci-info": "^4.4.0",
|
||||
"diff": "^8.0.2",
|
||||
"fastest-levenshtein": "^1.0.16",
|
||||
"fs-minipass": "^3.0.3",
|
||||
"glob": "^13.0.6",
|
||||
"graceful-fs": "^4.2.11",
|
||||
"hosted-git-info": "^10.1.1",
|
||||
"ini": "^7.0.0",
|
||||
"init-package-json": "^9.0.0",
|
||||
"is-cidr": "^7.0.0",
|
||||
"json-parse-even-better-errors": "^6.0.0",
|
||||
"libnpmaccess": "^11.0.0",
|
||||
"libnpmdiff": "^9.0.3",
|
||||
"libnpmexec": "^11.0.3",
|
||||
"libnpmfund": "^8.0.3",
|
||||
"libnpmorg": "^9.0.0",
|
||||
"libnpmpack": "^10.0.3",
|
||||
"libnpmpublish": "^12.0.1",
|
||||
"libnpmsearch": "^10.0.0",
|
||||
"libnpmteam": "^9.0.0",
|
||||
"libnpmversion": "^9.0.0",
|
||||
"make-fetch-happen": "^16.0.1",
|
||||
"minimatch": "^10.2.5",
|
||||
"minipass": "^7.1.3",
|
||||
"minipass-pipeline": "^1.2.4",
|
||||
"ms": "^2.1.2",
|
||||
"node-gyp": "^13.0.0",
|
||||
"nopt": "^10.0.1",
|
||||
"npm-audit-report": "^8.0.0",
|
||||
"npm-install-checks": "^9.0.0",
|
||||
"npm-package-arg": "^14.0.0",
|
||||
"npm-pick-manifest": "^12.0.0",
|
||||
"npm-profile": "^13.0.1",
|
||||
"npm-registry-fetch": "^20.0.1",
|
||||
"npm-user-validate": "^5.0.0",
|
||||
"p-map": "^7.0.4",
|
||||
"pacote": "^22.0.0",
|
||||
"parse-conflict-json": "^6.0.0",
|
||||
"proc-log": "^7.0.0",
|
||||
"qrcode-terminal": "^0.12.0",
|
||||
"read": "^6.0.0",
|
||||
"semver": "^7.8.5",
|
||||
"spdx-expression-parse": "^4.0.0",
|
||||
"ssri": "^14.0.0",
|
||||
"supports-color": "^10.2.2",
|
||||
"tar": "^7.5.22",
|
||||
"text-table": "~0.2.0",
|
||||
"tiny-relative-date": "^2.0.2",
|
||||
"treeverse": "^3.0.0",
|
||||
"validate-npm-package-name": "^8.0.0",
|
||||
"which": "^7.0.0"
|
||||
},
|
||||
"bundleDependencies": [
|
||||
"@isaacs/string-locale-compare",
|
||||
"@npmcli/arborist",
|
||||
"@npmcli/config",
|
||||
"@npmcli/fs",
|
||||
"@npmcli/git",
|
||||
"@npmcli/map-workspaces",
|
||||
"@npmcli/metavuln-calculator",
|
||||
"@npmcli/package-json",
|
||||
"@npmcli/promise-spawn",
|
||||
"@npmcli/redact",
|
||||
"@npmcli/run-script",
|
||||
"@sigstore/tuf",
|
||||
"abbrev",
|
||||
"archy",
|
||||
"bin-links",
|
||||
"cacache",
|
||||
"chalk",
|
||||
"ci-info",
|
||||
"diff",
|
||||
"fastest-levenshtein",
|
||||
"fs-minipass",
|
||||
"glob",
|
||||
"graceful-fs",
|
||||
"hosted-git-info",
|
||||
"ini",
|
||||
"init-package-json",
|
||||
"is-cidr",
|
||||
"json-parse-even-better-errors",
|
||||
"libnpmaccess",
|
||||
"libnpmdiff",
|
||||
"libnpmexec",
|
||||
"libnpmfund",
|
||||
"libnpmorg",
|
||||
"libnpmpack",
|
||||
"libnpmpublish",
|
||||
"libnpmsearch",
|
||||
"libnpmteam",
|
||||
"libnpmversion",
|
||||
"make-fetch-happen",
|
||||
"minimatch",
|
||||
"minipass",
|
||||
"minipass-pipeline",
|
||||
"ms",
|
||||
"node-gyp",
|
||||
"nopt",
|
||||
"npm-audit-report",
|
||||
"npm-install-checks",
|
||||
"npm-package-arg",
|
||||
"npm-pick-manifest",
|
||||
"npm-profile",
|
||||
"npm-registry-fetch",
|
||||
"npm-user-validate",
|
||||
"p-map",
|
||||
"pacote",
|
||||
"parse-conflict-json",
|
||||
"proc-log",
|
||||
"qrcode-terminal",
|
||||
"read",
|
||||
"semver",
|
||||
"spdx-expression-parse",
|
||||
"ssri",
|
||||
"supports-color",
|
||||
"tar",
|
||||
"text-table",
|
||||
"tiny-relative-date",
|
||||
"treeverse",
|
||||
"validate-npm-package-name",
|
||||
"which"
|
||||
],
|
||||
"scripts": {
|
||||
"dependencies": "node scripts/bundle-and-gitignore-deps.js && node scripts/dependency-graph.js",
|
||||
"dumpconf": "env | grep npm | sort | uniq",
|
||||
"licenses": "npx licensee --production --errors-only",
|
||||
"test": "tap",
|
||||
"test:nocolor": "CI=true tap -Rclassic",
|
||||
"test-all": "node . run test --workspaces --include-workspace-root --if-present",
|
||||
"snap": "tap",
|
||||
"prepack": "node . run build -w docs",
|
||||
"posttest": "node . run lint",
|
||||
"lint": "node . run eslint",
|
||||
"lintfix": "node . run eslint -- --fix",
|
||||
"lint-all": "node . run lint --workspaces --include-workspace-root --if-present",
|
||||
"resetdeps": "node scripts/resetdeps.js",
|
||||
"rp-pull-request": "node scripts/update-authors.js",
|
||||
"postlint": "template-oss-check",
|
||||
"template-oss-apply": "template-oss-apply --force",
|
||||
"eslint": "eslint \"**/*.{js,cjs,ts,mjs,jsx,tsx}\""
|
||||
},
|
||||
"tap": {
|
||||
"test-env": [
|
||||
"LC_ALL=sk"
|
||||
],
|
||||
"node-arg": [
|
||||
"--require",
|
||||
"./scripts/disable-agent-for-tests.js"
|
||||
],
|
||||
"timeout": 600,
|
||||
"nyc-arg": [
|
||||
"--exclude",
|
||||
"docs/**",
|
||||
"--exclude",
|
||||
"smoke-tests/**",
|
||||
"--exclude",
|
||||
"mock-globals/**",
|
||||
"--exclude",
|
||||
"mock-registry/**",
|
||||
"--exclude",
|
||||
"workspaces/**",
|
||||
"--exclude",
|
||||
"tap-snapshots/**"
|
||||
],
|
||||
"test-ignore": "^(docs|smoke-tests|mock-globals|mock-registry|workspaces)/"
|
||||
},
|
||||
"templateOSS": {
|
||||
"//@npmcli/template-oss": "This file is partially managed by @npmcli/template-oss. Edits may be overwritten.",
|
||||
"version": "5.1.1",
|
||||
"content": "./scripts/template-oss/root.js",
|
||||
"updateNpm": false
|
||||
},
|
||||
"license": "Artistic-2.0",
|
||||
"engines": {
|
||||
"node": "^22.22.2 || ^24.15.0 || >=26.0.0"
|
||||
},
|
||||
"overrides": {
|
||||
"brace-expansion": "5.0.12",
|
||||
"http-cache-semantics": "4.3.0",
|
||||
"undici": "6.29.0"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
registry=https://registry.npmjs.org/
|
||||
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"name": "npm",
|
||||
"version": "12.2.0",
|
||||
"url": "https://registry.npmjs.org/npm/-/npm-12.2.0.tgz",
|
||||
"sha512Base64": "ZsJjKpTnlmSXOLLXiU1xDCzC4Wlok4IwZmh/aw2KUuXytU7q6qMv/cUT7MoeSf95Slwuw/lRXYefGzGCspHPNQ==",
|
||||
"sha256": "6666b48816b39b86c3febac7b51a4ee4de6c5ca589c382ad8004b6b113f86677",
|
||||
"note": "Official source archive, runtime-only manifest and actual npm lock replace the bundled dependency tree; JavaScript source remains unchanged."
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
import gzip, hashlib, io, json, os, shutil, tarfile
|
||||
from pathlib import Path
|
||||
import sys
|
||||
|
||||
source, stage, archive, payload = map(Path, sys.argv[1:])
|
||||
if not (stage / 'dist/pnpm.cjs').is_file():
|
||||
raise SystemExit('Run the controlled bundle builder first')
|
||||
cli = source / 'pnpm'
|
||||
shutil.copytree(cli / 'bin', stage / 'bin')
|
||||
shutil.copytree(cli / 'node-gyp-bin', stage / 'dist/node-gyp-bin')
|
||||
shutil.copytree(cli / 'node_modules/@pnpm/tabtab/lib/templates', stage / 'dist/templates')
|
||||
shutil.copyfile(cli / 'pnpmrc', stage / 'dist/pnpmrc')
|
||||
for name in ['LICENSE', 'README.md']:
|
||||
for candidate in [cli / name, source / name]:
|
||||
if candidate.is_file():
|
||||
shutil.copyfile(candidate, stage / name)
|
||||
break
|
||||
manifest = json.loads((cli / 'package.json').read_text())
|
||||
for field in ['dependencies', 'optionalDependencies', 'devDependencies', 'scripts', 'pnpm', 'jest', 'publishConfig']:
|
||||
manifest.pop(field, None)
|
||||
manifest['qinglongSecurityPatch'] = {
|
||||
'baseVersion': '10.34.6',
|
||||
'upstreamCommit': '9287c31cea69206c4eeca7f21cd21df81d0b93e7',
|
||||
'platform': 'linux',
|
||||
'buildLockSha256': hashlib.sha256((payload / 'pnpm-lock.yaml').read_bytes()).hexdigest(),
|
||||
'overridesSha256': hashlib.sha256((payload / 'pnpm-workspace.yaml').read_bytes()).hexdigest(),
|
||||
'builderSha256': hashlib.sha256((payload / 'bundle-linux.cjs').read_bytes()).hexdigest(),
|
||||
'nativeReflink': 'external; Linux uses Node fs.COPYFILE_FICLONE_FORCE',
|
||||
}
|
||||
(stage / 'package.json').write_text(json.dumps(manifest, indent=2) + '\n')
|
||||
shutil.copyfile(payload / 'node-gyp-runtime/package.json', stage / 'dist/package.json')
|
||||
shutil.copyfile(payload / 'node-gyp-runtime/package-lock.json', stage / 'dist/package-lock.json')
|
||||
if not (stage / 'dist/node_modules/node-gyp/package.json').is_file():
|
||||
raise SystemExit('Install the recorded node-gyp-runtime lock into stage/dist first')
|
||||
files = sorted(p for p in stage.rglob('*') if p.is_file() and not p.is_symlink())
|
||||
for file in files:
|
||||
relative = str(file.relative_to(stage))
|
||||
magic = file.read_bytes()[:4]
|
||||
if file.suffix in ['.node', '.exe', '.dll', '.so', '.dylib'] or magic in [b'\x7fELF', b'\xcf\xfa\xed\xfe', b'\xfe\xed\xfa\xcf', b'\xca\xfe\xba\xbe'] or magic[:2] == b'MZ':
|
||||
raise SystemExit('Unexpected binary in Linux payload: ' + relative)
|
||||
with archive.open('wb') as raw:
|
||||
with gzip.GzipFile(filename='', fileobj=raw, mode='wb', mtime=0) as compressed:
|
||||
with tarfile.open(fileobj=compressed, mode='w', format=tarfile.PAX_FORMAT) as tar:
|
||||
for file in files:
|
||||
data = file.read_bytes()
|
||||
info = tarfile.TarInfo('package/' + str(file.relative_to(stage)))
|
||||
info.size, info.mtime, info.uid, info.gid = len(data), 0, 0, 0
|
||||
info.uname = info.gname = ''
|
||||
info.mode = 0o755 if file.stat().st_mode & 0o111 else 0o644
|
||||
tar.addfile(info, io.BytesIO(data))
|
||||
proof = {
|
||||
'fileCount': len(files), 'nativeOrPlatformBinaries': [],
|
||||
'archive': str(archive), 'sha256': hashlib.sha256(archive.read_bytes()).hexdigest(),
|
||||
'files': [{'path': str(p.relative_to(stage)), 'sha256': hashlib.sha256(p.read_bytes()).hexdigest()} for p in files],
|
||||
'provenance': manifest['qinglongSecurityPatch'],
|
||||
}
|
||||
archive.with_suffix('.evidence.json').write_text(json.dumps(proof, indent=2) + '\n')
|
||||
print(json.dumps({key: proof[key] for key in ['fileCount', 'nativeOrPlatformBinaries', 'archive', 'sha256']}, indent=2))
|
||||
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"name": "pnpm",
|
||||
"version": "10.34.6",
|
||||
"url": "https://registry.npmjs.org/pnpm/-/pnpm-10.34.6.tgz",
|
||||
"sha512Base64": "fhrmaoOpFDwRjMGUTzfMTyuKzd7aSwAc+niQj25G5F4VKSc5ymydAmQmzHV+kxjMmMVVHWoWwWUJewAouoe05g==",
|
||||
"sha256": "44d7db90fcbb2315b581f85989a913466765421fc656c28e80fac1b7ab5be456"
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
payload_dir=$(cd -- "$(dirname -- "$0")" && pwd)
|
||||
target_dir=${1:?Pass a fresh /tmp/qinglong-* or /private/tmp/qinglong-* build directory}
|
||||
case "$target_dir" in /tmp/qinglong-*|/private/tmp/qinglong-*) ;; *) echo 'Target outside preview prefix' >&2; exit 1 ;; esac
|
||||
if [[ -e "$target_dir" ]]; then echo 'Target already exists' >&2; exit 1; fi
|
||||
if [[ "$(node --version)" != v22.23.3 ]]; then echo 'This reviewed build pins Node v22.23.3' >&2; exit 1; fi
|
||||
npm_cli=${QL_TOOLS_BOOTSTRAP_NPM:?Pass the reviewed npm12 bin/npm-cli.js}
|
||||
if [[ "$(node "$npm_cli" --version)" != 12.2.0 ]]; then echo 'This reviewed build pins npm12.2.0' >&2; exit 1; fi
|
||||
mkdir -p "$target_dir/source" "$target_dir/bootstrap"
|
||||
if [[ -n "${QL_TOOLS_PNPM_SOURCE_ARCHIVE:-}" ]]; then
|
||||
cp -- "$QL_TOOLS_PNPM_SOURCE_ARCHIVE" "$target_dir/source.tgz"
|
||||
else
|
||||
curl --fail --location --proto '=https' --proto-redir '=https' \
|
||||
https://codeload.github.com/pnpm/pnpm/tar.gz/9287c31cea69206c4eeca7f21cd21df81d0b93e7 --output "$target_dir/source.tgz"
|
||||
fi
|
||||
if [[ -n "${QL_TOOLS_PNPM_BOOTSTRAP_ARCHIVE:-}" ]]; then
|
||||
cp -- "$QL_TOOLS_PNPM_BOOTSTRAP_ARCHIVE" "$target_dir/bootstrap.tgz"
|
||||
else
|
||||
curl --fail --location --proto '=https' --proto-redir '=https' \
|
||||
https://registry.npmjs.org/pnpm/-/pnpm-10.34.6.tgz --output "$target_dir/bootstrap.tgz"
|
||||
fi
|
||||
node - "$payload_dir" "$target_dir" <<'NODE'
|
||||
const fs=require('node:fs'),path=require('node:path'),crypto=require('node:crypto');
|
||||
const [payload,target]=process.argv.slice(2);
|
||||
for(const [metadata,archive,algorithm,encoding,key] of [
|
||||
['source-integrity.json','source.tgz','sha256','hex','sha256'],
|
||||
['bootstrap-integrity.json','bootstrap.tgz','sha512','base64','sha512Base64'],
|
||||
]){
|
||||
const pin=JSON.parse(fs.readFileSync(path.join(payload,metadata),'utf8'));
|
||||
const actual=crypto.createHash(algorithm).update(fs.readFileSync(path.join(target,archive))).digest(encoding);
|
||||
if(actual!==pin[key])throw new Error(archive+' integrity mismatch');
|
||||
}
|
||||
NODE
|
||||
tar -xzf "$target_dir/source.tgz" --strip-components=1 -C "$target_dir/source"
|
||||
tar -xzf "$target_dir/bootstrap.tgz" --strip-components=1 -C "$target_dir/bootstrap"
|
||||
cp -- "$payload_dir/pnpm-workspace.yaml" "$target_dir/source/pnpm-workspace.yaml"
|
||||
cp -- "$payload_dir/pnpm-lock.yaml" "$target_dir/source/pnpm-lock.yaml"
|
||||
offline_args=()
|
||||
if [[ "${QL_TOOLS_OFFLINE:-0}" == 1 ]]; then offline_args+=(--offline); fi
|
||||
export NPM_CONFIG_USERCONFIG="$payload_dir/public.npmrc"
|
||||
export NPM_CONFIG_CACHE=${QL_TOOLS_NPM_CACHE:-"$target_dir/npm-cache"}
|
||||
export PNPM_HOME="$target_dir/pnpm-home"
|
||||
export NODE_COMPILE_CACHE="$target_dir/node-compile-cache"
|
||||
unset NODE_PATH
|
||||
pnpm_store=${QL_TOOLS_PNPM_STORE:-"$target_dir/pnpm-store"}
|
||||
node "$target_dir/bootstrap/bin/pnpm.cjs" --dir "$target_dir/source" \
|
||||
--filter pnpm... --filter monorepo-root install --frozen-lockfile --ignore-scripts \
|
||||
--store-dir "$pnpm_store" --config.manage-package-manager-versions=false \
|
||||
--config.package-manager-strict=false --config.cache-dir="$target_dir/pnpm-cache" \
|
||||
--config.state-dir="$target_dir/pnpm-state" "${offline_args[@]}"
|
||||
cmp -- "$payload_dir/pnpm-lock.yaml" "$target_dir/source/pnpm-lock.yaml"
|
||||
(
|
||||
cd -- "$target_dir/source/pnpm"
|
||||
node ../node_modules/typescript/bin/tsc --build
|
||||
)
|
||||
node "$payload_dir/bundle-linux.cjs" "$target_dir/source" "$target_dir/package/dist"
|
||||
cp -- "$payload_dir/node-gyp-runtime/package.json" "$target_dir/package/dist/package.json"
|
||||
cp -- "$payload_dir/node-gyp-runtime/package-lock.json" "$target_dir/package/dist/package-lock.json"
|
||||
(
|
||||
cd -- "$target_dir/package/dist"
|
||||
node "$npm_cli" ci --ignore-scripts --omit=dev --no-audit --no-fund "${offline_args[@]}"
|
||||
)
|
||||
cmp -- "$payload_dir/node-gyp-runtime/package-lock.json" "$target_dir/package/dist/package-lock.json"
|
||||
python3 "$payload_dir/assemble-and-pack.py" "$target_dir/source" "$target_dir/package" \
|
||||
"$target_dir/pnpm-10.34.6-qinglong-security-linux.tgz" "$payload_dir"
|
||||
(
|
||||
cd -- "$target_dir"
|
||||
node "$target_dir/package/bin/pnpm.cjs" --config.manage-package-manager-versions=false --version
|
||||
node "$target_dir/package/dist/node_modules/node-gyp/bin/node-gyp.js" --version
|
||||
)
|
||||
@@ -0,0 +1,51 @@
|
||||
'use strict';
|
||||
const fs = require('node:fs');
|
||||
const path = require('node:path');
|
||||
const { createRequire } = require('node:module');
|
||||
const [source, output] = process.argv.slice(2);
|
||||
if (!source || !output) throw new Error('Pass official source root and fresh output/dist');
|
||||
const cliDir = path.join(source, 'pnpm');
|
||||
const req = createRequire(path.join(cliDir, 'package.json'));
|
||||
const { build } = req('esbuild');
|
||||
fs.mkdirSync(output, { recursive: true });
|
||||
// Upstream bundle.ts options, with the optional macOS/Windows reflink module external.
|
||||
// Linux uses Node fs.COPYFILE_FICLONE_FORCE; no native payload is needed there.
|
||||
const common = {
|
||||
absWorkingDir: cliDir,
|
||||
bundle: true,
|
||||
platform: 'node',
|
||||
external: ['@reflink/reflink'],
|
||||
loader: { '.node': 'copy' },
|
||||
nodePaths: [],
|
||||
plugins: [{
|
||||
name: 'only-recorded-source-tree',
|
||||
setup(builder) {
|
||||
builder.onLoad({ filter: /.*/ }, args => {
|
||||
const real = fs.realpathSync(args.path);
|
||||
if (real !== source && !real.startsWith(source + path.sep)) {
|
||||
return { errors: [{ text: 'Refusing unrecorded ancestor or global module: ' + args.path }] };
|
||||
}
|
||||
return undefined;
|
||||
});
|
||||
},
|
||||
}],
|
||||
};
|
||||
(async () => {
|
||||
await build({
|
||||
...common,
|
||||
entryPoints: ['lib/pnpm.js'],
|
||||
outfile: path.join(output, 'pnpm.cjs'),
|
||||
external: [...common.external, 'node-gyp', './get-uid-gid.js'],
|
||||
define: {
|
||||
'process.env.npm_package_name': JSON.stringify('pnpm'),
|
||||
'process.env.npm_package_version': JSON.stringify('10.34.6'),
|
||||
},
|
||||
});
|
||||
await build({
|
||||
...common,
|
||||
entryPoints: ['../worker/lib/worker.js'],
|
||||
outfile: path.join(output, 'worker.js'),
|
||||
});
|
||||
const binaries = fs.readdirSync(output).filter(name => name.endsWith('.node'));
|
||||
if (binaries.length) throw new Error(`Unexpected native outputs: ${binaries.join(', ')}`);
|
||||
})().catch(error => { console.error(error); process.exitCode = 1; });
|
||||
@@ -0,0 +1,250 @@
|
||||
{
|
||||
"name": "qinglong-pnpm10-runtime-node-gyp",
|
||||
"version": "1.0.0",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "qinglong-pnpm10-runtime-node-gyp",
|
||||
"version": "1.0.0",
|
||||
"dependencies": {
|
||||
"node-gyp": "12.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@isaacs/fs-minipass": {
|
||||
"version": "4.0.1",
|
||||
"resolved": "https://registry.npmjs.org/@isaacs/fs-minipass/-/fs-minipass-4.0.1.tgz",
|
||||
"integrity": "sha512-wgm9Ehl2jpeqP3zw/7mo3kRHFp5MEDhqAdwy1fTGkHAwnkGOVsgpvQhL8B5n1qlb01jV3n/bI0ZfZp5lWA1k4w==",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"minipass": "^7.0.4"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/abbrev": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/abbrev/-/abbrev-4.0.0.tgz",
|
||||
"integrity": "sha512-a1wflyaL0tHtJSmLSOVybYhy22vRih4eduhhrkcjgrWGnRfrZtovJ2FRjxuTtkkj47O/baf0R86QU5OuYpz8fA==",
|
||||
"license": "ISC",
|
||||
"engines": {
|
||||
"node": "^20.17.0 || >=22.9.0"
|
||||
}
|
||||
},
|
||||
"node_modules/chownr": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/chownr/-/chownr-3.0.0.tgz",
|
||||
"integrity": "sha512-+IxzY9BZOQd/XuYPRmrvEVjF/nqj5kgT4kEq7VofrDoM1MxoRjEWkrCC3EtLi59TVawxTAn+orJwFQcrqEN1+g==",
|
||||
"license": "BlueOak-1.0.0",
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/env-paths": {
|
||||
"version": "2.2.1",
|
||||
"resolved": "https://registry.npmjs.org/env-paths/-/env-paths-2.2.1.tgz",
|
||||
"integrity": "sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/exponential-backoff": {
|
||||
"version": "3.1.3",
|
||||
"resolved": "https://registry.npmjs.org/exponential-backoff/-/exponential-backoff-3.1.3.tgz",
|
||||
"integrity": "sha512-ZgEeZXj30q+I0EN+CbSSpIyPaJ5HVQD18Z1m+u1FXbAeT94mr1zw50q4q6jiiC447Nl/YTcIYSAftiGqetwXCA==",
|
||||
"license": "Apache-2.0"
|
||||
},
|
||||
"node_modules/fdir": {
|
||||
"version": "6.5.0",
|
||||
"resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz",
|
||||
"integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=12.0.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"picomatch": "^3 || ^4"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"picomatch": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/graceful-fs": {
|
||||
"version": "4.2.11",
|
||||
"resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz",
|
||||
"integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==",
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/isexe": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/isexe/-/isexe-4.0.0.tgz",
|
||||
"integrity": "sha512-FFUtZMpoZ8RqHS3XeXEmHWLA4thH+ZxCv2lOiPIn1Xc7CxrqhWzNSDzD+/chS/zbYezmiwWLdQC09JdQKmthOw==",
|
||||
"license": "BlueOak-1.0.0",
|
||||
"engines": {
|
||||
"node": ">=20"
|
||||
}
|
||||
},
|
||||
"node_modules/minipass": {
|
||||
"version": "7.1.3",
|
||||
"resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.3.tgz",
|
||||
"integrity": "sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==",
|
||||
"license": "BlueOak-1.0.0",
|
||||
"engines": {
|
||||
"node": ">=16 || 14 >=14.17"
|
||||
}
|
||||
},
|
||||
"node_modules/minizlib": {
|
||||
"version": "3.1.0",
|
||||
"resolved": "https://registry.npmjs.org/minizlib/-/minizlib-3.1.0.tgz",
|
||||
"integrity": "sha512-KZxYo1BUkWD2TVFLr0MQoM8vUUigWD3LlD83a/75BqC+4qE0Hb1Vo5v1FgcfaNXvfXzr+5EhQ6ing/CaBijTlw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"minipass": "^7.1.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 18"
|
||||
}
|
||||
},
|
||||
"node_modules/node-gyp": {
|
||||
"version": "12.4.0",
|
||||
"resolved": "https://registry.npmjs.org/node-gyp/-/node-gyp-12.4.0.tgz",
|
||||
"integrity": "sha512-OMcPNvqTCFUnNaBlmdgq+lfNqY7gTiSmNRDjY3uAXRyudeKZEZxu3CLtjMQrx4zZxCX2b/mpNqTtwuCJgXhHkw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"env-paths": "^2.2.0",
|
||||
"exponential-backoff": "^3.1.1",
|
||||
"graceful-fs": "^4.2.6",
|
||||
"nopt": "^9.0.0",
|
||||
"proc-log": "^6.0.0",
|
||||
"semver": "^7.3.5",
|
||||
"tar": "^7.5.4",
|
||||
"tinyglobby": "^0.2.12",
|
||||
"undici": "^6.25.0",
|
||||
"which": "^6.0.0"
|
||||
},
|
||||
"bin": {
|
||||
"node-gyp": "bin/node-gyp.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^20.17.0 || >=22.9.0"
|
||||
}
|
||||
},
|
||||
"node_modules/nopt": {
|
||||
"version": "9.0.0",
|
||||
"resolved": "https://registry.npmjs.org/nopt/-/nopt-9.0.0.tgz",
|
||||
"integrity": "sha512-Zhq3a+yFKrYwSBluL4H9XP3m3y5uvQkB/09CwDruCiRmR/UJYnn9W4R48ry0uGC70aeTPKLynBtscP9efFFcPw==",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"abbrev": "^4.0.0"
|
||||
},
|
||||
"bin": {
|
||||
"nopt": "bin/nopt.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^20.17.0 || >=22.9.0"
|
||||
}
|
||||
},
|
||||
"node_modules/picomatch": {
|
||||
"version": "4.0.7",
|
||||
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.7.tgz",
|
||||
"integrity": "sha512-qcJu88Q2IWqJsDD529JKMdwGm/dvInW4HvQnRwiH9JtihJvzGOscDtHE3x1pBKeUOTysQ8kVmLnJ2kJu7yhcGA==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/jonschlinkert"
|
||||
}
|
||||
},
|
||||
"node_modules/proc-log": {
|
||||
"version": "6.1.0",
|
||||
"resolved": "https://registry.npmjs.org/proc-log/-/proc-log-6.1.0.tgz",
|
||||
"integrity": "sha512-iG+GYldRf2BQ0UDUAd6JQ/RwzaQy6mXmsk/IzlYyal4A4SNFw54MeH4/tLkF4I5WoWG9SQwuqWzS99jaFQHBuQ==",
|
||||
"license": "ISC",
|
||||
"engines": {
|
||||
"node": "^20.17.0 || >=22.9.0"
|
||||
}
|
||||
},
|
||||
"node_modules/semver": {
|
||||
"version": "7.8.5",
|
||||
"resolved": "https://registry.npmjs.org/semver/-/semver-7.8.5.tgz",
|
||||
"integrity": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==",
|
||||
"license": "ISC",
|
||||
"bin": {
|
||||
"semver": "bin/semver.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
}
|
||||
},
|
||||
"node_modules/tar": {
|
||||
"version": "7.5.22",
|
||||
"resolved": "https://registry.npmjs.org/tar/-/tar-7.5.22.tgz",
|
||||
"integrity": "sha512-MFO/QzvtAOmJbkhOaCTvbGcFN9L9b+JunIsDwaKljSOdcLMea3NJ1k9Usz/rjdfSXTq4dfzfeS7W4p4YOAAHeA==",
|
||||
"license": "BlueOak-1.0.0",
|
||||
"dependencies": {
|
||||
"@isaacs/fs-minipass": "^4.0.0",
|
||||
"chownr": "^3.0.0",
|
||||
"minipass": "^7.1.2",
|
||||
"minizlib": "^3.1.0",
|
||||
"yallist": "^5.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/tinyglobby": {
|
||||
"version": "0.2.17",
|
||||
"resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.17.tgz",
|
||||
"integrity": "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"fdir": "^6.5.0",
|
||||
"picomatch": "^4.0.4"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/SuperchupuDev"
|
||||
}
|
||||
},
|
||||
"node_modules/undici": {
|
||||
"version": "6.29.0",
|
||||
"resolved": "https://registry.npmjs.org/undici/-/undici-6.29.0.tgz",
|
||||
"integrity": "sha512-R+RODBqp6i2pPflGdq+xIOUkl+RNfGgHwoinecKu/JCuf2uO06cOKoDbI2P7Dn6KcswdKwrczbU6IYJ6K8X+wg==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=18.17"
|
||||
}
|
||||
},
|
||||
"node_modules/which": {
|
||||
"version": "6.0.1",
|
||||
"resolved": "https://registry.npmjs.org/which/-/which-6.0.1.tgz",
|
||||
"integrity": "sha512-oGLe46MIrCRqX7ytPUf66EAYvdeMIZYn3WaocqqKZAxrBpkqHfL/qvTyJ/bTk5+AqHCjXmrv3CEWgy368zhRUg==",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"isexe": "^4.0.0"
|
||||
},
|
||||
"bin": {
|
||||
"node-which": "bin/which.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^20.17.0 || >=22.9.0"
|
||||
}
|
||||
},
|
||||
"node_modules/yallist": {
|
||||
"version": "5.0.0",
|
||||
"resolved": "https://registry.npmjs.org/yallist/-/yallist-5.0.0.tgz",
|
||||
"integrity": "sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw==",
|
||||
"license": "BlueOak-1.0.0",
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"name": "qinglong-pnpm10-runtime-node-gyp",
|
||||
"version": "1.0.0",
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
"node-gyp": "12.4.0"
|
||||
},
|
||||
"overrides": {
|
||||
"http-cache-semantics": "4.3.0",
|
||||
"brace-expansion@2": "2.1.7"
|
||||
}
|
||||
}
|
||||
+24869
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,465 @@
|
||||
packages:
|
||||
- .meta-updater
|
||||
- __typecheck__
|
||||
- __typings__
|
||||
- __utils__/*
|
||||
- '!__utils__/build-artifacts'
|
||||
- builder/*
|
||||
- cache/*
|
||||
- catalogs/*
|
||||
- cli/*
|
||||
- completion/*
|
||||
- config/*
|
||||
- crypto/*
|
||||
- dedupe/*
|
||||
- deps/*
|
||||
- env/*
|
||||
- exec/*
|
||||
- fetching/*
|
||||
- fs/*
|
||||
- hooks/*
|
||||
- lockfile/*
|
||||
- network/*
|
||||
- modules-mounter/*
|
||||
- object/*
|
||||
- packages/*
|
||||
- pkg-manager/*
|
||||
- pkg-manifest/*
|
||||
- patching/*
|
||||
- pnpm
|
||||
- pnpm/dev
|
||||
- testing/*
|
||||
- tools/*
|
||||
- worker
|
||||
- pnpm/artifacts/*
|
||||
- registry/*
|
||||
- releasing/*
|
||||
- resolving/*
|
||||
- reviewing/*
|
||||
- semver/*
|
||||
- store/*
|
||||
- text/*
|
||||
- workspace/*
|
||||
- yaml/*
|
||||
- '!**/example/**'
|
||||
- '!**/test/**'
|
||||
- '!resolving/local-resolver/example-package/**'
|
||||
allowBuilds:
|
||||
core-js: false
|
||||
esbuild: true
|
||||
fuse-native: true
|
||||
ghooks: true
|
||||
auditConfig:
|
||||
ignoreCves:
|
||||
- CVE-2025-56200
|
||||
ignoreGhsas:
|
||||
- GHSA-6rw7-vpxm-498p
|
||||
- GHSA-76c9-3jph-rj3q
|
||||
- GHSA-ffrw-9mx8-89p8
|
||||
- GHSA-mh29-5h37-fv8m
|
||||
- GHSA-w5hq-g745-h8pq
|
||||
- GHSA-xxjr-mmjv-4gpg
|
||||
- GHSA-h67p-54hq-rp68
|
||||
- GHSA-64mm-vxmg-q3vj
|
||||
catalog:
|
||||
'@babel/core': ^7.29.6
|
||||
'@babel/preset-typescript': ^7.28.5
|
||||
'@babel/types': ^7.29.0
|
||||
'@changesets/cli': ^2.31.0
|
||||
'@commitlint/cli': ^17.8.1
|
||||
'@commitlint/config-conventional': ^17.8.1
|
||||
'@commitlint/prompt-cli': ^17.8.1
|
||||
'@eslint/eslintrc': 3.1.0
|
||||
'@eslint/js': 9.9.1
|
||||
'@gwhitney/detect-indent': 7.0.1
|
||||
'@jest/globals': 29.7.0
|
||||
'@pnpm/byline': ^1.0.0
|
||||
'@pnpm/colorize-semver-diff': ^1.0.1
|
||||
'@pnpm/config.env-replace': ^3.0.2
|
||||
'@pnpm/config.nerf-dart': ^1.0.1
|
||||
'@pnpm/exec': ^2.0.0
|
||||
'@pnpm/log.group': 3.0.1
|
||||
'@pnpm/logger': ^1001.0.1
|
||||
'@pnpm/meta-updater': 2.0.6
|
||||
'@pnpm/network.agent': ^2.0.3
|
||||
'@pnpm/nopt': ^0.3.1
|
||||
'@pnpm/npm-conf': 3.0.3
|
||||
'@pnpm/npm-lifecycle': ^1001.0.0
|
||||
'@pnpm/npm-package-arg': ^2.0.0
|
||||
'@pnpm/os.env.path-extender': ^2.0.3
|
||||
'@pnpm/patch-package': 0.0.1
|
||||
'@pnpm/registry-mock': 5.2.0
|
||||
'@pnpm/semver-diff': ^1.1.0
|
||||
'@pnpm/tabtab': ^0.5.4
|
||||
'@pnpm/tgz-fixtures': 0.0.0
|
||||
'@pnpm/util.lex-comparator': ^3.0.2
|
||||
'@pnpm/workspace.find-packages': ^1000.0.65
|
||||
'@pnpm/workspace.read-manifest': ^1000.3.1
|
||||
'@reflink/reflink': 0.1.19
|
||||
'@rushstack/worker-pool': 0.4.9
|
||||
'@types/adm-zip': ^0.5.8
|
||||
'@types/archy': 0.0.33
|
||||
'@types/cross-spawn': ^6.0.6
|
||||
'@types/fs-extra': ^9.0.13
|
||||
'@types/graceful-fs': ^4.1.9
|
||||
'@types/hosted-git-info': ^3.0.5
|
||||
'@types/ini': 1.3.31
|
||||
'@types/is-gzip': 2.0.0
|
||||
'@types/is-windows': ^1.0.2
|
||||
'@types/isexe': 2.0.2
|
||||
'@types/jest': ^29.5.14
|
||||
'@types/js-yaml': ^4.0.9
|
||||
'@types/lodash.kebabcase': 4.1.9
|
||||
'@types/lodash.throttle': 4.1.7
|
||||
'@types/micromatch': ^4.0.10
|
||||
'@types/node': ^18.19.130
|
||||
'@types/normalize-package-data': ^2.4.4
|
||||
'@types/normalize-path': ^3.0.2
|
||||
'@types/object-hash': 3.0.6
|
||||
'@types/parse-json': ^4.0.2
|
||||
'@types/pnpm__byline': npm:@types/byline@^4.2.36
|
||||
'@types/proxyquire': ^1.3.31
|
||||
'@types/ramda': 0.29.12
|
||||
'@types/retry': ^0.12.5
|
||||
'@types/rimraf': 3.0.2
|
||||
'@types/semver': 7.5.3
|
||||
'@types/signal-exit': ^3.0.4
|
||||
'@types/sinon': ^10.0.20
|
||||
'@types/ssri': ^7.1.5
|
||||
'@types/tar': ^6.1.13
|
||||
'@types/tar-stream': ^2.2.3
|
||||
'@types/touch': ^3.1.5
|
||||
'@types/uuid': ^8.3.4
|
||||
'@types/validate-npm-package-name': ^4.0.2
|
||||
'@types/which': ^2.0.2
|
||||
'@types/write-file-atomic': ^4.0.3
|
||||
'@types/yarnpkg__lockfile': ^1.1.9
|
||||
'@types/yazl': ^3.3.1
|
||||
'@types/zkochan__table': npm:@types/table@6.0.0
|
||||
'@typescript-eslint/eslint-plugin': 6.18.1
|
||||
'@typescript-eslint/parser': 6.18.1
|
||||
'@yarnpkg/core': 4.2.0
|
||||
'@yarnpkg/extensions': 2.0.3
|
||||
'@yarnpkg/lockfile': ^1.1.0
|
||||
'@yarnpkg/nm': 4.0.5
|
||||
'@yarnpkg/parsers': 3.0.3
|
||||
'@yarnpkg/pnp': 4.0.8
|
||||
'@zkochan/cmd-shim': ^7.0.0
|
||||
'@zkochan/diable': ^1.0.2
|
||||
'@zkochan/retry': ^0.2.0
|
||||
'@zkochan/rimraf': ^3.0.2
|
||||
'@zkochan/table': ^2.0.1
|
||||
adm-zip: ^0.6.1
|
||||
ansi-diff: ^1.2.0
|
||||
archy: ^1.0.0
|
||||
better-path-resolve: 1.0.0
|
||||
bin-links: ^4.0.4
|
||||
bole: ^5.0.29
|
||||
boxen: npm:@zkochan/boxen@5.1.2
|
||||
c8: ^7.14.0
|
||||
camelcase: ^6.3.0
|
||||
camelcase-keys: ^6.2.2
|
||||
can-link: ^2.0.0
|
||||
can-write-to-dir: ^1.1.1
|
||||
chalk: ^4.1.2
|
||||
ci-info: ^3.9.0
|
||||
cli-columns: ^4.0.0
|
||||
cli-truncate: ^2.1.0
|
||||
cmd-extension: ^1.0.2
|
||||
comver-to-semver: ^1.0.0
|
||||
concurrently: 8.2.1
|
||||
cross-env: ^7.0.3
|
||||
cross-spawn: ^7.0.6
|
||||
cspell: 8.17.5
|
||||
deep-require-cwd: 1.0.0
|
||||
delay: ^5.0.0
|
||||
detect-libc: ^2.1.2
|
||||
didyoumean2: ^6.0.1
|
||||
dint: ^5.1.0
|
||||
dir-is-case-sensitive: ^2.0.0
|
||||
encode-registry: ^3.0.1
|
||||
enquirer: ^2.4.1
|
||||
esbuild: ^0.25.12
|
||||
escape-string-regexp: ^4.0.0
|
||||
eslint: ^8.57.1
|
||||
eslint-config-standard-with-typescript: ^39.1.1
|
||||
eslint-plugin-import: ^2.32.0
|
||||
eslint-plugin-n: ^16.6.2
|
||||
eslint-plugin-node: ^11.1.0
|
||||
eslint-plugin-promise: ^6.6.0
|
||||
eslint-plugin-regexp: 2.7.0
|
||||
execa: npm:safe-execa@0.1.2
|
||||
exists-link: 2.0.0
|
||||
fast-deep-equal: ^3.1.3
|
||||
fast-glob: ^3.3.3
|
||||
filenamify: ^4.3.0
|
||||
find-up: ^5.0.0
|
||||
fs-extra: ^11.3.4
|
||||
fuse-native: ^2.2.6
|
||||
get-npm-tarball-url: ^2.1.0
|
||||
get-port: ^5.1.1
|
||||
ghooks: 2.0.4
|
||||
graceful-fs: ^4.2.11
|
||||
graceful-git: ^4.0.0
|
||||
graph-cycles: 1.2.1
|
||||
hosted-git-info: npm:@pnpm/hosted-git-info@1.0.0
|
||||
https-proxy-server-express: 0.1.2
|
||||
husky: ^9.1.7
|
||||
hyperdrive-schemas: ^2.0.0
|
||||
ini: 4.1.1
|
||||
is-gzip: 2.0.0
|
||||
is-inner-link: ^4.0.0
|
||||
is-port-reachable: 3.0.0
|
||||
is-subdir: ^1.2.0
|
||||
is-windows: ^1.0.2
|
||||
isexe: 2.0.0
|
||||
jest: ^29.7.0
|
||||
jest-diff: ^29.7.0
|
||||
js-yaml: npm:@zkochan/js-yaml@0.0.11
|
||||
json5: ^2.2.3
|
||||
keyv: 4.5.4
|
||||
lcov-result-merger: ^3.3.0
|
||||
load-json-file: ^6.2.0
|
||||
lodash.kebabcase: ^4.1.1
|
||||
lodash.throttle: 4.1.1
|
||||
loud-rejection: ^2.2.0
|
||||
lru-cache: ^10.4.3
|
||||
make-empty-dir: ^3.0.2
|
||||
mdast-util-to-string: ^2.0.0
|
||||
mem: ^8.1.1
|
||||
micromatch: ^4.0.8
|
||||
nock: 13.3.4
|
||||
node-fetch: npm:@pnpm/node-fetch@1.0.0
|
||||
normalize-newline: 3.0.0
|
||||
normalize-package-data: ^7.0.1
|
||||
normalize-path: ^3.0.0
|
||||
normalize-registry-url: 2.0.1
|
||||
npm-packlist: 5.1.3
|
||||
object-hash: 3.0.0
|
||||
openpgp: ^6.3.1
|
||||
'@openpgp/web-stream-tools': 0.3.1
|
||||
p-defer: ^3.0.0
|
||||
p-every: ^2.0.0
|
||||
p-filter: ^2.1.0
|
||||
p-limit: ^3.1.0
|
||||
p-map-values: ^1.0.0
|
||||
p-memoize: 4.0.1
|
||||
p-queue: ^6.6.2
|
||||
parse-json: ^5.2.0
|
||||
parse-npm-tarball-url: ^4.0.0
|
||||
path-absolute: ^1.0.1
|
||||
path-exists: ^4.0.0
|
||||
path-name: ^1.0.0
|
||||
path-temp: ^2.1.1
|
||||
pidtree: ^0.6.0
|
||||
pkg: npm:@yao-pkg/pkg@5.12.0
|
||||
preferred-pm: ^3.1.4
|
||||
pretty-bytes: ^5.6.0
|
||||
pretty-ms: ^7.0.1
|
||||
process-exists: ^4.1.0
|
||||
promise-share: ^1.0.0
|
||||
proxyquire: ^2.1.3
|
||||
ps-list: ^7.2.0
|
||||
publish-packed: ^4.1.2
|
||||
ramda: npm:@pnpm/ramda@0.28.1
|
||||
read-ini-file: 4.0.0
|
||||
read-yaml-file: ^2.1.0
|
||||
realpath-missing: ^1.1.0
|
||||
remark-parse: ^9.0.0
|
||||
remark-stringify: ^9.0.1
|
||||
rename-overwrite: ^6.0.6
|
||||
render-help: ^1.0.3
|
||||
resolve-link-target: ^2.0.0
|
||||
rimraf: ^3.0.2
|
||||
root-link-target: ^3.1.0
|
||||
run-groups: ^3.0.1
|
||||
rxjs: ^7.8.2
|
||||
safe-buffer: 5.2.1
|
||||
safe-execa: ^0.1.4
|
||||
safe-promise-defer: ^1.0.1
|
||||
sanitize-filename: ^1.6.4
|
||||
semver: ^7.7.4
|
||||
semver-range-intersect: ^0.3.1
|
||||
semver-utils: ^1.1.4
|
||||
shlex: ^2.1.2
|
||||
shx: ^0.3.4
|
||||
signal-exit: ^3.0.7
|
||||
sinon: ^16.1.3
|
||||
sort-keys: ^4.2.0
|
||||
split-cmd: ^1.1.0
|
||||
split2: ^4.2.0
|
||||
ssri: 10.0.5
|
||||
stacktracey: ^2.2.0
|
||||
string-length: ^4.0.2
|
||||
strip-bom: ^4.0.0
|
||||
strip-comments-strings: 1.2.0
|
||||
symlink-dir: ^6.0.5
|
||||
tar: ^7.5.21
|
||||
tar-stream: ^2.2.0
|
||||
tempy: ^1.0.1
|
||||
terminal-link: ^2.1.1
|
||||
tinyglobby: ^0.2.16
|
||||
touch: 3.1.0
|
||||
tree-kill: ^1.2.2
|
||||
ts-jest: 29.2.3
|
||||
ts-jest-resolver: 2.0.1
|
||||
ts-node: ^10.9.2
|
||||
typescript: 5.5.4
|
||||
unified: ^9.2.2
|
||||
uuid: ^9.0.1
|
||||
validate-npm-package-name: 5.0.0
|
||||
verdaccio: 5.20.1
|
||||
version-selector-type: ^3.0.0
|
||||
which: npm:@pnpm/which@^3.0.1
|
||||
write-file-atomic: ^5.0.1
|
||||
write-ini-file: 4.0.1
|
||||
write-json-file: ^4.3.0
|
||||
write-json5-file: ^3.1.0
|
||||
write-pkg: 4.0.0
|
||||
write-yaml-file: ^5.0.0
|
||||
yaml: ^2.8.4
|
||||
yaml-tag: 1.1.0
|
||||
yazl: ^3.3.1
|
||||
catalogMode: strict
|
||||
cleanupUnusedCatalogs: true
|
||||
enableGlobalVirtualStore: false
|
||||
enablePrePostScripts: false
|
||||
engineStrict: false
|
||||
gitChecks: false
|
||||
hoistPattern:
|
||||
- jest-runner
|
||||
minimumReleaseAge: 1440
|
||||
minimumReleaseAgeExclude:
|
||||
- '@pnpm/*'
|
||||
- body-parser@2.2.1
|
||||
- express@4.22.1
|
||||
- glob@11.1.0
|
||||
- jws@3.2.3
|
||||
- normalize-registry-url
|
||||
- path-temp@2.1.1
|
||||
- parse-npm-tarball-url@4.0.0
|
||||
- pnpm
|
||||
- publish-packed@5.0.0
|
||||
- run-groups@4.0.0
|
||||
- tar@7.5.11
|
||||
- tmp@0.2.6
|
||||
- lodash@4.17.23
|
||||
optimisticRepeatInstall: true
|
||||
overrides:
|
||||
'@yarnpkg/fslib@2': '3'
|
||||
adm-zip@<0.6.1: ^0.6.1
|
||||
ajv@>=7.0.0-alpha.0 <8.18.0: '>=8.18.0'
|
||||
baseline-browser-mapping@<2.11.0: ^2.11.0
|
||||
body-parser@<1.20.6: ^1.20.6
|
||||
browserslist@<4.28.7: ^4.28.7
|
||||
clipanion: 3.2.0-rc.6
|
||||
cookie@<0.7.0: '>=0.7.0'
|
||||
cross-spawn@<7.0.5: '>=7.0.5'
|
||||
debug@<3.1.0: '>=3.1.0'
|
||||
express@<4.22.1: ^4.22.1
|
||||
fast-uri@>=3.0.0 <3.1.6: ^3.1.6
|
||||
follow-redirects@<=1.15.5: '>=1.15.6'
|
||||
glob-parent@<5.1.2: '>=5.1.2'
|
||||
handlebars@>=4.0.0 <4.7.9: '>=4.7.9'
|
||||
handlebars@>=4.0.0 <=4.7.8: '>=4.7.9'
|
||||
hosted-git-info@1: 'catalog:'
|
||||
http-proxy-middleware@<2.0.7: ^2.0.7
|
||||
ip-address@<=10.3.0: '>=10.3.1'
|
||||
isexe@3: 3.1.1
|
||||
istanbul-reports: npm:@zkochan/istanbul-reports
|
||||
js-yaml@<3.15.2: ^3.15.2
|
||||
js-yaml@^4.0.0: 'catalog:'
|
||||
json5@<2.2.2: 'catalog:'
|
||||
jsonwebtoken@<=8.5.1: '>=9.0.0'
|
||||
jws@<3.2.3: ^3.2.3
|
||||
lodash@<=4.17.23: '>=4.18.0'
|
||||
lodash@>=4.0.0 <=4.17.22: '>=4.17.23'
|
||||
lodash@>=4.0.0 <=4.17.23: '>=4.18.0'
|
||||
minimatch@<3.1.4: ^3.1.4
|
||||
minimatch@>=9.0.0 <9.0.7: ^9.0.7
|
||||
nopt@5: npm:@pnpm/nopt@^0.2.1
|
||||
on-headers@<1.1.0: '>=1.1.0'
|
||||
path-to-regexp@<0.1.12: ^0.1.12
|
||||
path-to-regexp@>=4.0.0 <6.3.0: '>=6.3.0'
|
||||
path-to-regexp@>=7.0.0 <8.0.0: '>=8.0.0'
|
||||
qs@<6.16.0: ^6.16.0
|
||||
request: npm:postman-request@2.88.1-postman.40
|
||||
semver@<7.5.2: 'catalog:'
|
||||
send@<0.19.0: ^0.19.0
|
||||
serve-static@<1.16.0: ^1.16.0
|
||||
shell-quote@<1.9.0: '>=1.9.0'
|
||||
shell-quote: 1.10.0
|
||||
socks@2: ^2.8.1
|
||||
tar@<=7.5.20: '>=7.5.21'
|
||||
tmp@<0.2.7: '>=0.2.7'
|
||||
tough-cookie@<4.1.3: '>=4.1.3'
|
||||
validator@<13.15.22: '>=13.15.22'
|
||||
yaml@<2.2.2: '>=2.2.2'
|
||||
brace-expansion@<2.0.0: 1.1.21
|
||||
brace-expansion@>=2.0.0 <3.0.0: 2.1.7
|
||||
brace-expansion@>=3.0.0: 5.0.12
|
||||
http-cache-semantics: 4.3.0
|
||||
node-gyp: 12.4.0
|
||||
ip-address: 10.7.1
|
||||
uuid: 11.1.1
|
||||
packageExtensions:
|
||||
'@babel/parser':
|
||||
peerDependencies:
|
||||
'@babel/types': '*'
|
||||
jest-circus:
|
||||
dependencies:
|
||||
slash: '3'
|
||||
remark-parse:
|
||||
peerDependencies:
|
||||
unified: '*'
|
||||
remark-stringify:
|
||||
peerDependencies:
|
||||
unified: '*'
|
||||
patchedDependencies:
|
||||
'@yao-pkg/pkg': __patches__/pkg.patch
|
||||
graceful-fs@4.2.11: __patches__/graceful-fs@4.2.11.patch
|
||||
mdast-util-to-string@2.0.0: __patches__/mdast-util-to-string@2.0.0.patch
|
||||
normalize-package-data@7.0.1: __patches__/normalize-package-data@7.0.1.patch
|
||||
patchesDir: __patches__
|
||||
publishBranch: main
|
||||
resolutionMode: lowest-direct
|
||||
savePrefix: ''
|
||||
sharedWorkspaceLockfile: true
|
||||
updateConfig:
|
||||
ignoreDependencies:
|
||||
- boxen
|
||||
- camelcase
|
||||
- camelcase-keys
|
||||
- chalk
|
||||
- detect-indent
|
||||
- escape-string-regexp
|
||||
- filenamify
|
||||
- find-up
|
||||
- get-port
|
||||
- is-port-reachable
|
||||
- load-json-file
|
||||
- mem
|
||||
- npm-packlist
|
||||
- node-fetch
|
||||
- normalize-newline
|
||||
- p-defer
|
||||
- p-filter
|
||||
- p-limit
|
||||
- p-memoize
|
||||
- p-queue
|
||||
- parse-json
|
||||
- path-exists
|
||||
- pretty-bytes
|
||||
- pretty-ms
|
||||
- process-exists
|
||||
- ps-list
|
||||
- sort-keys
|
||||
- string-length
|
||||
- strip-bom
|
||||
- tempy
|
||||
- unique-string
|
||||
- write-json-file
|
||||
- write-pkg
|
||||
verifyDepsBeforeRun: install
|
||||
managePackageManagerVersions: false
|
||||
packageManagerStrict: false
|
||||
@@ -0,0 +1 @@
|
||||
registry=https://registry.npmjs.org/
|
||||
@@ -0,0 +1,41 @@
|
||||
{
|
||||
"tag": "v10.34.6",
|
||||
"ref": {
|
||||
"ref": "refs/tags/v10.34.6",
|
||||
"node_id": "MDM6UmVmNTA1NjU0MzA6cmVmcy90YWdzL3YxMC4zNC42",
|
||||
"url": "https://api.github.com/repos/pnpm/pnpm/git/refs/tags/v10.34.6",
|
||||
"object": {
|
||||
"sha": "656c6b25d02a924ead9088ce6c69bba23b0d6d51",
|
||||
"type": "tag",
|
||||
"url": "https://api.github.com/repos/pnpm/pnpm/git/tags/656c6b25d02a924ead9088ce6c69bba23b0d6d51"
|
||||
}
|
||||
},
|
||||
"tagObject": {
|
||||
"node_id": "TA_kwDOAwORNtoAKDY1NmM2YjI1ZDAyYTkyNGVhZDkwODhjZTZjNjliYmEyM2IwZDZkNTE",
|
||||
"sha": "656c6b25d02a924ead9088ce6c69bba23b0d6d51",
|
||||
"url": "https://api.github.com/repos/pnpm/pnpm/git/tags/656c6b25d02a924ead9088ce6c69bba23b0d6d51",
|
||||
"tagger": {
|
||||
"name": "Zoltan Kochan",
|
||||
"email": "z@kochan.io",
|
||||
"date": "2026-09-28T18:14:05Z"
|
||||
},
|
||||
"object": {
|
||||
"sha": "9287c31cea69206c4eeca7f21cd21df81d0b93e7",
|
||||
"type": "commit",
|
||||
"url": "https://api.github.com/repos/pnpm/pnpm/git/commits/9287c31cea69206c4eeca7f21cd21df81d0b93e7"
|
||||
},
|
||||
"tag": "v10.34.6",
|
||||
"message": "=v10.34.6\n-----BEGIN PGP SIGNATURE-----\n\niQIzBAABCgAdFiEEe3TRKZVotYa6mWK1ZJ5NSvdOfewFAmq6rm0ACgkQZJ5NSvdO\nfew2+g/8D3KQPdz48P6WzDBjdk+p39nfUo9Yus9/NrNuUwAnZM7AfloI+U964zdO\nT81cTmlO4lh2psfE+QPrO+l/dKXtaxG8JrIASqFoNZ5JV+j091hneRQAj/ZrKq0E\nI/MdVtISCLCEHslt6wZeKZZQqTkJMkwbdaTxbjVPCvRPxrCbBKWtRs3z/7Uw5FND\nXvj+/eo8EBidJOHxxSk0Ib5lz3+vXHGx/tcilVmAdQVK29MMsEVwbck4Ve4rcbq+\n4hxjfxJrH1xJ8zI8zlAvpYQ51bab1DJZJ2hXzuhlGyZbgwRiPTLMTQKB+DnbDzaD\nVaRWQES+bmmKQlGplw7JuA7oryBBJo5nNldxwG/8YSbBTtxcyQzGJi5r2Of1XUth\no041P4f53IK2xhXdyRvgofTVRUMr/i6zC+AEW+/1h/RWc2iDXImjXlzN0BKz7bk3\nbqufant5hDMeOsJETAtZ3i7uOyfmqUOkR4gmumKO+XRgTI3OlMMaDIdrfAx5jHi4\nh8dXgzaplJ7KVzWkkmucF4Hv4ffkq/orJp4FE9KOCtIdzF4pnDmoB3jf70oE8V1H\nHcqK53ztD9fT0l3TKArT6A48s/cZBS/CAr9BHkIg7QrCITEZLCC2zUrz2f6KECgc\noVVsd2+Ww4h7DPCQqM5nsKXLF/wyYHGWzJJ+FQW7d+PX0zlw21w=\n=GC4w\n-----END PGP SIGNATURE-----\n",
|
||||
"verification": {
|
||||
"verified": true,
|
||||
"reason": "valid",
|
||||
"signature": "-----BEGIN PGP SIGNATURE-----\n\niQIzBAABCgAdFiEEe3TRKZVotYa6mWK1ZJ5NSvdOfewFAmq6rm0ACgkQZJ5NSvdO\nfew2+g/8D3KQPdz48P6WzDBjdk+p39nfUo9Yus9/NrNuUwAnZM7AfloI+U964zdO\nT81cTmlO4lh2psfE+QPrO+l/dKXtaxG8JrIASqFoNZ5JV+j091hneRQAj/ZrKq0E\nI/MdVtISCLCEHslt6wZeKZZQqTkJMkwbdaTxbjVPCvRPxrCbBKWtRs3z/7Uw5FND\nXvj+/eo8EBidJOHxxSk0Ib5lz3+vXHGx/tcilVmAdQVK29MMsEVwbck4Ve4rcbq+\n4hxjfxJrH1xJ8zI8zlAvpYQ51bab1DJZJ2hXzuhlGyZbgwRiPTLMTQKB+DnbDzaD\nVaRWQES+bmmKQlGplw7JuA7oryBBJo5nNldxwG/8YSbBTtxcyQzGJi5r2Of1XUth\no041P4f53IK2xhXdyRvgofTVRUMr/i6zC+AEW+/1h/RWc2iDXImjXlzN0BKz7bk3\nbqufant5hDMeOsJETAtZ3i7uOyfmqUOkR4gmumKO+XRgTI3OlMMaDIdrfAx5jHi4\nh8dXgzaplJ7KVzWkkmucF4Hv4ffkq/orJp4FE9KOCtIdzF4pnDmoB3jf70oE8V1H\nHcqK53ztD9fT0l3TKArT6A48s/cZBS/CAr9BHkIg7QrCITEZLCC2zUrz2f6KECgc\noVVsd2+Ww4h7DPCQqM5nsKXLF/wyYHGWzJJ+FQW7d+PX0zlw21w=\n=GC4w\n-----END PGP SIGNATURE-----\n",
|
||||
"payload": "object 9287c31cea69206c4eeca7f21cd21df81d0b93e7\ntype commit\ntag v10.34.6\ntagger Zoltan Kochan <z@kochan.io> 1790619245 +0200\n\n=v10.34.6\n",
|
||||
"verified_at": "2026-09-28T18:14:14Z"
|
||||
}
|
||||
},
|
||||
"commit": "9287c31cea69206c4eeca7f21cd21df81d0b93e7",
|
||||
"url": "https://codeload.github.com/pnpm/pnpm/tar.gz/9287c31cea69206c4eeca7f21cd21df81d0b93e7",
|
||||
"sha256": "260a2810d2710baac9d82852dada85d7a136a0043a4708090e406f79834e5d17",
|
||||
"sourcePath": "/private/tmp/qinglong-tools-fix-preview/pnpm10-official-source/pnpm-9287c31cea69206c4eeca7f21cd21df81d0b93e7"
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
#!/usr/bin/env node
|
||||
'use strict';
|
||||
const path = require('node:path');
|
||||
const { spawnSync } = require('node:child_process');
|
||||
const toolsHome = path.resolve(__dirname, '..');
|
||||
const prefix = path.resolve(toolsHome, '../../..');
|
||||
const args = process.argv.slice(2);
|
||||
const end = args.indexOf('--');
|
||||
const options = end < 0 ? args : args.slice(0, end);
|
||||
const global = options.some(arg => ['-g', '--global', '--location=global'].includes(arg));
|
||||
const cli = path.join(toolsHome, 'node_modules/pnpm/bin/pnpm.cjs');
|
||||
const forwarded = global ? ['--config.dangerously-allow-all-builds=true', ...args] : args;
|
||||
const result = spawnSync(process.execPath, [cli, ...forwarded], {
|
||||
stdio: 'inherit',
|
||||
env: {
|
||||
...process.env,
|
||||
PATH: path.join(prefix, 'bin') + path.delimiter + (process.env.PATH || ''),
|
||||
npm_config_node_gyp: path.join(toolsHome, 'node_modules/node-gyp/bin/node-gyp.js'),
|
||||
},
|
||||
});
|
||||
if (result.error) {
|
||||
console.error(result.error.message);
|
||||
process.exitCode = 1;
|
||||
} else {
|
||||
process.exitCode = result.status === null ? 1 : result.status;
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
payload_dir=$(cd -- "$(dirname -- "$0")" && pwd)
|
||||
node <<'NODE'
|
||||
const [major,minor,patch]=process.versions.node.split('.').map(Number);
|
||||
const supported=(major===22&&(minor>22||(minor===22&&patch>=2)))||(major===24&&minor>=15)||major>=26;
|
||||
if(!supported)throw new Error('npm12 requires Node ^22.22.2 || ^24.15.0 || >=26.0.0');
|
||||
NODE
|
||||
target_dir=${1:?Pass a new /tmp/qinglong-* or /private/tmp/qinglong-* directory}
|
||||
case "$target_dir" in
|
||||
/tmp/qinglong-*|/private/tmp/qinglong-*) ;;
|
||||
*) echo 'Refusing a target outside the preview prefix' >&2; exit 1 ;;
|
||||
esac
|
||||
if [[ -e "$target_dir" ]]; then echo 'Target already exists; use a fresh directory' >&2; exit 1; fi
|
||||
mkdir -p "$target_dir"
|
||||
cp -- "$payload_dir/package.json" "$target_dir/package.json"
|
||||
cp -- "$payload_dir/package-lock.json" "$target_dir/package-lock.json"
|
||||
cp -- "$payload_dir/pnpm-10.34.6-qinglong-security-linux.tgz" "$target_dir/pnpm-10.34.6-qinglong-security-linux.tgz"
|
||||
offline_args=()
|
||||
if [[ "${QL_TOOLS_OFFLINE:-0}" == 1 ]]; then offline_args+=(--offline); fi
|
||||
build_cache=${QL_TOOLS_NPM_CACHE:-"$target_dir/.build-cache"}
|
||||
(
|
||||
cd -- "$target_dir"
|
||||
NPM_CONFIG_USERCONFIG="$payload_dir/public.npmrc" \
|
||||
NPM_CONFIG_CACHE="$build_cache" \
|
||||
node "${QL_TOOLS_BOOTSTRAP_NPM:-$(command -v npm)}" ci --os=linux --ignore-scripts --omit=dev --no-audit --no-fund "${offline_args[@]}"
|
||||
)
|
||||
if [[ -z "${QL_TOOLS_NPM_CACHE:-}" ]]; then rm -rf -- "$target_dir/.build-cache"; fi
|
||||
node - "$target_dir" <<'NODE'
|
||||
const fs=require('node:fs');
|
||||
const path=require('node:path');
|
||||
const {createRequire}=require('node:module');
|
||||
const target=process.argv[2];
|
||||
const req=createRequire(path.join(target,'package.json'));
|
||||
const expected={'pnpm':'10.34.6','pm2':'7.0.4','node-gyp':'12.4.0','ts-node':'10.9.2','typescript':'5.9.3'};
|
||||
const proof={};
|
||||
for(const [name,version] of Object.entries(expected)){
|
||||
const file=path.join(target,'node_modules',name,'package.json');
|
||||
const actual=JSON.parse(fs.readFileSync(file,'utf8')).version;
|
||||
if(actual!==version)throw new Error(`${name}: expected ${version}, got ${actual}`);
|
||||
proof[name]={version:actual,path:file};
|
||||
}
|
||||
const pm2Req=createRequire(path.join(target,'node_modules/pm2/package.json'));
|
||||
for(const [name,version] of Object.entries({'js-yaml':'4.3.2','basic-ftp':'6.2.1'})){
|
||||
const file=pm2Req.resolve(name+'/package.json');
|
||||
const actual=JSON.parse(fs.readFileSync(file,'utf8')).version;
|
||||
if(actual!==version)throw new Error(`PM2 ${name}: expected ${version}, got ${actual}`);
|
||||
proof[`pm2/${name}`]={version:actual,path:file};
|
||||
}
|
||||
const pnpmDistReq=createRequire(path.join(target,'node_modules/pnpm/dist/package.json'));
|
||||
for(const [name,version] of Object.entries({'node-gyp':'12.4.0','tar':'7.5.22'})){
|
||||
const file=pnpmDistReq.resolve(name+'/package.json');
|
||||
const actual=JSON.parse(fs.readFileSync(file,'utf8')).version;
|
||||
if(actual!==version)throw new Error(`PNPM default ${name}: expected ${version}, got ${actual}`);
|
||||
proof[`pnpm-default/${name}`]={version:actual,path:file};
|
||||
}
|
||||
fs.writeFileSync(path.join(target,'qinglong-runtime-tools-proof.json'),JSON.stringify(proof,null,2)+'\n');
|
||||
console.log(JSON.stringify(proof));
|
||||
NODE
|
||||
+1405
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,24 @@
|
||||
{
|
||||
"name": "qinglong-runtime-tools",
|
||||
"version": "0.0.0",
|
||||
"private": true,
|
||||
"engines": {
|
||||
"node": "^22.22.2 || ^24.15.0 || >=26.0.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"pm2": "7.0.4",
|
||||
"pnpm": "file:pnpm-10.34.6-qinglong-security-linux.tgz",
|
||||
"node-gyp": "12.4.0",
|
||||
"ts-node": "10.9.2",
|
||||
"typescript": "5.9.3"
|
||||
},
|
||||
"overrides": {
|
||||
"pm2": {
|
||||
"js-yaml": "4.3.2",
|
||||
"basic-ftp": "6.2.1"
|
||||
},
|
||||
"http-cache-semantics": "4.3.0",
|
||||
"brace-expansion@1": "1.1.21",
|
||||
"brace-expansion@2": "2.1.7"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
registry=https://registry.npmjs.org/
|
||||
Executable
+68
@@ -0,0 +1,68 @@
|
||||
#!/usr/bin/env node
|
||||
'use strict';
|
||||
const fs = require('node:fs');
|
||||
const path = require('node:path');
|
||||
const crypto = require('node:crypto');
|
||||
const assert = require('node:assert/strict');
|
||||
|
||||
// CI additionally anchors the archive/proof in the separately retained producer
|
||||
// summary. This installed receipt detects damage; it is not a signature.
|
||||
const hash = (bytes) => crypto.createHash('sha256').update(bytes).digest('hex');
|
||||
const inside = (root, name) => name === root || name.startsWith(`${root}${path.sep}`);
|
||||
function verifyBundledDependencies(root) {
|
||||
root = fs.realpathSync(root);
|
||||
const receipt = JSON.parse(fs.readFileSync(path.join(root, 'qinglong-npm-dependency-proof.json')));
|
||||
assert.equal(receipt.version, 1);
|
||||
const checkedPath = (relative) => {
|
||||
assert(relative && !path.isAbsolute(relative) && !relative.split(/[\\/]/).includes('..'), `Unsafe receipt path ${relative}`);
|
||||
const file = path.join(root, relative);
|
||||
assert(inside(root, fs.realpathSync(file)), `Payload escapes package root: ${relative}`);
|
||||
return file;
|
||||
};
|
||||
const matchesHash = (relative, expected) => assert.equal(hash(fs.readFileSync(checkedPath(relative))), expected, `Package payload changed: ${relative}`);
|
||||
matchesHash('package.json', receipt.publishedRootManifestSHA256);
|
||||
for (const item of receipt.applicationFiles) matchesHash(item.path, item.sha256);
|
||||
const shipped = new Map();
|
||||
for (const item of receipt.shipped) {
|
||||
const directory = checkedPath(item.path);
|
||||
assert.equal(fs.realpathSync(directory), directory, `Bundled package is a link: ${item.path}`);
|
||||
shipped.set(directory, item);
|
||||
matchesHash(`${item.path}/package.json`, item.publishedManifestSHA256);
|
||||
for (const file of item.shippedFiles) if (file.path !== 'package.json') matchesHash(`${item.path}/${file.path}`, file.sha256);
|
||||
const pkg = JSON.parse(fs.readFileSync(path.join(directory, 'package.json')));
|
||||
assert.equal(pkg.name, item.package); assert.equal(pkg.version, item.version);
|
||||
}
|
||||
const resolve = (directory, name) => {
|
||||
assert(name && !name.split('/').includes('..') && !path.isAbsolute(name));
|
||||
for (let current = directory; inside(root, current); current = path.dirname(current)) {
|
||||
const candidate = path.join(current, 'node_modules', name);
|
||||
if (fs.existsSync(path.join(candidate, 'package.json'))) return fs.realpathSync(candidate);
|
||||
if (current === root) break;
|
||||
}
|
||||
throw new Error(`Missing bundled dependency ${name} from ${path.relative(root, directory)}`);
|
||||
};
|
||||
let edges = 0;
|
||||
for (const [directory, item] of shipped) {
|
||||
const pkg = JSON.parse(fs.readFileSync(path.join(directory, 'package.json')));
|
||||
for (const edge of item.edges) {
|
||||
assert.equal(pkg[edge.category]?.[edge.name], edge.publishedSpec);
|
||||
assert.equal(resolve(directory, edge.name), checkedPath(edge.resolvedPath), `Bundled resolution changed ${item.path} -> ${edge.name}`);
|
||||
assert.equal(shipped.get(resolve(directory, edge.name))?.context, edge.context);
|
||||
edges++;
|
||||
}
|
||||
}
|
||||
const pkg = JSON.parse(fs.readFileSync(path.join(root, 'package.json')));
|
||||
assert.deepEqual(Object.keys(pkg.dependencies).sort(), receipt.directDependencies.map((item) => item.name).sort());
|
||||
for (const edge of receipt.directDependencies) {
|
||||
assert.equal(pkg.dependencies[edge.name], edge.publishedSpec);
|
||||
assert.equal(resolve(root, edge.name), checkedPath(edge.resolvedPath));
|
||||
assert.equal(shipped.get(resolve(root, edge.name))?.context, edge.context);
|
||||
}
|
||||
return { receipt, verifiedPlacements: shipped.size, verifiedEdges: edges };
|
||||
}
|
||||
module.exports = { verifyBundledDependencies };
|
||||
if (require.main === module) {
|
||||
const root = process.argv[2] || path.join(__dirname, '..');
|
||||
const result = verifyBundledDependencies(root);
|
||||
console.log(JSON.stringify({ verifiedPlacements: result.verifiedPlacements, verifiedEdges: result.verifiedEdges, productionLockSHA256: result.receipt.productionLockSHA256 }));
|
||||
}
|
||||
Executable
+105
@@ -0,0 +1,105 @@
|
||||
#!/usr/bin/env node
|
||||
'use strict';
|
||||
const fs = require('node:fs');
|
||||
const path = require('node:path');
|
||||
const os = require('node:os');
|
||||
const crypto = require('node:crypto');
|
||||
const assert = require('node:assert/strict');
|
||||
const { createRequire } = require('node:module');
|
||||
const { spawnSync } = require('node:child_process');
|
||||
const options = {};
|
||||
for (let i = 2; i < process.argv.length; i++) {
|
||||
if (['--global', '--source-build', '--require-prebuilt'].includes(process.argv[i])) options[process.argv[i]] = true;
|
||||
else if (process.argv[i].startsWith('--') && process.argv[i + 1]) options[process.argv[i]] = process.argv[++i];
|
||||
else throw new Error('Unexpected verification argument');
|
||||
}
|
||||
for (const name of ['--tarball', '--build-summary', '--npm-cli', '--output']) assert(options[name], `Missing ${name}`);
|
||||
assert(!(options['--source-build'] && options['--require-prebuilt']), 'Source and prebuilt verification modes are mutually exclusive');
|
||||
const hash = (value) => crypto.createHash('sha256').update(value).digest('hex');
|
||||
const tarball = fs.realpathSync(options['--tarball']);
|
||||
const summary = JSON.parse(fs.readFileSync(options['--build-summary']));
|
||||
assert.equal(hash(fs.readFileSync(tarball)), summary.tarball.sha256, 'Tarball differs from trusted build summary');
|
||||
const output = path.resolve(options['--output']);
|
||||
const parent = fs.realpathSync(path.dirname(output));
|
||||
const permitted = [os.tmpdir(), '/tmp', process.env.RUNNER_TEMP].filter(Boolean).map((folder) => fs.realpathSync(folder));
|
||||
assert(permitted.some((root) => parent === root || parent.startsWith(`${root}${path.sep}`)), 'Verification output must be temporary');
|
||||
assert(!fs.existsSync(output), 'Verification output must be fresh');fs.mkdirSync(output);
|
||||
fs.writeFileSync(path.join(output, '.qinglong-npm-verify-owner.json'), JSON.stringify({ version: 1, id: crypto.randomUUID(), tarballSHA256: summary.tarball.sha256 }));
|
||||
const env = { ...process.env, NPM_CONFIG_USERCONFIG: path.join(output, 'public.npmrc'), NPM_CONFIG_GLOBALCONFIG: path.join(output, 'global.npmrc'), NPM_CONFIG_CACHE: path.join(output, 'npm-cache') };
|
||||
fs.writeFileSync(env.NPM_CONFIG_USERCONFIG, 'registry=https://registry.npmjs.org/\n');fs.writeFileSync(env.NPM_CONFIG_GLOBALCONFIG, '');
|
||||
if (options['--source-build']) env.npm_config_build_from_source = 'true';
|
||||
if (options['--require-prebuilt']) {
|
||||
env.npm_config_build_from_source = 'false';
|
||||
env.NPM_CONFIG_BUILD_FROM_SOURCE = 'false';
|
||||
}
|
||||
const npmCLI = fs.realpathSync(options['--npm-cli']);
|
||||
const run = (args, name, command = process.execPath, extraEnv = {}) => {
|
||||
const result = spawnSync(command, args, { cwd: output, env: { ...env, ...extraEnv }, encoding: 'utf8', maxBuffer: 64 * 1024 * 1024 });
|
||||
fs.writeFileSync(path.join(output, `${name}.stdout.log`), result.stdout || '');fs.writeFileSync(path.join(output, `${name}.stderr.log`), result.stderr || '');
|
||||
assert(!result.error && result.status === 0, `${name} failed; inspect ${output}`);return result.stdout;
|
||||
};
|
||||
const members = run(['-tzf', tarball], 'tar-inventory', 'tar').split('\n').filter(Boolean);
|
||||
assert(members.length > 0);
|
||||
for (const name of members) assert(name.startsWith('package/') && !name.split('/').includes('..') && !name.endsWith('.node') && !name.split('/').some((part) => ['.npmrc', '.git', '.pnpm', '_cacache'].includes(part)), `Forbidden tarball item ${name}`);
|
||||
const npmVersion = String(run([npmCLI, '--version'], 'npm-version')).trim();
|
||||
const global = !!options['--global'];
|
||||
const prefix = path.join(output, 'global-prefix');
|
||||
if (!global) {
|
||||
const manifest = { name: 'qinglong-final-tarball-consumer', version: '1.0.0', private: true, dependencies: { '@whyour/qinglong': `file:${tarball}` } };
|
||||
if (Number(npmVersion.split('.')[0]) >= 12) manifest.allowScripts = { [`file:${tarball}`]: true };
|
||||
fs.writeFileSync(path.join(output, 'package.json'), `${JSON.stringify(manifest, null, 2)}\n`);
|
||||
}
|
||||
const install = [npmCLI, 'install', '--foreground-scripts', '--no-audit', '--no-fund'];
|
||||
if (global) {install.push('--global', '--prefix', prefix);if (Number(npmVersion.split('.')[0]) >= 12) install.push(`--allow-scripts=file:${tarball}`);install.push(tarball);}
|
||||
const installLog = run(install, 'consumer-install');
|
||||
const installErrorLog = fs.readFileSync(path.join(output, 'consumer-install.stderr.log'), 'utf8');
|
||||
if (options['--source-build']) {
|
||||
assert(installLog.includes('SOLINK_MODULE') || installErrorLog.includes('SOLINK_MODULE'), 'Native source compilation evidence missing');
|
||||
assert(installLog.includes('"installation":"source-built"'), 'Parent lifecycle did not report an actual source build');
|
||||
}
|
||||
if (options['--require-prebuilt']) {
|
||||
assert(/\[@whyour\/sqlite3\] Success: .* is installed via remote/.test(installLog), 'Successful SQLite prebuilt download evidence missing');
|
||||
assert(installLog.includes('"installation":"installer-ran"'), 'Parent lifecycle did not install the missing native binding');
|
||||
assert(!/SOLINK_MODULE|"installation":"source-built"|gyp info spawn (?:make|ninja)/.test(`${installLog}\n${installErrorLog}`), 'SQLite source fallback cannot count as prebuilt verification');
|
||||
}
|
||||
const packageRoot = global ? path.join(prefix, 'lib', 'node_modules', '@whyour/qinglong') : path.join(output, 'node_modules', '@whyour/qinglong');
|
||||
const receipt = JSON.parse(fs.readFileSync(path.join(packageRoot, 'qinglong-npm-dependency-proof.json')));
|
||||
assert.equal(receipt.sourceCommit, summary.sourceCommit);assert.equal(receipt.productionLockSHA256, summary.productionLockSHA256);
|
||||
assert.equal(receipt.ciBuildInfoSHA256, summary.ciBuildInfoSHA256);
|
||||
const bundled = require(path.join(__dirname, 'verify-bundled-dependencies.cjs')).verifyBundledDependencies(packageRoot);
|
||||
assert.equal(bundled.verifiedPlacements, summary.shippedPackagePlacements);
|
||||
for (const item of receipt.applicationFiles) assert.equal(hash(fs.readFileSync(path.join(packageRoot, item.path))), item.sha256, `Application payload changed ${item.path}`);
|
||||
for (const item of receipt.shipped) {
|
||||
for (const file of item.shippedFiles || item.sourceFiles) if (file.path !== 'package.json') assert.equal(hash(fs.readFileSync(path.join(packageRoot, item.path, file.path))), file.sha256, `Bundled source changed ${item.path}/${file.path}`);
|
||||
assert.equal(hash(fs.readFileSync(path.join(packageRoot, item.path, 'package.json'))), item.publishedManifestSHA256);
|
||||
}
|
||||
const listArgs = [npmCLI, 'ls', '--all', '--json'];if (global) listArgs.push('--global', '--prefix', prefix);
|
||||
const dependencyList = JSON.parse(run(listArgs, 'consumer-dependency-list'));assert(!dependencyList.problems?.length);
|
||||
let audit = null;
|
||||
if (!global) {
|
||||
audit = JSON.parse(run([npmCLI, 'audit', '--omit=dev', '--json'], 'consumer-production-audit'));
|
||||
assert.equal(audit.metadata.vulnerabilities.total, 0);
|
||||
}
|
||||
const protocols = run([path.join(packageRoot, 'scripts', 'verify-production-dependencies.cjs')], 'consumer-production-protocols');
|
||||
for (const name of ['verifyDatabase', 'verifyGrpc', 'verifyWebsocket', 'verifyMailAndJwt']) assert(protocols.includes(`${name}: passed`));
|
||||
const requirePackage = createRequire(path.join(packageRoot, 'package.json'));
|
||||
run([path.join(packageRoot, 'cli', 'dist', 'ql.js'), '--help'], 'internal-cli-help');
|
||||
assert(fs.existsSync(path.join(packageRoot, 'cli', 'dist', 'internal', 'maintenance', 'upgradeArtifacts.cjs')));
|
||||
requirePackage('./cli/src/internal/maintenance/upgradeArtifacts.cjs');
|
||||
// Repeated ql check / upgrade must verify the archive without re-resolving it.
|
||||
const recheck = run([path.join(packageRoot, 'scripts', 'install-panel-dependencies.cjs'), '--root', packageRoot], 'panel-bundled-recheck', process.execPath, { npm_config_build_from_source: 'false', NPM_CONFIG_BUILD_FROM_SOURCE: 'false' });
|
||||
assert(recheck.includes('"resolverInstallSkipped":true') && recheck.includes('"installation":"existing-binding"'));
|
||||
const { Keyv } = requirePackage('keyv'); const { default: KeyvSqlite } = requirePackage('@keyv/sqlite');
|
||||
assert.equal(createRequire(requirePackage.resolve('@keyv/sqlite')).resolve('sqlite3/package.json'), requirePackage.resolve('sqlite3/package.json'));
|
||||
(async () => {
|
||||
const store = new KeyvSqlite('sqlite://:memory:');const keyv = new Keyv({ store, namespace: 'npm-consumer' });
|
||||
keyv.on('error', (error) => { throw error; });
|
||||
await keyv.set('record', { value: 7 });assert.deepEqual(await keyv.get('record'), { value: 7 });
|
||||
const rows = await store.query('SELECT key, value FROM keyv WHERE key = ?', 'npm-consumer:record');assert.equal(rows.length, 1);
|
||||
await keyv.set('expires', 'short-lived', 30);await new Promise((resolve) => setTimeout(resolve, 60));assert.equal(await keyv.get('expires'), undefined);
|
||||
assert.equal(await keyv.delete('record'), true);await keyv.set('clear', true);await keyv.clear();assert.equal(await keyv.get('clear'), undefined);await keyv.disconnect();
|
||||
const sqliteDirectory = path.dirname(requirePackage.resolve('sqlite3/package.json'));
|
||||
const binding = Object.keys(require.cache).find((file) => file.endsWith('.node') && file.startsWith(`${sqliteDirectory}${path.sep}`));assert(binding);
|
||||
const report = { version: 1, sourceCommit: receipt.sourceCommit, productionLockSHA256: receipt.productionLockSHA256, tarballSHA256: summary.tarball.sha256, node: process.version, platform: process.platform, arch: process.arch, npm: npmVersion, global, nativeSourceBuildRequested: !!options['--source-build'], nativeSourceBuildVerified: !!options['--source-build'], nativePrebuiltRequested: !!options['--require-prebuilt'], nativePrebuiltVerified: !!options['--require-prebuilt'], nativeBinding: { path: path.relative(packageRoot, binding), sha256: hash(fs.readFileSync(binding)), header: fs.readFileSync(binding).subarray(0, 16).toString('hex') }, shippedPackagePlacements: receipt.shippedPackagePlacements, bundledSourceHashesVerified: true, applicationHashesVerified: true, fullCliHelpPassed: true, rawUpgradeHelperLoaded: true, dependencyProblems: [], productionAudit: audit?.metadata || null, productionProtocolsPassed: 4, keyvActualSqlitePassed: true };
|
||||
fs.writeFileSync(path.join(output, 'npm-consumer-validation.json'), `${JSON.stringify(report, null, 2)}\n`);console.log(JSON.stringify(report));
|
||||
})().catch((error) => { console.error(error);process.exitCode = 1; });
|
||||
@@ -0,0 +1,191 @@
|
||||
#!/usr/bin/env node
|
||||
'use strict';
|
||||
|
||||
const assert = require('node:assert/strict');
|
||||
const { createServer } = require('node:http');
|
||||
const sqlite3 = require('sqlite3');
|
||||
const { Sequelize, DataTypes } = require('sequelize');
|
||||
const jwt = require('jsonwebtoken');
|
||||
const protobuf = require('node:module').createRequire(
|
||||
require.resolve('@grpc/proto-loader'),
|
||||
)('protobufjs');
|
||||
const grpc = require('@grpc/grpc-js');
|
||||
const sockjs = require('sockjs');
|
||||
const { WebSocket, ProxyAgent } = require('undici');
|
||||
const nodemailer = require('nodemailer');
|
||||
|
||||
async function verifyDatabase() {
|
||||
const db = new sqlite3.Database(':memory:');
|
||||
try {
|
||||
const row = await new Promise((resolve, reject) =>
|
||||
db.get('SELECT sqlite_version() AS version', (error, row) =>
|
||||
error ? reject(error) : resolve(row),
|
||||
),
|
||||
);
|
||||
assert.match(row.version, /^\d+\.\d+\.\d+$/);
|
||||
} finally {
|
||||
await new Promise((resolve, reject) =>
|
||||
db.close((error) => (error ? reject(error) : resolve())),
|
||||
);
|
||||
}
|
||||
const orm = new Sequelize({
|
||||
dialect: 'sqlite',
|
||||
storage: ':memory:',
|
||||
logging: false,
|
||||
});
|
||||
try {
|
||||
const Task = orm.define('ReadinessTask', {
|
||||
name: DataTypes.STRING,
|
||||
payload: DataTypes.JSON,
|
||||
});
|
||||
await orm.sync();
|
||||
await orm.transaction(async (transaction) => {
|
||||
await Task.create(
|
||||
{ name: 'readiness', payload: { status: 'ready' } },
|
||||
{ transaction },
|
||||
);
|
||||
});
|
||||
const task = await Task.findOne({ where: { name: 'readiness' } });
|
||||
assert.deepEqual(task.get('payload'), { status: 'ready' });
|
||||
await task.update({ name: 'verified' });
|
||||
assert.equal(await Task.count({ where: { name: 'verified' } }), 1);
|
||||
} finally {
|
||||
await orm.close();
|
||||
}
|
||||
}
|
||||
|
||||
async function verifyGrpc() {
|
||||
const Message = protobuf.Root.fromJSON({
|
||||
nested: {
|
||||
Message: { fields: { value: { type: 'string', id: 1 } } },
|
||||
},
|
||||
}).lookupType('Message');
|
||||
const serialize = (value) => Buffer.from(Message.encode(value).finish());
|
||||
const deserialize = (value) => Message.toObject(Message.decode(value));
|
||||
const definition = {
|
||||
echo: {
|
||||
path: '/readiness.Echo/Echo',
|
||||
requestStream: false,
|
||||
responseStream: false,
|
||||
requestSerialize: serialize,
|
||||
requestDeserialize: deserialize,
|
||||
responseSerialize: serialize,
|
||||
responseDeserialize: deserialize,
|
||||
},
|
||||
};
|
||||
const server = new grpc.Server();
|
||||
server.addService(definition, {
|
||||
echo: (call, callback) => callback(null, call.request),
|
||||
});
|
||||
const port = await new Promise((resolve, reject) =>
|
||||
server.bindAsync(
|
||||
'127.0.0.1:0',
|
||||
grpc.ServerCredentials.createInsecure(),
|
||||
(error, port) => (error ? reject(error) : resolve(port)),
|
||||
),
|
||||
);
|
||||
const Client = grpc.makeGenericClientConstructor(definition, 'Echo');
|
||||
const client = new Client(
|
||||
`127.0.0.1:${port}`,
|
||||
grpc.credentials.createInsecure(),
|
||||
);
|
||||
try {
|
||||
const response = await new Promise((resolve, reject) =>
|
||||
client.echo(
|
||||
{ value: 'ready' },
|
||||
{ deadline: Date.now() + 5000 },
|
||||
(error, response) => (error ? reject(error) : resolve(response)),
|
||||
),
|
||||
);
|
||||
assert.equal(response.value, 'ready');
|
||||
} finally {
|
||||
client.close();
|
||||
server.forceShutdown();
|
||||
}
|
||||
}
|
||||
|
||||
async function verifyWebsocket() {
|
||||
const server = createServer();
|
||||
const transport = sockjs.createServer({
|
||||
prefix: '/readiness',
|
||||
log: () => {},
|
||||
});
|
||||
transport.on('connection', (connection) =>
|
||||
connection.on('data', (message) => connection.write(message)),
|
||||
);
|
||||
transport.installHandlers(server);
|
||||
await new Promise((resolve) => server.listen(0, '127.0.0.1', resolve));
|
||||
const client = new WebSocket(
|
||||
`ws://127.0.0.1:${server.address().port}/readiness/000/fixture/websocket`,
|
||||
);
|
||||
try {
|
||||
await new Promise((resolve, reject) => {
|
||||
const timer = setTimeout(
|
||||
() => reject(new Error('WebSocket echo timed out')),
|
||||
5000,
|
||||
);
|
||||
client.addEventListener('error', () => {
|
||||
clearTimeout(timer);
|
||||
reject(new Error('WebSocket connection failed'));
|
||||
});
|
||||
client.addEventListener('message', ({ data }) => {
|
||||
if (data === 'o') client.send(JSON.stringify(['dependency-readiness']));
|
||||
else {
|
||||
try {
|
||||
assert.equal(data, 'a["dependency-readiness"]');
|
||||
clearTimeout(timer);
|
||||
resolve();
|
||||
} catch (error) {
|
||||
clearTimeout(timer);
|
||||
reject(error);
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
} finally {
|
||||
client.close();
|
||||
server.closeAllConnections();
|
||||
await new Promise((resolve) => server.close(resolve));
|
||||
}
|
||||
const proxy = new ProxyAgent('http://127.0.0.1:1');
|
||||
await proxy.close();
|
||||
}
|
||||
|
||||
async function verifyMailAndJwt() {
|
||||
const token = jwt.sign({ readiness: true }, 'local-fixture-secret', {
|
||||
expiresIn: 60,
|
||||
});
|
||||
assert.equal(jwt.verify(token, 'local-fixture-secret').readiness, true);
|
||||
assert.throws(() => jwt.verify(token, 'different-local-fixture-secret'));
|
||||
const mail = nodemailer.createTransport({
|
||||
streamTransport: true,
|
||||
buffer: true,
|
||||
});
|
||||
try {
|
||||
const message = await mail.sendMail({
|
||||
from: 'Qinglong <readiness@localhost>',
|
||||
to: 'fixture@localhost',
|
||||
subject: 'Dependency readiness',
|
||||
text: 'Local fixture only',
|
||||
});
|
||||
assert.match(message.message.toString(), /Local fixture only/);
|
||||
assert.deepEqual(message.envelope.to, ['fixture@localhost']);
|
||||
} finally {
|
||||
mail.close();
|
||||
}
|
||||
}
|
||||
|
||||
(async () => {
|
||||
for (const verify of [
|
||||
verifyDatabase,
|
||||
verifyGrpc,
|
||||
verifyWebsocket,
|
||||
verifyMailAndJwt,
|
||||
]) {
|
||||
await verify();
|
||||
console.log(`${verify.name}: passed`);
|
||||
}
|
||||
})().catch((error) => {
|
||||
console.error(error);
|
||||
process.exitCode = 1;
|
||||
});
|
||||
@@ -0,0 +1,29 @@
|
||||
'use strict';
|
||||
const fs = require('node:fs');
|
||||
const path = require('node:path');
|
||||
const crypto = require('node:crypto');
|
||||
const { execFileSync } = require('node:child_process');
|
||||
function verifyRuntimeToolsSource(payload, sourceCommit) {
|
||||
const scripts = path.resolve(payload, '..');
|
||||
const paths = [];
|
||||
const collect = directory => { for (const name of fs.readdirSync(directory).sort()) { const file = path.join(directory, name), stat = fs.lstatSync(file); if (stat.isDirectory()) collect(file); else if (stat.isFile()) paths.push(file); else throw new Error('Build inputs must be regular tracked files: ' + file); } };
|
||||
collect(payload);
|
||||
for (const name of ['build-runtime-tools.sh', 'pack-runtime-tools.cjs', 'install-runtime-tools.cjs', 'verify-runtime-tools-source.cjs']) paths.push(path.join(scripts, name));
|
||||
const digest = bytes => crypto.createHash('sha256').update(bytes).digest('hex');
|
||||
if (process.env.QL_TOOLS_ALLOW_HOST_SMOKE === '1') return { verified: false, previewOnly: true, inputs: paths.map(file => ({ path: 'scripts/' + path.relative(scripts, file), sha256: digest(fs.readFileSync(file)) })) };
|
||||
const git = arguments_ => execFileSync('git', ['-C', scripts, ...arguments_], { maxBuffer: 8 * 1024 * 1024 });
|
||||
const root = git(['rev-parse', '--show-toplevel']).toString().trim();
|
||||
const head = git(['rev-parse', 'HEAD']).toString().trim();
|
||||
if (head !== sourceCommit) throw new Error('QL_TOOLS_SOURCE_COMMIT does not match actual Git HEAD');
|
||||
git(['diff', '--exit-code', '--quiet', 'HEAD', '--']);
|
||||
const inputs = paths.map(file => {
|
||||
const relative = path.relative(root, file).split(path.sep).join('/');
|
||||
if (relative.startsWith('../')) throw new Error('Build input outside Git tree');
|
||||
const actual = fs.readFileSync(file), blob = git(['cat-file', 'blob', sourceCommit + ':' + relative]);
|
||||
if (!actual.equals(blob)) throw new Error('Build input does not match source Git blob: ' + relative);
|
||||
return { path: relative, sha256: digest(actual), gitBlob: git(['rev-parse', sourceCommit + ':' + relative]).toString().trim() };
|
||||
});
|
||||
return { verified: true, head, inputs };
|
||||
}
|
||||
module.exports = verifyRuntimeToolsSource;
|
||||
if (require.main === module) console.log(JSON.stringify(verifyRuntimeToolsSource(process.argv[2], process.argv[3])));
|
||||
@@ -0,0 +1,20 @@
|
||||
const { spawnSync } = require('node:child_process');
|
||||
const env = { PATH: '/usr/local/bin:/usr/bin:/bin', HOME: '/root', LANG: 'C' };
|
||||
const cases = [
|
||||
['ssh', '/usr/bin/ssh', ['-V'], /OpenSSH_/],
|
||||
['curl', '/usr/bin/curl', ['--version'], /^curl\s+\d/m],
|
||||
['openssl', '/usr/bin/openssl', ['version'], /^OpenSSL\s+\d/m],
|
||||
['pythonSsl', 'python3', ['-c', 'import ssl;print("QA_SSL_IMPORT_OK")'], /^QA_SSL_IMPORT_OK\s*$/m],
|
||||
];
|
||||
const checks = {};
|
||||
for (const [name, command, args, marker] of cases) {
|
||||
const r = spawnSync(command, args, { env, encoding: 'utf8', timeout: 15000 });
|
||||
const markerPresent = marker.test(String(r.stdout || '') + String(r.stderr || ''));
|
||||
const signal = ['SIGSEGV', 'SIGTERM', 'SIGKILL', 'SIGILL', 'SIGABRT'].includes(r.signal)
|
||||
? r.signal : r.signal ? 'OTHER' : null;
|
||||
checks[name] = { exitCode: r.status, signal, executionError: !!r.error, markerPresent,
|
||||
pass: r.status === 0 && !r.signal && !r.error && markerPresent };
|
||||
}
|
||||
const pass = Object.values(checks).every(r => r.pass);
|
||||
console.log(JSON.stringify({ gate: 'runtime-system-tools', pass, checks }));
|
||||
process.exitCode = pass ? 0 : 1;
|
||||
+10
-3
@@ -6,7 +6,7 @@ reset_env() {
|
||||
t '---> 配置文件检测完成\n'
|
||||
|
||||
t '---> 2. 开始安装青龙依赖\n'
|
||||
npm_install_2 $dir_root
|
||||
npm_install_2 "$dir_root" || return $?
|
||||
t '---> 青龙依赖安装完成\n'
|
||||
|
||||
t '---> 脚本依赖安装完成\n'
|
||||
@@ -97,9 +97,16 @@ check_pm2() {
|
||||
|
||||
main() {
|
||||
t '=====> 开始检测'
|
||||
npm i -g pnpm@8.3.1 pm2 ts-node typescript@5
|
||||
local ql_tools_prefix
|
||||
ql_tools_prefix=$(npm prefix --global) || return $?
|
||||
node "$dir_root/scripts/install-runtime-tools.cjs" \
|
||||
--archive "$dir_root/static/runtime-tools.tgz" \
|
||||
--proof "$dir_root/static/runtime-tools-proof.json" \
|
||||
--prefix "$ql_tools_prefix" || return $?
|
||||
export PATH="$ql_tools_prefix/bin:$PATH"
|
||||
export NODE_PATH="$ql_tools_prefix/lib/node_modules${NODE_PATH:+:$NODE_PATH}"
|
||||
|
||||
reset_env
|
||||
reset_env || return $?
|
||||
copy_dep
|
||||
reload_pm2
|
||||
local checkStatus=0
|
||||
|
||||
+14
-9
@@ -232,24 +232,29 @@ fix_config() {
|
||||
}
|
||||
|
||||
npm_install_sub() {
|
||||
if [ $is_termux -eq 1 ]; then
|
||||
npm install --production --no-bin-links
|
||||
elif ! type pnpm &>/dev/null; then
|
||||
npm install --production
|
||||
if node "$dir_root/scripts/install-panel-dependencies.cjs" --root "$PWD" "$@"; then
|
||||
exit_status=0
|
||||
else
|
||||
pnpm install --loglevel error --production
|
||||
exit_status=$?
|
||||
fi
|
||||
exit_status=$?
|
||||
return "$exit_status"
|
||||
}
|
||||
|
||||
npm_install_2() {
|
||||
local dir_current=$(pwd)
|
||||
local dir_work=$1
|
||||
local static_root="${2:-}"
|
||||
local source_commit="${3:-}"
|
||||
local installer_args=()
|
||||
[[ -n "$static_root" ]] && installer_args+=(--static-dir "$static_root")
|
||||
[[ -n "$source_commit" ]] && installer_args+=(--source-commit "$source_commit")
|
||||
|
||||
cd $dir_work
|
||||
cd "$dir_work" || return $?
|
||||
t '安装 %s 依赖包...\n' "$dir_work"
|
||||
npm_install_sub
|
||||
cd $dir_current
|
||||
local install_status=0
|
||||
npm_install_sub "${installer_args[@]}" || install_status=$?
|
||||
cd "$dir_current" || return $?
|
||||
return "$install_status"
|
||||
}
|
||||
|
||||
diff_and_copy() {
|
||||
|
||||
+7
-3
@@ -33,6 +33,7 @@ if [[ $QL_DATA_DIR != */data ]]; then
|
||||
fi
|
||||
|
||||
command="$1"
|
||||
ql_tools_prefix=$(npm prefix --global)
|
||||
|
||||
if [[ $command != "reload" ]]; then
|
||||
# 安装依赖
|
||||
@@ -74,7 +75,10 @@ if [[ $command != "reload" ]]; then
|
||||
;;
|
||||
esac
|
||||
|
||||
npm install -g pnpm@8.3.1 pm2 ts-node typescript@5
|
||||
node "$QL_DIR/scripts/install-runtime-tools.cjs" \
|
||||
--archive "$QL_DIR/static/runtime-tools.tgz" \
|
||||
--proof "$QL_DIR/static/runtime-tools-proof.json" \
|
||||
--prefix "$ql_tools_prefix"
|
||||
fi
|
||||
|
||||
export PYTHON_SHORT_VERSION=$(python3 -c 'import sys; print(f"{sys.version_info.major}.{sys.version_info.minor}")')
|
||||
@@ -82,8 +86,8 @@ export PNPM_HOME=${QL_DIR}/data/dep_cache/node
|
||||
export PYTHON_HOME=${QL_DIR}/data/dep_cache/python3
|
||||
export PYTHONUSERBASE=${QL_DIR}/data/dep_cache/python3
|
||||
|
||||
export PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:${PNPM_HOME}:${PYTHON_HOME}/bin
|
||||
export NODE_PATH=/usr/local/bin:/usr/local/lib/node_modules:${PNPM_HOME}/global/5/node_modules
|
||||
export PATH=${ql_tools_prefix:+${ql_tools_prefix}/bin:}/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:${PNPM_HOME}:${PYTHON_HOME}/bin
|
||||
export NODE_PATH=${ql_tools_prefix:+${ql_tools_prefix}/lib/node_modules:}/usr/local/bin:/usr/local/lib/node_modules:${PNPM_HOME}/global/5/node_modules
|
||||
export PIP_CACHE_DIR=${PYTHON_HOME}/pip
|
||||
export PYTHONPATH=${PYTHON_HOME}:${PYTHON_HOME}/lib/python${PYTHON_SHORT_VERSION}:${PYTHON_HOME}/lib/python${PYTHON_SHORT_VERSION}/site-packages
|
||||
|
||||
|
||||
@@ -45,8 +45,27 @@ test('release images receive the same-run artifact and verify it before use', ()
|
||||
const workflow = yaml.load(
|
||||
fs.readFileSync('.github/workflows/build-docker-image.yml', 'utf8'),
|
||||
);
|
||||
assert.equal(workflow.jobs['build-static'].needs, 'validate');
|
||||
const upload = workflow.jobs['build-static'].steps.find(step => step.uses?.startsWith('actions/upload-artifact@'));
|
||||
assert.deepEqual(workflow.jobs['build-static'].needs, [
|
||||
'validate',
|
||||
'runtime-tools',
|
||||
]);
|
||||
const staticSteps = workflow.jobs['build-static'].steps;
|
||||
const toolsDownload = staticSteps.findIndex((step) =>
|
||||
step.uses?.startsWith('actions/download-artifact@'),
|
||||
);
|
||||
assert.notEqual(toolsDownload, -1);
|
||||
assert.equal(
|
||||
staticSteps[toolsDownload].with.name,
|
||||
'qinglong-runtime-tools-${{ github.sha }}',
|
||||
);
|
||||
assert.equal(staticSteps[toolsDownload].with.path, 'static/');
|
||||
const buildInfo = staticSteps.findIndex(
|
||||
(step) => step.run === 'pnpm build:info',
|
||||
);
|
||||
assert.ok(buildInfo > toolsDownload);
|
||||
const upload = workflow.jobs['build-static'].steps.find((step) =>
|
||||
step.uses?.startsWith('actions/upload-artifact@'),
|
||||
);
|
||||
assert.equal(upload.with['include-hidden-files'], true);
|
||||
for (const name of [
|
||||
'build-alpine',
|
||||
@@ -78,6 +97,65 @@ test('release images receive the same-run artifact and verify it before use', ()
|
||||
}
|
||||
});
|
||||
|
||||
test('npm publication uses the archive approved by every consumer without rebuilding it', () => {
|
||||
const yaml = require('js-yaml');
|
||||
const workflow = yaml.load(
|
||||
fs.readFileSync('.github/workflows/build-docker-image.yml', 'utf8'),
|
||||
);
|
||||
const gate = yaml.load(
|
||||
fs.readFileSync('.github/workflows/npm-package.yml', 'utf8'),
|
||||
);
|
||||
assert.deepEqual(gate.jobs.verified.needs, [
|
||||
'archive',
|
||||
'consumer',
|
||||
'source-fallback',
|
||||
]);
|
||||
assert.equal(gate.jobs.consumer.needs, 'archive');
|
||||
assert.deepEqual(gate.jobs.consumer.strategy.matrix.client, [
|
||||
{ npm: '10', mode: 'local' },
|
||||
{ npm: '12', mode: 'local' },
|
||||
{ npm: '12', mode: 'global' },
|
||||
]);
|
||||
assert.deepEqual(gate.jobs.consumer.strategy.matrix.sqlite, [
|
||||
'prebuilt',
|
||||
'source',
|
||||
]);
|
||||
const publish = workflow.jobs.publish;
|
||||
assert.ok(publish.needs.includes('npm-package'));
|
||||
assert.equal(
|
||||
publish.env.VERIFIED_TARBALL_SHA256,
|
||||
'${{ needs.npm-package.outputs.tarball-sha256 }}',
|
||||
);
|
||||
const approvedDownload = publish.steps.find((step) =>
|
||||
step.uses?.startsWith('actions/download-artifact@'),
|
||||
);
|
||||
assert.equal(
|
||||
approvedDownload.with.name,
|
||||
'${{ needs.npm-package.outputs.artifact-name }}',
|
||||
);
|
||||
const checks = publish.steps.findIndex((step) =>
|
||||
step.run?.includes(
|
||||
'assert.equal(fileHash(tarball), process.env.VERIFIED_TARBALL_SHA256)',
|
||||
),
|
||||
);
|
||||
const installer = publish.steps.findIndex((step) =>
|
||||
step.run?.includes('node scripts/install-runtime-tools.cjs'),
|
||||
);
|
||||
const publication = publish.steps.findIndex((step) =>
|
||||
step.run?.includes('publish "$QINGLONG_PUBLISH_TARBALL" --ignore-scripts'),
|
||||
);
|
||||
assert.ok(checks !== -1 && installer > checks && publication > installer);
|
||||
const operations = publish.steps.map((step) => step.run || '').join('\n');
|
||||
assert.doesNotMatch(
|
||||
operations,
|
||||
/build-npm-package|build:front|build:back|build:info/,
|
||||
);
|
||||
assert.doesNotMatch(
|
||||
operations,
|
||||
/\b(?:npm|pnpm)\s+(?:install|ci|rebuild|pack)\b/,
|
||||
);
|
||||
});
|
||||
|
||||
test('complete artifact manifests reject changed, missing, extra files and untracked build inputs', (t) => {
|
||||
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'ql-build-manifest-'));
|
||||
t.after(() => fs.rmSync(dir, { recursive: true, force: true }));
|
||||
@@ -97,6 +175,8 @@ test('complete artifact manifests reject changed, missing, extra files and untra
|
||||
'dist/chunks/main.js',
|
||||
'dist/assets/main.css',
|
||||
'dist/.well-known/config',
|
||||
'runtime-tools.tgz',
|
||||
'runtime-tools-proof.json',
|
||||
];
|
||||
for (const file of artifacts) {
|
||||
fs.mkdirSync(path.dirname(path.join(dir, 'static', file)), {
|
||||
@@ -123,6 +203,8 @@ test('complete artifact manifests reject changed, missing, extra files and untra
|
||||
'dist/chunks/main.js',
|
||||
'dist/assets/main.css',
|
||||
'dist/.well-known/config',
|
||||
'runtime-tools.tgz',
|
||||
'runtime-tools-proof.json',
|
||||
]) {
|
||||
const file = path.join(dir, 'static', name);
|
||||
fs.writeFileSync(file, 'stale');
|
||||
|
||||
@@ -8,18 +8,21 @@ const yaml = require('js-yaml');
|
||||
const workflow = yaml.load(
|
||||
fs.readFileSync('.github/workflows/build-docker-image.yml', 'utf8'),
|
||||
);
|
||||
const publication = workflow.jobs.publish.steps.find(
|
||||
(step) => step.name === 'Publish npm package with OIDC',
|
||||
).run;
|
||||
const publicationStep = workflow.jobs.publish.steps.find(
|
||||
(step) => step.name === 'Publish the verified npm archive with OIDC',
|
||||
);
|
||||
assert.ok(publicationStep, 'Verified archive publication step must exist');
|
||||
const publication = publicationStep.run;
|
||||
|
||||
test('npm publication waits for every release image, including Python 3.10', () => {
|
||||
for (const name of [
|
||||
test('npm publication waits for the archive gate and every release image, including Python 3.10', () => {
|
||||
assert.deepEqual(workflow.jobs.publish.needs, [
|
||||
'build-alpine',
|
||||
'build-debian',
|
||||
'build-alpine310',
|
||||
'build-debian310',
|
||||
])
|
||||
assert.ok(workflow.jobs.publish.needs.includes(name), name);
|
||||
'npm-package',
|
||||
]);
|
||||
assert.equal(workflow.jobs.publish.permissions['id-token'], 'write');
|
||||
});
|
||||
|
||||
for (const [scenario, expectedStatus, published] of [
|
||||
@@ -28,30 +31,49 @@ for (const [scenario, expectedStatus, published] of [
|
||||
['unauthorized', 1, false],
|
||||
['network', 1, false],
|
||||
['unexpected-version', 1, false],
|
||||
['wrong-npm-version', 1, false],
|
||||
]) {
|
||||
test(`npm publication handles ${scenario} without publishing an unverified version`, (t) => {
|
||||
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'ql-npm-publication-'));
|
||||
t.after(() => fs.rmSync(root, { recursive: true, force: true }));
|
||||
const bin = path.join(root, 'bin');
|
||||
fs.mkdirSync(bin);
|
||||
fs.symlinkSync(process.execPath, path.join(bin, 'node'));
|
||||
const npmCLI = path.join(
|
||||
root,
|
||||
'npm-publisher-tools/lib/node_modules/npm/bin/npm-cli.js',
|
||||
);
|
||||
fs.mkdirSync(path.dirname(npmCLI), { recursive: true });
|
||||
const tarball = path.join(root, 'verified qinglong-2.23.0.tgz');
|
||||
fs.writeFileSync(tarball, 'Fixture only: never install or publish this file.\n');
|
||||
fs.writeFileSync(
|
||||
path.join(root, 'package.json'),
|
||||
JSON.stringify({ name: '@fixture/qinglong', version: '2.22.0' }),
|
||||
JSON.stringify({ name: '@fixture/qinglong', version: '2.23.0' }),
|
||||
);
|
||||
fs.writeFileSync(
|
||||
path.join(bin, 'npm'),
|
||||
`#!/usr/bin/env node
|
||||
npmCLI,
|
||||
`
|
||||
const fs = require('node:fs');
|
||||
if (process.argv[2] === 'publish') { fs.writeFileSync('published', 'yes'); process.exit(0); }
|
||||
if (process.argv[2] !== 'view') process.exit(99);
|
||||
const args = process.argv.slice(2);
|
||||
fs.appendFileSync('npm-calls.jsonl', JSON.stringify(args) + '\\n');
|
||||
if (args[0] === '--version') {
|
||||
console.log(process.env.QL_PUBLISH_SCENARIO === 'wrong-npm-version' ? '12.1.0' : '12.2.0');
|
||||
process.exit(0);
|
||||
}
|
||||
if (args[0] === 'publish') { fs.writeFileSync('published', JSON.stringify(args)); process.exit(0); }
|
||||
if (args[0] !== 'view') process.exit(99);
|
||||
switch (process.env.QL_PUBLISH_SCENARIO) {
|
||||
case 'existing': console.log(JSON.stringify('2.22.0')); break;
|
||||
case 'unexpected-version': console.log(JSON.stringify('2.21.0')); break;
|
||||
case 'existing': console.log(JSON.stringify('2.23.0')); break;
|
||||
case 'unexpected-version': console.log(JSON.stringify('2.22.0')); break;
|
||||
case 'missing': console.log(JSON.stringify({error:{code:'E404'}})); process.exit(1);
|
||||
case 'unauthorized': console.log(JSON.stringify({error:{code:'E401'}})); process.exit(1);
|
||||
case 'network': console.log('upstream unavailable'); process.exit(1);
|
||||
}
|
||||
`,
|
||||
);
|
||||
fs.writeFileSync(
|
||||
path.join(bin, 'npm'),
|
||||
'#!/bin/sh\nprintf wrong-client > wrong-path-npm\nexit 97\n',
|
||||
{ mode: 0o755 },
|
||||
);
|
||||
const result = spawnSync('bash', ['-c', publication], {
|
||||
@@ -62,9 +84,41 @@ case 'network': console.log('upstream unavailable'); process.exit(1);
|
||||
PATH: `${bin}${path.delimiter}${process.env.PATH}`,
|
||||
RUNNER_TEMP: root,
|
||||
QL_PUBLISH_SCENARIO: scenario,
|
||||
QINGLONG_PUBLISH_TARBALL: tarball,
|
||||
},
|
||||
});
|
||||
assert.equal(result.status, expectedStatus, result.stdout + result.stderr);
|
||||
assert.equal(fs.existsSync(path.join(root, 'published')), published);
|
||||
assert.equal(fs.existsSync(path.join(root, 'wrong-path-npm')), false);
|
||||
const calls = fs
|
||||
.readFileSync(path.join(root, 'npm-calls.jsonl'), 'utf8')
|
||||
.trim()
|
||||
.split('\n')
|
||||
.map((line) => JSON.parse(line));
|
||||
const expectedCalls = [['--version']];
|
||||
if (scenario !== 'wrong-npm-version')
|
||||
expectedCalls.push([
|
||||
'view',
|
||||
'@fixture/qinglong@2.23.0',
|
||||
'version',
|
||||
'--json',
|
||||
'--registry=https://registry.npmjs.org',
|
||||
]);
|
||||
if (published) {
|
||||
const publishArgs = [
|
||||
'publish',
|
||||
tarball,
|
||||
'--ignore-scripts',
|
||||
'--access',
|
||||
'public',
|
||||
'--registry=https://registry.npmjs.org',
|
||||
];
|
||||
expectedCalls.push(publishArgs);
|
||||
assert.deepEqual(
|
||||
JSON.parse(fs.readFileSync(path.join(root, 'published'))),
|
||||
publishArgs,
|
||||
);
|
||||
}
|
||||
assert.deepEqual(calls, expectedCalls);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -9,39 +9,73 @@ const { spawnSync } = require('node:child_process');
|
||||
const source = fs
|
||||
.readFileSync('shell/start.sh', 'utf8')
|
||||
.split('. ${QL_DIR}/shell/share.sh')[0];
|
||||
|
||||
function fixture(t, existing) {
|
||||
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'ql-env-'));
|
||||
t.after(() => fs.rmSync(root, { recursive: true, force: true }));
|
||||
fs.mkdirSync(path.join(root, 'shell'));
|
||||
fs.mkdirSync(path.join(root, 'scripts'));
|
||||
fs.writeFileSync(path.join(root, 'shell/test.sh'), '# fixture');
|
||||
fs.writeFileSync(path.join(root, '.env.example'), 'BACK_PORT=5700\n');
|
||||
const custom = 'BACK_PORT=5800\nJWT_SECRET=synthetic-test-only\n';
|
||||
if (existing)
|
||||
fs.writeFileSync(path.join(root, '.env'), custom, { mode: 0o600 });
|
||||
const trace = path.join(root, 'runtime-install.json');
|
||||
fs.writeFileSync(
|
||||
path.join(root, 'scripts/install-runtime-tools.cjs'),
|
||||
`const fs = require('node:fs');
|
||||
fs.writeFileSync(process.env.QL_INSTALL_TRACE, JSON.stringify(process.argv.slice(2)));
|
||||
if (process.env.QL_INSTALL_FAILURE === '1') process.exit(19);
|
||||
`,
|
||||
);
|
||||
return { root, custom, trace };
|
||||
}
|
||||
|
||||
function startup(root, trace, command, fail = false) {
|
||||
return spawnSync(
|
||||
'/bin/bash',
|
||||
[
|
||||
'-c',
|
||||
`npm(){ [[ "$*" == 'prefix --global' ]] || return 2; printf '%s\\n' "$QL_DIR/global-prefix"; };
|
||||
node(){ command "$QL_TEST_NODE" "$@"; };
|
||||
pip3(){ :; }; apk(){ :; }; sudo(){ "$@"; }; python3(){ printf 3.11; };
|
||||
${source}`,
|
||||
'startup-test',
|
||||
command,
|
||||
],
|
||||
{
|
||||
encoding: 'utf8',
|
||||
env: {
|
||||
...process.env,
|
||||
QL_DIR: root,
|
||||
QL_DATA_DIR: path.join(root, 'data'),
|
||||
QL_OS_TYPE: 'alpine',
|
||||
QL_INSTALL_TRACE: trace,
|
||||
QL_TEST_NODE: process.execPath,
|
||||
QL_INSTALL_FAILURE: fail ? '1' : '0',
|
||||
},
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
for (const command of ['start', 'reload']) {
|
||||
for (const existing of [false, true]) {
|
||||
test(`${command} ${
|
||||
existing ? 'preserves existing' : 'initializes missing'
|
||||
} .env`, (t) => {
|
||||
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'ql-env-'));
|
||||
t.after(() => fs.rmSync(root, { recursive: true, force: true }));
|
||||
fs.mkdirSync(path.join(root, 'shell'));
|
||||
fs.writeFileSync(path.join(root, 'shell/test.sh'), '# fixture');
|
||||
fs.writeFileSync(path.join(root, '.env.example'), 'BACK_PORT=5700\n');
|
||||
const custom = 'BACK_PORT=5800\nJWT_SECRET=synthetic-test-only\n';
|
||||
if (existing)
|
||||
fs.writeFileSync(path.join(root, '.env'), custom, { mode: 0o600 });
|
||||
const result = spawnSync(
|
||||
'/bin/bash',
|
||||
[
|
||||
'-c',
|
||||
'npm(){ :; }; pip3(){ :; }; apk(){ :; }; sudo(){ "$@"; }; python3(){ printf 3.11; };\n' +
|
||||
source,
|
||||
'startup-test',
|
||||
command,
|
||||
],
|
||||
{
|
||||
encoding: 'utf8',
|
||||
env: {
|
||||
...process.env,
|
||||
QL_DIR: root,
|
||||
QL_DATA_DIR: root + '/data',
|
||||
QL_OS_TYPE: 'alpine',
|
||||
},
|
||||
},
|
||||
);
|
||||
const { root, custom, trace } = fixture(t, existing);
|
||||
const result = startup(root, trace, command);
|
||||
assert.equal(result.status, 0, result.stderr);
|
||||
if (command === 'start')
|
||||
assert.deepEqual(JSON.parse(fs.readFileSync(trace, 'utf8')), [
|
||||
'--archive',
|
||||
path.join(root, 'static/runtime-tools.tgz'),
|
||||
'--proof',
|
||||
path.join(root, 'static/runtime-tools-proof.json'),
|
||||
'--prefix',
|
||||
path.join(root, 'global-prefix'),
|
||||
]);
|
||||
else assert.equal(fs.existsSync(trace), false);
|
||||
assert.equal(
|
||||
fs.readFileSync(path.join(root, '.env'), 'utf8'),
|
||||
existing ? custom : 'BACK_PORT=5700\n',
|
||||
@@ -51,3 +85,18 @@ for (const command of ['start', 'reload']) {
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
for (const existing of [false, true]) {
|
||||
test(`failed runtime installation stops before ${
|
||||
existing ? 'changing existing' : 'creating missing'
|
||||
} .env`, (t) => {
|
||||
const { root, custom, trace } = fixture(t, existing);
|
||||
const result = startup(root, trace, 'start', true);
|
||||
assert.equal(result.status, 19, result.stderr);
|
||||
assert.equal(fs.existsSync(trace), true);
|
||||
if (existing) {
|
||||
assert.equal(fs.readFileSync(path.join(root, '.env'), 'utf8'), custom);
|
||||
assert.equal(fs.statSync(path.join(root, '.env')).mode & 0o777, 0o600);
|
||||
} else assert.equal(fs.existsSync(path.join(root, '.env')), false);
|
||||
});
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user