mirror of
https://github.com/whyour/qinglong.git
synced 2026-11-05 21:01:11 +08:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b95d6583ca | ||
|
|
27a9c35809 | ||
|
|
af93fb6cf0 | ||
|
|
2218c2c175 |
@@ -28,7 +28,7 @@ const backend = read(path.resolve(__dirname, '../app.js'));
|
|||||||
const root = process.env.QL_DIR || path.resolve(__dirname, '../../..');
|
const root = process.env.QL_DIR || path.resolve(__dirname, '../../..');
|
||||||
const version = read(path.join(root, 'version.yaml'))
|
const version = read(path.join(root, 'version.yaml'))
|
||||||
?.toString()
|
?.toString()
|
||||||
.match(/^\uFEFF?version:\s*["']?(\d+\.\d+\.\d+)/m)?.[1];
|
.match(/^\uFEFF?version:\s*["']?(\d+\.\d+\.\d+(?:-[\w.-]+)?(?:\+[\w.-]+)?)/m)?.[1];
|
||||||
|
|
||||||
export const runtimeBuild = Object.freeze({
|
export const runtimeBuild = Object.freeze({
|
||||||
instanceId: randomUUID(),
|
instanceId: randomUUID(),
|
||||||
|
|||||||
@@ -61,7 +61,9 @@ async function withPanelLifecycle(root, tmp, env, operation, signal) {
|
|||||||
|
|
||||||
async function targetVersion(source) {
|
async function targetVersion(source) {
|
||||||
const text = await fs.readFile(path.join(source, 'version.yaml'), 'utf8');
|
const text = await fs.readFile(path.join(source, 'version.yaml'), 'utf8');
|
||||||
const version = text.match(/^\uFEFF?version:\s*["']?(\d+\.\d+\.\d+)/m)?.[1];
|
const version = text.match(
|
||||||
|
/^\uFEFF?version:\s*["']?(\d+\.\d+\.\d+(?:-[\w.-]+)?(?:\+[\w.-]+)?)/m,
|
||||||
|
)?.[1];
|
||||||
if (!version) throw new Error('Invalid update version');
|
if (!version) throw new Error('Invalid update version');
|
||||||
return version;
|
return version;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -92,7 +92,9 @@ async function prestartBackend(
|
|||||||
.digest('hex');
|
.digest('hex');
|
||||||
const version = (
|
const version = (
|
||||||
await fs.readFile(path.join(selected.source, 'version.yaml'), 'utf8')
|
await fs.readFile(path.join(selected.source, 'version.yaml'), 'utf8')
|
||||||
).match(/^\uFEFF?version:\s*["']?(\d+\.\d+\.\d+)/m)?.[1];
|
).match(
|
||||||
|
/^\uFEFF?version:\s*["']?(\d+\.\d+\.\d+(?:-[\w.-]+)?(?:\+[\w.-]+)?)/m,
|
||||||
|
)?.[1];
|
||||||
// An allowlist prevents NODE_OPTIONS, startup hooks, real data paths and
|
// An allowlist prevents NODE_OPTIONS, startup hooks, real data paths and
|
||||||
// package-manager settings from escaping into the disposable backend.
|
// package-manager settings from escaping into the disposable backend.
|
||||||
const env = {
|
const env = {
|
||||||
|
|||||||
@@ -14,9 +14,9 @@ CI 从同次构建的不可变镜像摘要导出完整 tar.gz,包含源码中
|
|||||||
gh workflow run build-docker-image.yml --ref develop
|
gh workflow run build-docker-image.yml --ref develop
|
||||||
```
|
```
|
||||||
|
|
||||||
开发包更新共用的 `upgrade-develop/latest.json`。`develop` 和 `debian-dev` 镜像都使用该索引,按实际 Node 架构和 musl/glibc 选择包,无需设置测试专用下载源。开发批次位于 `upgrade-build-develop-v<version>-<run-id>-<run-attempt>`,不会改写正式版本或 `upgrade-master`。同一开发渠道的镜像构建和包发布串行执行,与 master 独立。
|
开发包只使用 `upgrade-develop` 这一个 prerelease。`develop` 和 `debian-dev` 镜像读取其中的 `latest.json`,按实际 Node 架构和 musl/glibc 选择包,无需设置测试专用下载源。新批次先以带构建 ID 的文件名上传并校验,再切换 `latest.json`;索引仍保留开发版本供客户端判断更新。开发渠道不会改写正式版本或 `upgrade-master`。同一开发渠道的镜像构建和包发布串行执行,与 master 独立。
|
||||||
|
|
||||||
开发包只保留最新一个已验证批次:所有平台资产和渠道索引的大小、SHA256 校验成功后,删除其他名称匹配的开发构建 prerelease;不删除正式包、渠道索引或无法分类的旧 release。构建或上传失败不会删除上一套可用包。旧批次被删除后,已选中旧索引但尚未下载完的客户端可能需要重新发起下载;准备失败仍保留旧服务和数据。
|
开发包只保留最新一个已验证批次:所有平台资产和渠道索引的大小、SHA256 校验成功后,清理 `upgrade-develop` 中旧批次资产及旧命名的开发构建 prerelease;不删除正式包或无法分类的 release。构建或上传失败不会删除上一套可用包。旧资产被删除后,已选中旧索引但尚未下载完的客户端可能需要重新发起下载;准备失败仍保留旧服务和数据。
|
||||||
|
|
||||||
开发和正式包都必须在 11 个平台完成归档、解包后复验及依赖试运行,并且全部上传资产的大小和 SHA256 复验通过后,才更新各自索引。平台范围与现有 Alpine/Debian 镜像一致:ARMv6 和 ARMv7 仅提供 musl 包,Debian 不再发布 ARMv7 镜像或 glibc 平台包。
|
开发和正式包都必须在 11 个平台完成归档、解包后复验及依赖试运行,并且全部上传资产的大小和 SHA256 复验通过后,才更新各自索引。平台范围与现有 Alpine/Debian 镜像一致:ARMv6 和 ARMv7 仅提供 musl 包,Debian 不再发布 ARMv7 镜像或 glibc 平台包。
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,11 @@
|
|||||||
|
# 青龙 2.24.0-dev.0 开发候选
|
||||||
|
|
||||||
|
这是 `develop` 渠道的开发构建,上一正式版本为 2.23.0。此版本尚未正式发布。
|
||||||
|
|
||||||
|
## 本轮变化
|
||||||
|
|
||||||
|
- Alpine 与 Debian 镜像统一使用 Node.js 24;SQLite 依赖升级至 1.1.4。
|
||||||
|
- Alpine 镜像保留 arm/v6 和 arm/v7,Debian 镜像暂不提供 arm/v7。
|
||||||
|
- ARM64 升级包改用原生 runner 构建,避免在 x64 QEMU 下归档时发生异常。
|
||||||
|
|
||||||
|
升级前请备份完整持久化数据,并在目标设备验证脚本、依赖和通知。正式发布说明将在候选验收后整理。
|
||||||
+1
-1
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "@whyour/qinglong",
|
"name": "@whyour/qinglong",
|
||||||
"packageManager": "pnpm@8.3.1",
|
"packageManager": "pnpm@8.3.1",
|
||||||
"version": "2.23.0",
|
"version": "2.24.0-dev.0",
|
||||||
"description": "Timed task management platform supporting Python3, JavaScript, Shell, Typescript",
|
"description": "Timed task management platform supporting Python3, JavaScript, Shell, Typescript",
|
||||||
"repository": {
|
"repository": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
|
|||||||
+184
-111
@@ -18,11 +18,12 @@ const platforms = [
|
|||||||
'linux-s390x-glibc',
|
'linux-s390x-glibc',
|
||||||
];
|
];
|
||||||
function releaseIndex(directory, commit, repository, options = {}) {
|
function releaseIndex(directory, commit, repository, options = {}) {
|
||||||
const { version, releaseTag = `upgrade-${commit}` } = options;
|
const { version, releaseTag = `upgrade-${commit}`, assetBuildId } = options;
|
||||||
if (
|
if (
|
||||||
!/^[a-f0-9]{40}$/.test(commit) ||
|
!/^[a-f0-9]{40}$/.test(commit) ||
|
||||||
!/^[\w.-]+\/[\w.-]+$/.test(repository) ||
|
!/^[\w.-]+\/[\w.-]+$/.test(repository) ||
|
||||||
!/^[\w.+-]+$/.test(releaseTag)
|
!/^[\w.+-]+$/.test(releaseTag) ||
|
||||||
|
(assetBuildId && !/^\d+-\d+$/.test(assetBuildId))
|
||||||
)
|
)
|
||||||
throw new Error('Invalid release source');
|
throw new Error('Invalid release source');
|
||||||
const packages = {};
|
const packages = {};
|
||||||
@@ -47,11 +48,45 @@ function releaseIndex(directory, commit, repository, options = {}) {
|
|||||||
sha256: record.sha256,
|
sha256: record.sha256,
|
||||||
url: `https://github.com/${repository}/releases/download/${encodeURIComponent(
|
url: `https://github.com/${repository}/releases/download/${encodeURIComponent(
|
||||||
releaseTag,
|
releaseTag,
|
||||||
)}/${filename}`,
|
)}/${
|
||||||
|
assetBuildId
|
||||||
|
? filename.replace(/\.tar\.gz$/, `-${assetBuildId}.tar.gz`)
|
||||||
|
: filename
|
||||||
|
}`,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
return { format: 2, sourceCommit: commit, packages };
|
return { format: 2, sourceCommit: commit, packages };
|
||||||
}
|
}
|
||||||
|
function stageDevelopmentAssets(directory, commit, buildId) {
|
||||||
|
const staged = fs.mkdtempSync(path.join(directory, '.upgrade-develop-'));
|
||||||
|
const assets = [];
|
||||||
|
try {
|
||||||
|
for (const target of platforms) {
|
||||||
|
const filename = `qinglong-${commit}-${target}.tar.gz`;
|
||||||
|
const archiveName = filename.replace(/\.tar\.gz$/, `-${buildId}.tar.gz`);
|
||||||
|
const archive = path.join(staged, archiveName);
|
||||||
|
fs.linkSync(path.join(directory, filename), archive);
|
||||||
|
const record = JSON.parse(
|
||||||
|
fs.readFileSync(path.join(directory, filename + '.json')),
|
||||||
|
);
|
||||||
|
fs.writeFileSync(
|
||||||
|
archive + '.json',
|
||||||
|
JSON.stringify({
|
||||||
|
...record,
|
||||||
|
filename: archiveName,
|
||||||
|
}) + '\n',
|
||||||
|
);
|
||||||
|
assets.push(archive, archive + '.json');
|
||||||
|
}
|
||||||
|
const installer = path.join(staged, `install-bundle-${buildId}.cjs`);
|
||||||
|
fs.linkSync(path.join(directory, 'install-bundle.cjs'), installer);
|
||||||
|
assets.push(installer);
|
||||||
|
return { staged, assets };
|
||||||
|
} catch (error) {
|
||||||
|
fs.rmSync(staged, { recursive: true, force: true });
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
function publishBundles({
|
function publishBundles({
|
||||||
directory,
|
directory,
|
||||||
commit,
|
commit,
|
||||||
@@ -75,13 +110,14 @@ function publishBundles({
|
|||||||
// unchanged. Only the version/channel pointer is overwritten, never bytes
|
// unchanged. Only the version/channel pointer is overwritten, never bytes
|
||||||
// that an in-flight download has already selected by hash.
|
// that an in-flight download has already selected by hash.
|
||||||
const development = branch === 'develop';
|
const development = branch === 'develop';
|
||||||
const tag = `upgrade-build-${
|
const tag = development
|
||||||
development ? 'develop-' : ''
|
? 'upgrade-develop'
|
||||||
}v${version}-${buildId}`;
|
: `upgrade-build-v${version}-${buildId}`;
|
||||||
const index = {
|
const index = {
|
||||||
...releaseIndex(directory, commit, repository, {
|
...releaseIndex(directory, commit, repository, {
|
||||||
version,
|
version,
|
||||||
releaseTag: tag,
|
releaseTag: tag,
|
||||||
|
assetBuildId: development ? buildId : undefined,
|
||||||
}),
|
}),
|
||||||
version,
|
version,
|
||||||
buildId,
|
buildId,
|
||||||
@@ -93,7 +129,7 @@ function publishBundles({
|
|||||||
sha256: sha256(installer),
|
sha256: sha256(installer),
|
||||||
url: `https://github.com/${repository}/releases/download/${encodeURIComponent(
|
url: `https://github.com/${repository}/releases/download/${encodeURIComponent(
|
||||||
tag,
|
tag,
|
||||||
)}/install-bundle.cjs`,
|
)}/${development ? `install-bundle-${buildId}.cjs` : 'install-bundle.cjs'}`,
|
||||||
};
|
};
|
||||||
fs.writeFileSync(
|
fs.writeFileSync(
|
||||||
path.join(directory, 'latest.json'),
|
path.join(directory, 'latest.json'),
|
||||||
@@ -113,7 +149,9 @@ function publishBundles({
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
const notes =
|
const notes =
|
||||||
`Verified platform bundles for v${version}, source ${commit}, build ${buildId}. Existing Node runtime is retained.\n\n` +
|
`${
|
||||||
|
development ? 'Development' : 'Verified'
|
||||||
|
} platform bundles for v${version}, source ${commit}, build ${buildId}. Existing Node runtime is retained.\n\n` +
|
||||||
Object.entries(index.packages)
|
Object.entries(index.packages)
|
||||||
.map(([target, entry]) => `- [${target}](${entry.url})`)
|
.map(([target, entry]) => `- [${target}](${entry.url})`)
|
||||||
.join('\n') +
|
.join('\n') +
|
||||||
@@ -147,118 +185,153 @@ function publishBundles({
|
|||||||
console.log('Newer source exists; leaving version and channel unchanged');
|
console.log('Newer source exists; leaving version and channel unchanged');
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
ensureRelease(tag, `Platform upgrade v${version} (${buildId})`);
|
const staged = development
|
||||||
const assets = platforms
|
? stageDevelopmentAssets(directory, commit, buildId)
|
||||||
.flatMap((target) => {
|
: null;
|
||||||
const archive = path.join(
|
try {
|
||||||
directory,
|
|
||||||
`qinglong-${commit}-${target}.tar.gz`,
|
|
||||||
);
|
|
||||||
return [archive, archive + '.json'];
|
|
||||||
})
|
|
||||||
.concat(
|
|
||||||
path.join(directory, 'install-bundle.cjs'),
|
|
||||||
path.join(directory, 'latest.json'),
|
|
||||||
);
|
|
||||||
const published = lookup(tag);
|
|
||||||
const missing = [];
|
|
||||||
const matches = (asset, file) => {
|
|
||||||
const bytes = fs.readFileSync(file);
|
|
||||||
return (
|
|
||||||
asset?.size === bytes.length && asset.digest === `sha256:${sha256(bytes)}`
|
|
||||||
);
|
|
||||||
};
|
|
||||||
for (const file of assets) {
|
|
||||||
const previous = published.assets.find(
|
|
||||||
(a) => a.name === path.basename(file),
|
|
||||||
);
|
|
||||||
if (!previous) missing.push(file);
|
|
||||||
else if (!matches(previous, file))
|
|
||||||
throw new Error(
|
|
||||||
`Immutable release asset differs: ${path.basename(file)}`,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
if (missing.length)
|
|
||||||
gh('release', 'upload', tag, ...missing, '--repo', repository);
|
|
||||||
const uploaded = lookup(tag);
|
|
||||||
if (
|
|
||||||
!assets.every((file) =>
|
|
||||||
matches(
|
|
||||||
uploaded.assets.find((a) => a.name === path.basename(file)),
|
|
||||||
file,
|
|
||||||
),
|
|
||||||
)
|
|
||||||
)
|
|
||||||
throw new Error('Published platform bundle verification failed');
|
|
||||||
|
|
||||||
// Each channel serializes its image builds and pointer writes as one release.
|
|
||||||
// A rerun of an earlier workflow is allowed to replace the same
|
|
||||||
// version/commit; an obsolete source commit must never advance either alias.
|
|
||||||
const aliases = development
|
|
||||||
? ['upgrade-develop']
|
|
||||||
: [`upgrade-v${version}`, 'upgrade-master'];
|
|
||||||
for (const ref of aliases) {
|
|
||||||
if (!currentSource()) {
|
|
||||||
console.log('Newer source exists; leaving version and channel unchanged');
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
ensureRelease(
|
ensureRelease(
|
||||||
ref,
|
tag,
|
||||||
ref === `upgrade-${branch}`
|
development
|
||||||
? `Upgrade channel ${branch}`
|
? 'Upgrade channel develop'
|
||||||
: `Platform upgrade v${version}`,
|
: `Platform upgrade v${version} (${buildId})`,
|
||||||
);
|
);
|
||||||
gh(
|
const assets =
|
||||||
'release',
|
staged?.assets ||
|
||||||
'upload',
|
platforms
|
||||||
ref,
|
.flatMap((target) => {
|
||||||
path.join(directory, 'latest.json'),
|
const archive = path.join(
|
||||||
'--repo',
|
directory,
|
||||||
repository,
|
`qinglong-${commit}-${target}.tar.gz`,
|
||||||
'--clobber',
|
);
|
||||||
);
|
return [archive, archive + '.json'];
|
||||||
gh('release', 'edit', ref, '--repo', repository, '--notes', notes);
|
})
|
||||||
|
.concat(
|
||||||
|
path.join(directory, 'install-bundle.cjs'),
|
||||||
|
path.join(directory, 'latest.json'),
|
||||||
|
);
|
||||||
|
const published = lookup(tag);
|
||||||
|
const missing = [];
|
||||||
|
const matches = (asset, file) => {
|
||||||
|
const bytes = fs.readFileSync(file);
|
||||||
|
return (
|
||||||
|
asset?.size === bytes.length &&
|
||||||
|
asset.digest === `sha256:${sha256(bytes)}`
|
||||||
|
);
|
||||||
|
};
|
||||||
|
for (const file of assets) {
|
||||||
|
const previous = published.assets.find(
|
||||||
|
(a) => a.name === path.basename(file),
|
||||||
|
);
|
||||||
|
if (!previous) missing.push(file);
|
||||||
|
else if (!matches(previous, file))
|
||||||
|
throw new Error(
|
||||||
|
`Immutable release asset differs: ${path.basename(file)}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (missing.length)
|
||||||
|
gh('release', 'upload', tag, ...missing, '--repo', repository);
|
||||||
|
const uploaded = lookup(tag);
|
||||||
if (
|
if (
|
||||||
!matches(
|
!assets.every((file) =>
|
||||||
lookup(ref)?.assets.find((a) => a.name === 'latest.json'),
|
matches(
|
||||||
path.join(directory, 'latest.json'),
|
uploaded.assets.find((a) => a.name === path.basename(file)),
|
||||||
|
file,
|
||||||
|
),
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
throw new Error('Published channel index verification failed');
|
throw new Error('Published platform bundle verification failed');
|
||||||
}
|
|
||||||
// Only prune development releases after the complete new batch and its
|
// Each channel serializes its image builds and pointer writes as one release.
|
||||||
// channel pointer are verified. Production versions and aliases are retained.
|
// A rerun of an earlier workflow is allowed to replace the same
|
||||||
if (development && currentSource()) {
|
// version/commit; an obsolete source commit must never advance either alias.
|
||||||
const pages = JSON.parse(
|
const aliases = development
|
||||||
gh(
|
? ['upgrade-develop']
|
||||||
'api',
|
: [`upgrade-v${version}`, 'upgrade-master'];
|
||||||
`repos/${repository}/releases?per_page=100`,
|
for (const ref of aliases) {
|
||||||
'--paginate',
|
if (!currentSource()) {
|
||||||
'--slurp',
|
console.log(
|
||||||
),
|
'Newer source exists; leaving version and channel unchanged',
|
||||||
);
|
|
||||||
for (const release of pages.flat()) {
|
|
||||||
if (
|
|
||||||
release.tag_name !== tag &&
|
|
||||||
/^upgrade-build-develop-v\d+\.\d+\.\d+(?:-[\w.-]+)?(?:\+[\w.-]+)?-\d+-\d+$/.test(
|
|
||||||
release.tag_name,
|
|
||||||
) &&
|
|
||||||
release.prerelease === true &&
|
|
||||||
release.draft === false
|
|
||||||
) {
|
|
||||||
if (!currentSource()) break;
|
|
||||||
gh(
|
|
||||||
'release',
|
|
||||||
'delete',
|
|
||||||
release.tag_name,
|
|
||||||
'--repo',
|
|
||||||
repository,
|
|
||||||
'--yes',
|
|
||||||
);
|
);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
ensureRelease(
|
||||||
|
ref,
|
||||||
|
ref === `upgrade-${branch}`
|
||||||
|
? `Upgrade channel ${branch}`
|
||||||
|
: `Platform upgrade v${version}`,
|
||||||
|
);
|
||||||
|
gh(
|
||||||
|
'release',
|
||||||
|
'upload',
|
||||||
|
ref,
|
||||||
|
path.join(directory, 'latest.json'),
|
||||||
|
'--repo',
|
||||||
|
repository,
|
||||||
|
'--clobber',
|
||||||
|
);
|
||||||
|
gh('release', 'edit', ref, '--repo', repository, '--notes', notes);
|
||||||
|
if (
|
||||||
|
!matches(
|
||||||
|
lookup(ref)?.assets.find((a) => a.name === 'latest.json'),
|
||||||
|
path.join(directory, 'latest.json'),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
throw new Error('Published channel index verification failed');
|
||||||
|
}
|
||||||
|
// Only prune development releases after the complete new batch and its
|
||||||
|
// channel pointer are verified. Production versions and aliases are retained.
|
||||||
|
if (development && currentSource()) {
|
||||||
|
const currentAssets = new Set(assets.map((file) => path.basename(file)));
|
||||||
|
for (const asset of lookup(tag).assets) {
|
||||||
|
if (
|
||||||
|
!currentAssets.has(asset.name) &&
|
||||||
|
(/^qinglong-[a-f0-9]{40}-linux-[\w-]+-\d+-\d+\.tar\.gz(?:\.json)?$/.test(
|
||||||
|
asset.name,
|
||||||
|
) ||
|
||||||
|
/^install-bundle-\d+-\d+\.cjs$/.test(asset.name))
|
||||||
|
) {
|
||||||
|
if (!currentSource()) break;
|
||||||
|
gh(
|
||||||
|
'api',
|
||||||
|
'-X',
|
||||||
|
'DELETE',
|
||||||
|
`repos/${repository}/releases/assets/${asset.id}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const pages = JSON.parse(
|
||||||
|
gh(
|
||||||
|
'api',
|
||||||
|
`repos/${repository}/releases?per_page=100`,
|
||||||
|
'--paginate',
|
||||||
|
'--slurp',
|
||||||
|
),
|
||||||
|
);
|
||||||
|
for (const release of pages.flat()) {
|
||||||
|
if (
|
||||||
|
release.tag_name !== tag &&
|
||||||
|
/^upgrade-build-develop-(?:v\d+\.\d+\.\d+(?:-[\w.-]+)?(?:\+[\w.-]+)?-)?\d+-\d+$/.test(
|
||||||
|
release.tag_name,
|
||||||
|
) &&
|
||||||
|
release.prerelease === true &&
|
||||||
|
release.draft === false
|
||||||
|
) {
|
||||||
|
if (!currentSource()) break;
|
||||||
|
gh(
|
||||||
|
'release',
|
||||||
|
'delete',
|
||||||
|
release.tag_name,
|
||||||
|
'--repo',
|
||||||
|
repository,
|
||||||
|
'--yes',
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
return true;
|
||||||
|
} finally {
|
||||||
|
if (staged) fs.rmSync(staged.staged, { recursive: true, force: true });
|
||||||
}
|
}
|
||||||
return true;
|
|
||||||
}
|
}
|
||||||
module.exports = { releaseIndex, platforms, publishBundles };
|
module.exports = { releaseIndex, platforms, publishBundles };
|
||||||
if (require.main === module) {
|
if (require.main === module) {
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ const {
|
|||||||
sourceCommit,
|
sourceCommit,
|
||||||
} = require('../helpers/upgrade-fixture.cjs');
|
} = require('../helpers/upgrade-fixture.cjs');
|
||||||
|
|
||||||
async function fixture(t) {
|
async function fixture(t, targetVersion = '2.24.0') {
|
||||||
const base = await fs.mkdtemp(path.join(os.tmpdir(), 'ql-panel-job-'));
|
const base = await fs.mkdtemp(path.join(os.tmpdir(), 'ql-panel-job-'));
|
||||||
const root = path.join(base, 'panel'),
|
const root = path.join(base, 'panel'),
|
||||||
tmp = path.join(root, '.tmp');
|
tmp = path.join(root, '.tmp');
|
||||||
@@ -30,7 +30,10 @@ async function fixture(t) {
|
|||||||
await fs.writeFile(name, contents);
|
await fs.writeFile(name, contents);
|
||||||
};
|
};
|
||||||
for (const dir of [root, source]) {
|
for (const dir of [root, source]) {
|
||||||
await write(path.join(dir, 'package.json'), '{"version":"2.24.0"}');
|
await write(
|
||||||
|
path.join(dir, 'package.json'),
|
||||||
|
JSON.stringify({ version: targetVersion }),
|
||||||
|
);
|
||||||
await write(path.join(dir, 'pnpm-lock.yaml'), 'lock');
|
await write(path.join(dir, 'pnpm-lock.yaml'), 'lock');
|
||||||
await write(path.join(dir, 'sample/config.sample.sh'), '# config');
|
await write(path.join(dir, 'sample/config.sample.sh'), '# config');
|
||||||
}
|
}
|
||||||
@@ -39,7 +42,7 @@ async function fixture(t) {
|
|||||||
'installed dependency',
|
'installed dependency',
|
||||||
);
|
);
|
||||||
await write(path.join(root, 'version.yaml'), 'version: 2.23.0');
|
await write(path.join(root, 'version.yaml'), 'version: 2.23.0');
|
||||||
await write(path.join(source, 'version.yaml'), 'version: 2.24.0');
|
await write(path.join(source, 'version.yaml'), `version: ${targetVersion}`);
|
||||||
await write(path.join(staticRoot, 'build/app.js'), 'new backend');
|
await write(path.join(staticRoot, 'build/app.js'), 'new backend');
|
||||||
writeManifest(source, staticRoot);
|
writeManifest(source, staticRoot);
|
||||||
for (const repo of ['qinglong', 'qinglong-static'])
|
for (const repo of ['qinglong', 'qinglong-static'])
|
||||||
@@ -81,7 +84,7 @@ fs.writeFileSync(args[args.indexOf('--output') + 1], buffer);
|
|||||||
);
|
);
|
||||||
await fs.chmod(path.join(base, 'bin/curl'), 0o700);
|
await fs.chmod(path.join(base, 'bin/curl'), 0o700);
|
||||||
const runtime = {
|
const runtime = {
|
||||||
version: '2.24.0',
|
version: targetVersion,
|
||||||
sourceCommit,
|
sourceCommit,
|
||||||
instanceId: 'after-restart',
|
instanceId: 'after-restart',
|
||||||
parentPid: 100002,
|
parentPid: 100002,
|
||||||
@@ -186,6 +189,21 @@ test('detached download survives its initiating request and is idempotent; apply
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('prerelease version is retained through download and restart verification', async (t) => {
|
||||||
|
const f = await fixture(t, '2.24.0-dev.0');
|
||||||
|
await startJob(f, 'download');
|
||||||
|
const ready = await finished(f.tmp);
|
||||||
|
assert.equal(ready.status, 'ready', ready.log);
|
||||||
|
assert.equal(ready.version, '2.24.0-dev.0');
|
||||||
|
await startJob(f, 'apply');
|
||||||
|
const complete = await finished(f.tmp);
|
||||||
|
assert.equal(complete.status, 'completed', complete.log);
|
||||||
|
assert.equal(
|
||||||
|
await fs.readFile(path.join(f.root, 'version.yaml'), 'utf8'),
|
||||||
|
'version: 2.24.0-dev.0',
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
test('invalid staged payload fails asynchronously before stopping the panel', async (t) => {
|
test('invalid staged payload fails asynchronously before stopping the panel', async (t) => {
|
||||||
const f = await fixture(t);
|
const f = await fixture(t);
|
||||||
await startJob(f, 'download');
|
await startJob(f, 'download');
|
||||||
|
|||||||
@@ -66,6 +66,17 @@ function fixture(t) {
|
|||||||
const gh = (...args) => {
|
const gh = (...args) => {
|
||||||
state.calls.push(args);
|
state.calls.push(args);
|
||||||
if (args[0] === 'api') {
|
if (args[0] === 'api') {
|
||||||
|
if (args[1] === '-X' && args[2] === 'DELETE') {
|
||||||
|
if (state.failDelete) throw new Error('Delete unavailable');
|
||||||
|
const id = Number(args[3].split('/').at(-1));
|
||||||
|
for (const release of state.releases.values())
|
||||||
|
for (const asset of release.values())
|
||||||
|
if (asset.id === id) {
|
||||||
|
release.delete(asset.name);
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
return notFound();
|
||||||
|
}
|
||||||
const route = args[1];
|
const route = args[1];
|
||||||
if (route.endsWith('/git/ref/heads/master')) return state.head;
|
if (route.endsWith('/git/ref/heads/master')) return state.head;
|
||||||
if (route.endsWith('/git/ref/heads/develop')) return state.developHead;
|
if (route.endsWith('/git/ref/heads/develop')) return state.developHead;
|
||||||
@@ -337,20 +348,23 @@ test('existing master clients can migrate from indexes without version/build met
|
|||||||
assert.equal(f.index().buildId, '100-2');
|
assert.equal(f.index().buildId, '100-2');
|
||||||
});
|
});
|
||||||
|
|
||||||
test('automatic development publication shares musl/glibc packages without changing version or latest production assets', (t) => {
|
test('automatic development publication keeps its prerelease version separate from production assets', (t) => {
|
||||||
const f = fixture(t);
|
const f = fixture(t);
|
||||||
f.publish();
|
f.publish();
|
||||||
const production = f.index();
|
const production = f.index();
|
||||||
f.write(f.state.developHead, 'development');
|
const developmentVersion = '2.24.0-dev.0';
|
||||||
|
f.write(f.state.developHead, 'development', developmentVersion);
|
||||||
assert.equal(
|
assert.equal(
|
||||||
f.publish({
|
f.publish({
|
||||||
branch: 'develop',
|
branch: 'develop',
|
||||||
commit: f.state.developHead,
|
commit: f.state.developHead,
|
||||||
|
version: developmentVersion,
|
||||||
}),
|
}),
|
||||||
true,
|
true,
|
||||||
);
|
);
|
||||||
const development = f.index('upgrade-develop');
|
const development = f.index('upgrade-develop');
|
||||||
assert.equal(development.channel, 'develop');
|
assert.equal(development.channel, 'develop');
|
||||||
|
assert.equal(development.version, developmentVersion);
|
||||||
assert.equal(development.sourceCommit, f.state.developHead);
|
assert.equal(development.sourceCommit, f.state.developHead);
|
||||||
assert.equal(Object.keys(development.packages).length, 11);
|
assert.equal(Object.keys(development.packages).length, 11);
|
||||||
assert.ok(development.packages['linux-x64-musl']);
|
assert.ok(development.packages['linux-x64-musl']);
|
||||||
@@ -363,14 +377,29 @@ test('automatic development publication shares musl/glibc packages without chang
|
|||||||
...Object.values(development.packages),
|
...Object.values(development.packages),
|
||||||
development.installer,
|
development.installer,
|
||||||
])
|
])
|
||||||
assert.ok(entry.url.includes('/upgrade-build-develop-v2.24.0-100-1/'));
|
assert.ok(entry.url.includes('/upgrade-develop/'));
|
||||||
|
for (const entry of Object.values(development.packages))
|
||||||
|
assert.ok(entry.url.endsWith('-100-1.tar.gz'));
|
||||||
|
assert.ok(development.installer.url.endsWith('/install-bundle-100-1.cjs'));
|
||||||
|
assert.ok(
|
||||||
|
f.state.calls.some(
|
||||||
|
(args) =>
|
||||||
|
args[0] === 'release' &&
|
||||||
|
args[1] === 'create' &&
|
||||||
|
args[2] === 'upgrade-develop' &&
|
||||||
|
args[args.indexOf('--title') + 1] === 'Upgrade channel develop',
|
||||||
|
),
|
||||||
|
);
|
||||||
assert.deepEqual(f.index(), production);
|
assert.deepEqual(f.index(), production);
|
||||||
assert.deepEqual(f.index('upgrade-v2.24.0'), production);
|
assert.deepEqual(f.index('upgrade-v2.24.0'), production);
|
||||||
assert.equal(f.state.releases.get('upgrade-build-v2.24.0-100-1').size, 24);
|
assert.equal(f.state.releases.get('upgrade-build-v2.24.0-100-1').size, 24);
|
||||||
assert.equal(
|
assert.equal(f.state.releases.get('upgrade-develop').size, 24);
|
||||||
f.state.releases.get('upgrade-build-develop-v2.24.0-100-1').size,
|
assert.ok(!f.state.releases.has('upgrade-build-develop-100-1'));
|
||||||
24,
|
const archiveName = path.basename(development.packages['linux-x64-musl'].url);
|
||||||
|
const sidecar = JSON.parse(
|
||||||
|
f.state.releases.get('upgrade-develop').get(`${archiveName}.json`).bytes,
|
||||||
);
|
);
|
||||||
|
assert.equal(sidecar.filename, archiveName);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('development builds cannot publish obsolete development source', (t) => {
|
test('development builds cannot publish obsolete development source', (t) => {
|
||||||
@@ -420,14 +449,21 @@ for (const fault of [
|
|||||||
const options = { branch: 'develop', commit: f.state.developHead };
|
const options = { branch: 'develop', commit: f.state.developHead };
|
||||||
f.write(options.commit, 'development');
|
f.write(options.commit, 'development');
|
||||||
f.publish(options);
|
f.publish(options);
|
||||||
const oldTag = 'upgrade-build-develop-v2.24.0-100-1';
|
const oldTag = 'upgrade-build-develop-100-1';
|
||||||
|
const legacyTag = 'upgrade-build-develop-v2.24.0-99-1';
|
||||||
|
f.state.releases.set(oldTag, new Map());
|
||||||
|
f.state.releases.set(legacyTag, new Map());
|
||||||
|
const oldAsset = Array.from(
|
||||||
|
f.state.releases.get('upgrade-develop').keys(),
|
||||||
|
).find((name) => name.endsWith('-100-1.tar.gz'));
|
||||||
|
assert.ok(oldAsset);
|
||||||
// Never prune production, channel aliases, legacy/unclassified releases,
|
// Never prune production, channel aliases, legacy/unclassified releases,
|
||||||
// drafts or a non-prerelease even when its name resembles a dev build.
|
// drafts or a non-prerelease even when its name resembles a dev build.
|
||||||
const protectedTags = [
|
const protectedTags = [
|
||||||
'upgrade-86c37f426a5ba9945d6af1910a7885a26f77e4bc',
|
'upgrade-86c37f426a5ba9945d6af1910a7885a26f77e4bc',
|
||||||
'v2.24.0',
|
'v2.24.0',
|
||||||
'upgrade-build-develop-v2.24.0-10-1',
|
'upgrade-build-develop-10-1',
|
||||||
'upgrade-build-develop-v2.24.0-11-1',
|
'upgrade-build-develop-11-1',
|
||||||
];
|
];
|
||||||
for (const tag of protectedTags) f.state.releases.set(tag, new Map());
|
for (const tag of protectedTags) f.state.releases.set(tag, new Map());
|
||||||
f.state.releaseMetadata = {
|
f.state.releaseMetadata = {
|
||||||
@@ -445,7 +481,7 @@ for (const fault of [
|
|||||||
assert.equal(f.publish({ ...options, buildId: '101-1' }), true);
|
assert.equal(f.publish({ ...options, buildId: '101-1' }), true);
|
||||||
assert.equal(f.index('upgrade-develop').buildId, '101-1');
|
assert.equal(f.index('upgrade-develop').buildId, '101-1');
|
||||||
const deletion = f.state.calls.findIndex(
|
const deletion = f.state.calls.findIndex(
|
||||||
(a) => a[0] === 'release' && a[1] === 'delete',
|
(a) => a[0] === 'api' && a[1] === '-X' && a[2] === 'DELETE',
|
||||||
);
|
);
|
||||||
const aliasEdit = f.state.calls.findLastIndex(
|
const aliasEdit = f.state.calls.findLastIndex(
|
||||||
(a) => a[0] === 'release' && a[1] === 'edit',
|
(a) => a[0] === 'release' && a[1] === 'edit',
|
||||||
@@ -459,9 +495,15 @@ for (const fault of [
|
|||||||
a[0] === 'api' && a[1].includes('/releases/tags/upgrade-develop'),
|
a[0] === 'api' && a[1].includes('/releases/tags/upgrade-develop'),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
assert.ok(f.state.releases.has('upgrade-build-develop-v2.24.0-101-1'));
|
assert.ok(f.state.releases.has('upgrade-develop'));
|
||||||
|
assert.ok(!f.state.releases.has('upgrade-build-develop-101-1'));
|
||||||
}
|
}
|
||||||
assert.equal(f.state.releases.has(oldTag), Boolean(fault));
|
assert.equal(f.state.releases.has(oldTag), Boolean(fault));
|
||||||
|
assert.equal(f.state.releases.has(legacyTag), Boolean(fault));
|
||||||
|
assert.equal(
|
||||||
|
f.state.releases.get('upgrade-develop').has(oldAsset),
|
||||||
|
Boolean(fault),
|
||||||
|
);
|
||||||
for (const tag of protectedTags) assert.ok(f.state.releases.has(tag));
|
for (const tag of protectedTags) assert.ok(f.state.releases.has(tag));
|
||||||
assert.deepEqual(f.index(), production);
|
assert.deepEqual(f.index(), production);
|
||||||
assert.deepEqual(f.index('upgrade-v2.24.0'), production);
|
assert.deepEqual(f.index('upgrade-v2.24.0'), production);
|
||||||
@@ -472,17 +514,21 @@ test('failed development cleanup is retryable without overwriting immutable asse
|
|||||||
const options = { branch: 'develop', commit: f.state.developHead };
|
const options = { branch: 'develop', commit: f.state.developHead };
|
||||||
f.write(options.commit, 'development');
|
f.write(options.commit, 'development');
|
||||||
f.publish(options);
|
f.publish(options);
|
||||||
|
const oldAsset = Array.from(
|
||||||
|
f.state.releases.get('upgrade-develop').keys(),
|
||||||
|
).find((name) => name.endsWith('-100-1.tar.gz'));
|
||||||
|
assert.ok(oldAsset);
|
||||||
f.state.failDelete = true;
|
f.state.failDelete = true;
|
||||||
assert.throws(
|
assert.throws(
|
||||||
() => f.publish({ ...options, buildId: '101-1' }),
|
() => f.publish({ ...options, buildId: '101-1' }),
|
||||||
/Delete unavailable/,
|
/Delete unavailable/,
|
||||||
);
|
);
|
||||||
assert.equal(f.index('upgrade-develop').buildId, '101-1');
|
assert.equal(f.index('upgrade-develop').buildId, '101-1');
|
||||||
assert.ok(f.state.releases.has('upgrade-build-develop-v2.24.0-100-1'));
|
assert.ok(f.state.releases.get('upgrade-develop').has(oldAsset));
|
||||||
f.state.failDelete = false;
|
f.state.failDelete = false;
|
||||||
f.publish({ ...options, buildId: '101-1' });
|
f.publish({ ...options, buildId: '101-1' });
|
||||||
assert.ok(!f.state.releases.has('upgrade-build-develop-v2.24.0-100-1'));
|
assert.ok(!f.state.releases.get('upgrade-develop').has(oldAsset));
|
||||||
assert.ok(f.state.releases.has('upgrade-build-develop-v2.24.0-101-1'));
|
assert.equal(f.state.releases.get('upgrade-develop').size, 24);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('a newer development commit during upload preserves the published channel and its old package', (t) => {
|
test('a newer development commit during upload preserves the published channel and its old package', (t) => {
|
||||||
@@ -491,10 +537,14 @@ test('a newer development commit during upload preserves the published channel a
|
|||||||
f.write(options.commit, 'development');
|
f.write(options.commit, 'development');
|
||||||
f.publish(options);
|
f.publish(options);
|
||||||
const before = f.index('upgrade-develop');
|
const before = f.index('upgrade-develop');
|
||||||
|
const oldAsset = Array.from(
|
||||||
|
f.state.releases.get('upgrade-develop').keys(),
|
||||||
|
).find((name) => name.endsWith('-100-1.tar.gz'));
|
||||||
|
assert.ok(oldAsset);
|
||||||
f.state.changeDevelopHeadAfterUpload = true;
|
f.state.changeDevelopHeadAfterUpload = true;
|
||||||
assert.equal(f.publish({ ...options, buildId: '101-1' }), false);
|
assert.equal(f.publish({ ...options, buildId: '101-1' }), false);
|
||||||
assert.deepEqual(f.index('upgrade-develop'), before);
|
assert.deepEqual(f.index('upgrade-develop'), before);
|
||||||
assert.ok(f.state.releases.has('upgrade-build-develop-v2.24.0-100-1'));
|
assert.ok(f.state.releases.get('upgrade-develop').has(oldAsset));
|
||||||
assert.ok(
|
assert.ok(
|
||||||
!f.state.calls.some((a) => a[0] === 'release' && a[1] === 'delete'),
|
!f.state.calls.some((a) => a[0] === 'release' && a[1] === 'delete'),
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -32,7 +32,7 @@ test('a running process retains its own build identity when installed files chan
|
|||||||
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'ql-runtime-build-'));
|
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'ql-runtime-build-'));
|
||||||
t.after(() => fs.rmSync(root, { recursive: true, force: true }));
|
t.after(() => fs.rmSync(root, { recursive: true, force: true }));
|
||||||
fs.mkdirSync(path.join(root, 'static/build'), { recursive: true });
|
fs.mkdirSync(path.join(root, 'static/build'), { recursive: true });
|
||||||
fs.writeFileSync(path.join(root, 'version.yaml'), 'version: 2.24.0');
|
fs.writeFileSync(path.join(root, 'version.yaml'), 'version: 2.24.0-dev.0');
|
||||||
fs.writeFileSync(path.join(root, 'static/build/app.js'), 'old backend');
|
fs.writeFileSync(path.join(root, 'static/build/app.js'), 'old backend');
|
||||||
fs.writeFileSync(
|
fs.writeFileSync(
|
||||||
path.join(root, 'static/build-info.json'),
|
path.join(root, 'static/build-info.json'),
|
||||||
@@ -45,7 +45,7 @@ test('a running process retains its own build identity when installed files chan
|
|||||||
JSON.stringify({ sourceCommit: 'b'.repeat(40) }),
|
JSON.stringify({ sourceCommit: 'b'.repeat(40) }),
|
||||||
);
|
);
|
||||||
fs.writeFileSync(path.join(root, 'version.yaml'), 'version: 2.25.0');
|
fs.writeFileSync(path.join(root, 'version.yaml'), 'version: 2.25.0');
|
||||||
assert.equal(running.version, '2.24.0');
|
assert.equal(running.version, '2.24.0-dev.0');
|
||||||
assert.equal(running.sourceCommit, 'a'.repeat(40));
|
assert.equal(running.sourceCommit, 'a'.repeat(40));
|
||||||
assert.equal(
|
assert.equal(
|
||||||
running.backendSha256,
|
running.backendSha256,
|
||||||
|
|||||||
@@ -10,8 +10,12 @@ const {
|
|||||||
repairDependencies,
|
repairDependencies,
|
||||||
} = require('../../cli/src/internal/maintenance/upgradePrestart.cjs');
|
} = require('../../cli/src/internal/maintenance/upgradePrestart.cjs');
|
||||||
|
|
||||||
for (const exitCode of [0, 1])
|
for (const [version, exitCode] of [
|
||||||
test(`prestart exit ${exitCode} reaps children retaining output pipes before removing its workspace`, async (t) => {
|
['2.23.0', 0],
|
||||||
|
['2.23.0', 1],
|
||||||
|
['2.24.0-dev.0', 0],
|
||||||
|
])
|
||||||
|
test(`prestart ${version} exit ${exitCode} reaps children retaining output pipes before removing its workspace`, async (t) => {
|
||||||
const root = await fs.mkdtemp(path.join(os.tmpdir(), 'ql-probe-exit-'));
|
const root = await fs.mkdtemp(path.join(os.tmpdir(), 'ql-probe-exit-'));
|
||||||
t.after(() => fs.rm(root, { recursive: true, force: true }));
|
t.after(() => fs.rm(root, { recursive: true, force: true }));
|
||||||
const source = path.join(root, 'source'),
|
const source = path.join(root, 'source'),
|
||||||
@@ -19,7 +23,7 @@ for (const exitCode of [0, 1])
|
|||||||
survivor = path.join(root, 'survivor');
|
survivor = path.join(root, 'survivor');
|
||||||
await fs.mkdir(path.join(source, 'node_modules'), { recursive: true });
|
await fs.mkdir(path.join(source, 'node_modules'), { recursive: true });
|
||||||
await fs.mkdir(path.join(statics, 'build/bootstrap'), { recursive: true });
|
await fs.mkdir(path.join(statics, 'build/bootstrap'), { recursive: true });
|
||||||
await fs.writeFile(path.join(source, 'version.yaml'), 'version: 2.23.0');
|
await fs.writeFile(path.join(source, 'version.yaml'), `version: ${version}`);
|
||||||
await fs.writeFile(path.join(statics, 'build/app.js'), 'backend');
|
await fs.writeFile(path.join(statics, 'build/app.js'), 'backend');
|
||||||
await fs.writeFile(
|
await fs.writeFile(
|
||||||
path.join(statics, 'build-info.json'),
|
path.join(statics, 'build-info.json'),
|
||||||
@@ -36,7 +40,7 @@ for (const exitCode of [0, 1])
|
|||||||
)}],{stdio:'inherit'});
|
)}],{stdio:'inherit'});
|
||||||
process.send({type:'upgrade-probe',ok:${
|
process.send({type:'upgrade-probe',ok:${
|
||||||
exitCode === 0
|
exitCode === 0
|
||||||
},message:'simulated prestart failure',build:{sourceCommit:'a'.repeat(40),version:'2.23.0',backendSha256:crypto.createHash('sha256').update('backend').digest('hex')}},()=>process.exit(${exitCode}));
|
},message:'simulated prestart failure',build:{sourceCommit:'a'.repeat(40),version:${JSON.stringify(version)},backendSha256:crypto.createHash('sha256').update('backend').digest('hex')}},()=>process.exit(${exitCode}));
|
||||||
`,
|
`,
|
||||||
);
|
);
|
||||||
const operation = prestartBackend(
|
const operation = prestartBackend(
|
||||||
|
|||||||
+8
-14
@@ -1,15 +1,9 @@
|
|||||||
version: 2.23.0
|
version: 2.24.0-dev.0
|
||||||
changeLogLink: https://github.com/whyour/qinglong/blob/master/docs/releases/2.23.0.md
|
changeLogLink: https://github.com/whyour/qinglong/blob/develop/docs/releases/2.24.0-dev.0.md
|
||||||
publishTime: "2026-10-11 12:27:02"
|
publishTime: "2026-10-25 19:22:54"
|
||||||
changeLog: |
|
changeLog: |
|
||||||
1. 新增脚本历史版本查看、对比与恢复,支持 CLI 和 OpenAPI;恢复前检查当前内容哈希,避免覆盖并发修改
|
1. 开发候选 2.24.0-dev.0,尚未正式发布;以 2.23.0 为上一正式版本
|
||||||
2. 普通任务与订阅调度迁移到 node-cron,保留旧规则兼容及日期/星期 OR 语义,补充宏和夏令时说明
|
2. Alpine 与 Debian 镜像统一使用 Node.js 24,升级 sqlite3 至 1.1.4
|
||||||
3. 修复已失效排队任务仍被执行的问题,完善任务进程超时、停止和 Shell 环境传递
|
3. Alpine 保留 arm/v6 和 arm/v7;Debian 暂不提供 arm/v7 镜像
|
||||||
4. 修复系统令牌主动续期与认证缓存并发更新;Shell API 遇到 401 时刷新令牌并重试一次
|
4. ARM64 升级包在原生 runner 构建,避免 x64 QEMU 下的归档异常
|
||||||
5. 降低任务启动开销,JS/Python 通知依赖改为首次调用时加载
|
5. 升级前备份完整数据,并在目标设备验证脚本、依赖和通知
|
||||||
6. 加固环境变量、订阅参数、文件访问和 SSH 配置输入,改善资源冲突与 CLI 错误反馈
|
|
||||||
7. 修复页面升级、ql update 与静态资源版本同步,完善离线备份和升级产物校验
|
|
||||||
8. 修复 Debian 依赖安装、通知与调度回归,补齐前端中英文翻译
|
|
||||||
9. 最小修补 HTTP 压缩、上传、gRPC、代理 IP、WebSocket 和 JSON 查询相关依赖漏洞
|
|
||||||
10. 远程 CLI 更新至 0.1.2,新增脚本历史命令,最低 Node.js 版本仍为 22.12
|
|
||||||
11. 本次为 Python 3.10 兼容镜像的最后一个版本,发布后停止维护;升级前备份完整数据并验证脚本、依赖和通知
|
|
||||||
|
|||||||
Reference in New Issue
Block a user