mirror of
https://github.com/whyour/qinglong.git
synced 2026-11-06 17:34:34 +08:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8976447b1e |
@@ -550,10 +550,14 @@ jobs:
|
||||
cache-from: type=registry,ref=whyour/qinglong:cache-debian-python3.10
|
||||
cache-to: type=registry,ref=whyour/qinglong:cache-debian-python3.10,mode=max
|
||||
|
||||
# One unmodified amd64 package for migration acceptance only. This does not
|
||||
# Unmodified amd64 packages for glibc and musl migration acceptance. This does not
|
||||
# publish bundles or advance development/formal channel indexes.
|
||||
build-npm-migration-bundle:
|
||||
needs: build-debian
|
||||
needs: [build-debian, build-alpine]
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
libc: [glibc, musl]
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 20
|
||||
steps:
|
||||
@@ -561,7 +565,7 @@ jobs:
|
||||
- uses: docker/setup-buildx-action@v4
|
||||
- name: Export the official platform package from this run's image digest
|
||||
env:
|
||||
IMAGE_DIGEST: ${{ needs.build-debian.outputs.image-digest }}
|
||||
IMAGE_DIGEST: ${{ matrix.libc == 'musl' && needs.build-alpine.outputs.image-digest || needs.build-debian.outputs.image-digest }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
[[ "$IMAGE_DIGEST" =~ ^sha256:[a-f0-9]{64}$ ]]
|
||||
@@ -569,20 +573,21 @@ jobs:
|
||||
docker buildx build --platform linux/amd64 --build-arg "BASE_IMAGE=$image" --file docker/Dockerfile.upgrade-bundle --output "type=local,dest=$RUNNER_TEMP/npm-migration-bundle" .
|
||||
- uses: actions/upload-artifact@v6
|
||||
with:
|
||||
name: npm-migration-bundle-${{ github.sha }}
|
||||
name: npm-migration-bundle-${{ matrix.libc }}-${{ github.sha }}
|
||||
path: ${{ runner.temp }}/npm-migration-bundle/*
|
||||
if-no-files-found: error
|
||||
retention-days: 3
|
||||
|
||||
verify-npm-package:
|
||||
needs: [build-static, build-npm-migration-bundle]
|
||||
needs: [build-static, build-npm-migration-bundle, build-alpine]
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 30
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
node: ['20.18.1', '22.17.0', '24']
|
||||
container: node:${{ matrix.node }}-bookworm
|
||||
node: ['20.18.1', '22.17.0', '24', 'alpine']
|
||||
container:
|
||||
image: ${{ matrix.node == 'alpine' && format('ghcr.io/{0}@{1}', github.repository, needs.build-alpine.outputs.image-digest) || format('node:{0}-bookworm', matrix.node) }}
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
- uses: actions/download-artifact@v6
|
||||
@@ -591,12 +596,16 @@ jobs:
|
||||
path: ${{ runner.temp }}/npm-panel/
|
||||
- uses: actions/download-artifact@v6
|
||||
with:
|
||||
name: npm-migration-bundle-${{ github.sha }}
|
||||
name: npm-migration-bundle-${{ matrix.node == 'alpine' && 'musl' || 'glibc' }}-${{ github.sha }}
|
||||
path: ${{ runner.temp }}/npm-migration-bundle/
|
||||
- name: Install isolated service test tools
|
||||
run: |
|
||||
apt-get update
|
||||
apt-get install -y --no-install-recommends python3 openssl curl jq nginx procps
|
||||
if [ -f /etc/alpine-release ]; then
|
||||
apk add --no-cache python3 openssl curl jq nginx procps make g++
|
||||
else
|
||||
apt-get update
|
||||
apt-get install -y --no-install-recommends python3 openssl curl jq nginx procps
|
||||
fi
|
||||
# The published legacy launcher resets PATH to standard directories.
|
||||
# /usr/local is confined to this disposable job container; PM2_HOME
|
||||
# remains private to each verification run.
|
||||
|
||||
@@ -22,8 +22,16 @@ RUN set -x \
|
||||
npm_config_target_libc=musl \
|
||||
QL_BUILD_ONLY=1 pnpm install --prod --frozen-lockfile
|
||||
|
||||
# The amd64 APK runtime has crashed in native URL parsing on dev. Keep its
|
||||
# URL implementation bundled with the pinned upstream musl Node binary.
|
||||
FROM --platform=linux/amd64 node:24.18.1-alpine3.24@sha256:f70403e87646dc51b45295f4b8b70cdad0b63d2297c4c9899119b03f7af7a6b3 AS node-runtime-amd64
|
||||
|
||||
FROM python:3.11-alpine
|
||||
|
||||
ARG TARGETARCH
|
||||
ARG TARGETVARIANT
|
||||
COPY --from=node-runtime-amd64 /usr/local/bin/node /tmp/node-runtime-amd64
|
||||
|
||||
ARG QL_MAINTAINER="whyour"
|
||||
LABEL maintainer="${QL_MAINTAINER}"
|
||||
ARG QL_URL=https://github.com/${QL_MAINTAINER}/qinglong.git
|
||||
@@ -62,6 +70,10 @@ RUN set -x \
|
||||
netcat-openbsd \
|
||||
unzip \
|
||||
npm \
|
||||
&& if [ "$TARGETARCH" = amd64 ]; then \
|
||||
mv /tmp/node-runtime-amd64 /usr/local/bin/node; \
|
||||
ln -sf /usr/local/bin/node /usr/bin/node; \
|
||||
else rm /tmp/node-runtime-amd64; fi \
|
||||
&& rm -rf /var/cache/apk/* \
|
||||
&& apk update \
|
||||
&& ln -sf /usr/share/zoneinfo/Asia/Shanghai /etc/localtime \
|
||||
@@ -86,6 +98,10 @@ COPY static/ /ql/static/
|
||||
COPY docker/verify-build.cjs docker/build-manifest.cjs /tmp/
|
||||
COPY --from=builder /tmp/build/pnpm-lock.yaml /tmp/dependency-lock.yaml
|
||||
RUN cd ${QL_DIR} && node /tmp/verify-build.cjs /tmp/dependency-lock.yaml
|
||||
RUN if [ "$TARGETARCH" = arm ] && [ "${TARGETVARIANT:-}" = v6 ]; then \
|
||||
QEMU_CPU=arm1176 node --jitless ${QL_DIR}/shell/node-runtime-probe.cjs; \
|
||||
else node ${QL_DIR}/shell/node-runtime-probe.cjs; fi
|
||||
|
||||
|
||||
ENV PNPM_HOME=${QL_DIR}/data/dep_cache/node \
|
||||
PYTHON_HOME=${QL_DIR}/data/dep_cache/python3 \
|
||||
|
||||
@@ -22,8 +22,16 @@ RUN set -x \
|
||||
npm_config_target_libc=musl \
|
||||
QL_BUILD_ONLY=1 pnpm install --prod --frozen-lockfile
|
||||
|
||||
# The amd64 APK runtime has crashed in native URL parsing on dev. Keep its
|
||||
# URL implementation bundled with the pinned upstream musl Node binary.
|
||||
FROM --platform=linux/amd64 node:24.18.1-alpine3.24@sha256:f70403e87646dc51b45295f4b8b70cdad0b63d2297c4c9899119b03f7af7a6b3 AS node-runtime-amd64
|
||||
|
||||
FROM python:3.10-alpine
|
||||
|
||||
ARG TARGETARCH
|
||||
ARG TARGETVARIANT
|
||||
COPY --from=node-runtime-amd64 /usr/local/bin/node /tmp/node-runtime-amd64
|
||||
|
||||
ARG QL_MAINTAINER="whyour"
|
||||
LABEL maintainer="${QL_MAINTAINER}"
|
||||
ARG QL_URL=https://github.com/${QL_MAINTAINER}/qinglong.git
|
||||
@@ -62,6 +70,10 @@ RUN set -x \
|
||||
netcat-openbsd \
|
||||
unzip \
|
||||
npm \
|
||||
&& if [ "$TARGETARCH" = amd64 ]; then \
|
||||
mv /tmp/node-runtime-amd64 /usr/local/bin/node; \
|
||||
ln -sf /usr/local/bin/node /usr/bin/node; \
|
||||
else rm /tmp/node-runtime-amd64; fi \
|
||||
&& rm -rf /var/cache/apk/* \
|
||||
&& apk update \
|
||||
&& ln -sf /usr/share/zoneinfo/Asia/Shanghai /etc/localtime \
|
||||
@@ -86,6 +98,10 @@ COPY static/ /ql/static/
|
||||
COPY docker/verify-build.cjs docker/build-manifest.cjs /tmp/
|
||||
COPY --from=builder /tmp/build/pnpm-lock.yaml /tmp/dependency-lock.yaml
|
||||
RUN cd ${QL_DIR} && node /tmp/verify-build.cjs /tmp/dependency-lock.yaml
|
||||
RUN if [ "$TARGETARCH" = arm ] && [ "${TARGETVARIANT:-}" = v6 ]; then \
|
||||
QEMU_CPU=arm1176 node --jitless ${QL_DIR}/shell/node-runtime-probe.cjs; \
|
||||
else node ${QL_DIR}/shell/node-runtime-probe.cjs; fi
|
||||
|
||||
|
||||
ENV PNPM_HOME=${QL_DIR}/data/dep_cache/node \
|
||||
PYTHON_HOME=${QL_DIR}/data/dep_cache/python3 \
|
||||
|
||||
@@ -33,6 +33,7 @@ const requiredFiles = [
|
||||
'shell/install-package.cjs',
|
||||
'shell/upgrade-lock.yaml',
|
||||
'shell/runtime-guard.cjs',
|
||||
'shell/node-runtime-probe.cjs',
|
||||
'shell/start.sh',
|
||||
'shell/start-simplify.sh',
|
||||
'shell/task.sh',
|
||||
@@ -381,6 +382,16 @@ async function verifyPanel(input, output) {
|
||||
).stdout.trim();
|
||||
root = path.join(globalRoot, '@whyour/qinglong');
|
||||
env.QL_DIR = root;
|
||||
const runtimeProbes = [];
|
||||
const checkRuntime = async () => {
|
||||
const result = await run(
|
||||
process.execPath,
|
||||
[path.join(root, 'shell/node-runtime-probe.cjs')],
|
||||
{ cwd: temp, env },
|
||||
);
|
||||
runtimeProbes.push(JSON.parse(result.stdout.trim()));
|
||||
};
|
||||
await checkRuntime();
|
||||
// Use a subprocess: cached candidate modules must not mask missing files
|
||||
// after old-package installation and the real npm overwrite below.
|
||||
await run(
|
||||
@@ -566,6 +577,7 @@ async function verifyPanel(input, output) {
|
||||
const applying = await api('update/system', 'PUT');
|
||||
await job(applying.id, 'completed');
|
||||
const afterOnline = await healthy();
|
||||
await checkRuntime();
|
||||
assert.notEqual(afterOnline.build.instanceId, original.build.instanceId);
|
||||
assert.equal(afterOnline.build.sourceCommit, fixture.sourceCommit);
|
||||
await fs.access(path.join(root, 'node_modules/.pnpm'));
|
||||
@@ -617,6 +629,7 @@ async function verifyPanel(input, output) {
|
||||
timeout: 240000,
|
||||
});
|
||||
const afterOffline = await healthy();
|
||||
await checkRuntime();
|
||||
assert.notEqual(
|
||||
afterOffline.build.instanceId,
|
||||
afterOnline.build.instanceId,
|
||||
@@ -662,6 +675,7 @@ async function verifyPanel(input, output) {
|
||||
},
|
||||
migratedFromNpmToOfficialPnpmLayout: true,
|
||||
existingNodePreserved: true,
|
||||
nativeUrlRuntimeProbes: runtimeProbes,
|
||||
frontendAndBackendStarted: true,
|
||||
checksumFailureKeptOldService: true,
|
||||
prestartBeforeReady: true,
|
||||
|
||||
@@ -0,0 +1,61 @@
|
||||
const assert = require('node:assert/strict');
|
||||
const fs = require('node:fs');
|
||||
const os = require('node:os');
|
||||
const path = require('node:path');
|
||||
const { createHash } = require('node:crypto');
|
||||
const { spawnSync } = require('node:child_process');
|
||||
|
||||
// Run in new processes: a healthy parent can outlive a broken native runtime.
|
||||
function probeNodeRuntime(executable = process.execPath) {
|
||||
const directory = fs.mkdtempSync(path.join(os.tmpdir(), 'ql-node-url-'));
|
||||
const source = `
|
||||
const assert = require('node:assert/strict');
|
||||
const { pathToFileURL, fileURLToPath } = require('node:url');
|
||||
for (let i = 0; i < 1000; i++) {
|
||||
assert.equal(new URL('/任务?q=' + i, 'https://example.com').hostname, 'example.com');
|
||||
assert.equal(new URL('https://例子.测试').hostname, 'xn--fsqu00a.xn--0zwm56d');
|
||||
assert.equal(new URL('http://[::1]:5700').port, '5700');
|
||||
const filename = '/tmp/中文 path/' + i + '.cjs';
|
||||
assert.equal(fileURLToPath(pathToFileURL(filename)), filename);
|
||||
}
|
||||
console.log('QINGLONG_NODE_URL_OK');
|
||||
`;
|
||||
const file = path.join(directory, 'probe.cjs');
|
||||
fs.writeFileSync(file, source);
|
||||
try {
|
||||
for (let i = 0; i < 4; i++) {
|
||||
const args = process.execArgv.includes('--jitless') ? ['--jitless'] : [];
|
||||
args.push(...(i % 2 ? [file] : ['-e', source]));
|
||||
const child = spawnSync(executable, args, {
|
||||
encoding: 'utf8',
|
||||
timeout: 20000,
|
||||
maxBuffer: 65536,
|
||||
env: { ...process.env, NODE_OPTIONS: '' },
|
||||
});
|
||||
assert.equal(
|
||||
child.status,
|
||||
0,
|
||||
`Node URL runtime probe failed (${i % 2 ? 'file' : 'eval'}): signal=${
|
||||
child.signal
|
||||
}; status=${child.status}; ${child.error?.message || child.stderr}`,
|
||||
);
|
||||
assert.match(child.stdout, /QINGLONG_NODE_URL_OK/);
|
||||
}
|
||||
return {
|
||||
ok: true,
|
||||
node: process.version,
|
||||
architecture: process.arch,
|
||||
ada: process.versions.ada,
|
||||
processes: 4,
|
||||
urlIterationsPerProcess: 1000,
|
||||
nodeSha256: createHash('sha256')
|
||||
.update(fs.readFileSync(executable))
|
||||
.digest('hex'),
|
||||
};
|
||||
} finally {
|
||||
fs.rmSync(directory, { recursive: true, force: true });
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = { probeNodeRuntime };
|
||||
if (require.main === module) console.log(JSON.stringify(probeNodeRuntime()));
|
||||
@@ -0,0 +1,25 @@
|
||||
const test = require('node:test');
|
||||
const assert = require('node:assert/strict');
|
||||
const fs = require('node:fs');
|
||||
const os = require('node:os');
|
||||
const path = require('node:path');
|
||||
const { probeNodeRuntime } = require('../../shell/node-runtime-probe.cjs');
|
||||
|
||||
test('the retained Node handles URL, IDNA and file paths in fresh eval and file processes', () => {
|
||||
const result = probeNodeRuntime();
|
||||
assert.equal(result.ok, true);
|
||||
assert.equal(result.processes, 4);
|
||||
assert.match(result.nodeSha256, /^[0-9a-f]{64}$/);
|
||||
});
|
||||
|
||||
test('a native child crash fails the runtime probe instead of accepting its healthy parent', (t) => {
|
||||
if (process.platform === 'win32') return t.skip('POSIX crash fixture');
|
||||
const directory = fs.mkdtempSync(path.join(os.tmpdir(), 'ql-node-crash-'));
|
||||
t.after(() => fs.rmSync(directory, { recursive: true, force: true }));
|
||||
const executable = path.join(directory, 'node');
|
||||
fs.writeFileSync(executable, '#!/bin/sh\nkill -SEGV $$\n', { mode: 0o700 });
|
||||
assert.throws(
|
||||
() => probeNodeRuntime(executable),
|
||||
/Node URL runtime probe failed.*SIGSEGV/,
|
||||
);
|
||||
});
|
||||
@@ -484,14 +484,18 @@ test('npm publication consumes the exact archive whose isolated installation pas
|
||||
assert.deepEqual(verify.needs, [
|
||||
'build-static',
|
||||
'build-npm-migration-bundle',
|
||||
'build-alpine',
|
||||
]);
|
||||
assert.ok(
|
||||
verify.steps.some(
|
||||
(step) => step.with?.name === 'npm-migration-bundle-${{ github.sha }}',
|
||||
(step) =>
|
||||
step.with?.name ===
|
||||
"npm-migration-bundle-${{ matrix.node == 'alpine' && 'musl' || 'glibc' }}-${{ github.sha }}",
|
||||
),
|
||||
);
|
||||
const migration = workflow.jobs['build-npm-migration-bundle'];
|
||||
assert.equal(migration.needs, 'build-debian');
|
||||
assert.deepEqual(migration.needs, ['build-debian', 'build-alpine']);
|
||||
assert.deepEqual(migration.strategy.matrix.libc, ['glibc', 'musl']);
|
||||
assert.ok(
|
||||
migration.steps.some((step) =>
|
||||
/Dockerfile.upgrade-bundle/.test(step.run || ''),
|
||||
@@ -502,8 +506,17 @@ test('npm publication consumes the exact archive whose isolated installation pas
|
||||
/publish-upgrade-bundles/.test(step.run || ''),
|
||||
),
|
||||
);
|
||||
assert.deepEqual(verify.strategy.matrix.node, ['20.18.1', '22.17.0', '24']);
|
||||
assert.match(verify.container, /node:.*bookworm/);
|
||||
assert.deepEqual(verify.strategy.matrix.node, [
|
||||
'20.18.1',
|
||||
'22.17.0',
|
||||
'24',
|
||||
'alpine',
|
||||
]);
|
||||
assert.match(
|
||||
verify.container.image,
|
||||
/needs\.build-alpine\.outputs\.image-digest/,
|
||||
);
|
||||
assert.match(verify.container.image, /node:.*bookworm/);
|
||||
const tools = verify.steps.find(
|
||||
(step) => step.name === 'Install isolated service test tools',
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user