mirror of
https://github.com/whyour/qinglong.git
synced 2026-11-04 09:28:35 +08:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b95d6583ca | ||
|
|
27a9c35809 | ||
|
|
af93fb6cf0 | ||
|
|
2218c2c175 | ||
|
|
32c20ba556 |
@@ -20,29 +20,29 @@ gh workflow run build-docker-image.yml --ref develop
|
||||
|
||||
开发和正式包都必须在 11 个平台完成归档、解包后复验及依赖试运行,并且全部上传资产的大小和 SHA256 复验通过后,才更新各自索引。平台范围与现有 Alpine/Debian 镜像一致:ARMv6 和 ARMv7 仅提供 musl 包,Debian 不再发布 ARMv7 镜像或 glibc 平台包。
|
||||
|
||||
每个正式版本只使用一个 GitHub prerelease `upgrade-v<version>`,其中存放完整平台包、校验文件、安装器和 `latest.json`;在线正式渠道 `upgrade-master` 只保留指向最新版本的 `latest.json`,兼容现有客户端。不再创建 `upgrade-build-v<version>-<run-id>-<run-attempt>` 构建批次 release。索引记录渠道、版本、源码提交、构建批次、准确字节数、SHA256,以及该版本 release 中带构建 ID 的固定资产 URL。相同版本的镜像 tag 重新发布时,先上传并校验新资产,再更新版本索引和正式渠道索引;同一提交重跑也允许产生新内容。旧批次资产保留在同一版本 release,已选中旧索引的下载可继续完成,避免覆盖文件导致哈希不匹配。master 的镜像构建和完整包发布整条流水线串行执行,避免镜像 tag 与完整包被并发重发交错覆盖;旧工作流明确重跑时也允许同版本、同提交重新发布。过期源码提交不能推进对应渠道或版本索引;平台包上传失败时不切换索引。
|
||||
版本入口为 GitHub prerelease `upgrade-v<version>` 下的 `latest.json`;在线正式渠道保留 `upgrade-master/latest.json`,兼容现有客户端。二者指向同一套完整包,指定版本与 latest 不重复生成产物。索引记录渠道、版本、源码提交、构建批次、准确字节数、SHA256,以及 `upgrade-build-v<version>-<run-id>-<run-attempt>` 下的固定下载 URL。相同版本的镜像 tag 重新发布时,完整包也重新生成,版本和正式渠道索引同步覆盖到新批次;同一提交重跑也允许产生新内容。已选中旧索引的下载继续使用旧批次资产,避免覆盖文件导致哈希不匹配。master 的镜像构建和完整包发布整条流水线串行执行,避免镜像 tag 与完整包被并发重发交错覆盖;旧工作流明确重跑时也允许同版本、同提交重新发布。过期源码提交不能推进对应渠道或版本索引;平台包上传失败时不切换索引。
|
||||
|
||||
## 手动离线安装和升级
|
||||
|
||||
从 `upgrade-v<version>` 的说明或 `latest.json` 中取得当前批次的下载链接,下载匹配设备的 `.tar.gz` 和带构建 ID 的独立安装器;两者都来自该版本 release。独立安装器已内置解析依赖,在目标设备正常情况无需 npm、pnpm 或重新安装 SQLite。离线准备和预启动默认不联网;依赖不符合要求时直接报错。可对照同一 release 的 `latest.json` 核对归档 SHA256。
|
||||
从 `upgrade-v<version>` 的说明或 `latest.json` 中取得当前批次的下载链接,下载匹配设备的 `.tar.gz` 和独立 `install-bundle.cjs`;两者都来自同一个 `upgrade-build-v<version>-<run-id>-<run-attempt>` release。独立安装器已内置解析依赖,在目标设备正常情况无需 npm、pnpm 或重新安装 SQLite。离线准备和预启动默认不联网;依赖不符合要求时直接报错。可对照同批次 release 的 `latest.json` 核对归档 SHA256。
|
||||
|
||||
已有安装(默认应用并重启):
|
||||
|
||||
```sh
|
||||
node install-bundle-<build-id>.cjs --root /ql --file /absolute/path/qinglong-<commit>-linux-arm64-musl-<build-id>.tar.gz
|
||||
node install-bundle.cjs --root /ql --file /absolute/path/qinglong-<commit>-linux-arm64-musl.tar.gz
|
||||
```
|
||||
|
||||
只准备,稍后手动重启:
|
||||
|
||||
```sh
|
||||
node install-bundle-<build-id>.cjs --root /ql --file /absolute/path/package.tar.gz --download-only
|
||||
node install-bundle.cjs --root /ql --file /absolute/path/package.tar.gz --download-only
|
||||
ql reload system
|
||||
```
|
||||
|
||||
仅在明确允许联网修复时添加 `--repair-deps`(安装和升级均支持),例如:
|
||||
|
||||
```sh
|
||||
node install-bundle-<build-id>.cjs --root /ql --file /absolute/path/package.tar.gz --download-only --repair-deps
|
||||
node install-bundle.cjs --root /ql --file /absolute/path/package.tar.gz --download-only --repair-deps
|
||||
```
|
||||
|
||||
修复需要已有 pnpm 8.3.1 及必要编译工具。依赖在私有目录按 `--prod --frozen-lockfile` 重装,仅重建 SQLite;ARMv6 强制源码编译和 ARMv6 硬浮点指令,禁止下载 generic ARM 的 ARMv7 二进制。修复仅执行一次;不安装或替换 Node、不改锁文件、不改已校验源码和静态产物。应用前以原包校验清单和独立记录的修复依赖摘要复验。
|
||||
@@ -58,7 +58,7 @@ node install-bundle-<build-id>.cjs --root /ql --file /absolute/path/package.tar.
|
||||
安装到空目录:
|
||||
|
||||
```sh
|
||||
node install-bundle-<build-id>.cjs --root /opt/qinglong --file /absolute/path/package.tar.gz --install
|
||||
node install-bundle.cjs --root /opt/qinglong --file /absolute/path/package.tar.gz --install
|
||||
QL_DIR=/opt/qinglong QL_DATA_DIR=/opt/qinglong/data bash /opt/qinglong/shell/start-simplify.sh
|
||||
```
|
||||
|
||||
|
||||
@@ -57,8 +57,8 @@ function releaseIndex(directory, commit, repository, options = {}) {
|
||||
}
|
||||
return { format: 2, sourceCommit: commit, packages };
|
||||
}
|
||||
function stageReleaseAssets(directory, commit, buildId, channel) {
|
||||
const staged = fs.mkdtempSync(path.join(directory, `.upgrade-${channel}-`));
|
||||
function stageDevelopmentAssets(directory, commit, buildId) {
|
||||
const staged = fs.mkdtempSync(path.join(directory, '.upgrade-develop-'));
|
||||
const assets = [];
|
||||
try {
|
||||
for (const target of platforms) {
|
||||
@@ -87,9 +87,6 @@ function stageReleaseAssets(directory, commit, buildId, channel) {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
function stageDevelopmentAssets(directory, commit, buildId) {
|
||||
return stageReleaseAssets(directory, commit, buildId, 'develop');
|
||||
}
|
||||
function publishBundles({
|
||||
directory,
|
||||
commit,
|
||||
@@ -113,12 +110,14 @@ function publishBundles({
|
||||
// unchanged. Only the version/channel pointer is overwritten, never bytes
|
||||
// that an in-flight download has already selected by hash.
|
||||
const development = branch === 'develop';
|
||||
const tag = development ? 'upgrade-develop' : `upgrade-v${version}`;
|
||||
const tag = development
|
||||
? 'upgrade-develop'
|
||||
: `upgrade-build-v${version}-${buildId}`;
|
||||
const index = {
|
||||
...releaseIndex(directory, commit, repository, {
|
||||
version,
|
||||
releaseTag: tag,
|
||||
assetBuildId: buildId,
|
||||
assetBuildId: development ? buildId : undefined,
|
||||
}),
|
||||
version,
|
||||
buildId,
|
||||
@@ -130,7 +129,7 @@ function publishBundles({
|
||||
sha256: sha256(installer),
|
||||
url: `https://github.com/${repository}/releases/download/${encodeURIComponent(
|
||||
tag,
|
||||
)}/install-bundle-${buildId}.cjs`,
|
||||
)}/${development ? `install-bundle-${buildId}.cjs` : 'install-bundle.cjs'}`,
|
||||
};
|
||||
fs.writeFileSync(
|
||||
path.join(directory, 'latest.json'),
|
||||
@@ -188,13 +187,28 @@ function publishBundles({
|
||||
}
|
||||
const staged = development
|
||||
? stageDevelopmentAssets(directory, commit, buildId)
|
||||
: stageReleaseAssets(directory, commit, buildId, `v${version}`);
|
||||
: null;
|
||||
try {
|
||||
ensureRelease(
|
||||
tag,
|
||||
development ? 'Upgrade channel develop' : `Platform upgrade v${version}`,
|
||||
development
|
||||
? 'Upgrade channel develop'
|
||||
: `Platform upgrade v${version} (${buildId})`,
|
||||
);
|
||||
const assets = staged.assets;
|
||||
const assets =
|
||||
staged?.assets ||
|
||||
platforms
|
||||
.flatMap((target) => {
|
||||
const archive = path.join(
|
||||
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) => {
|
||||
@@ -230,7 +244,9 @@ function publishBundles({
|
||||
// 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'] : [tag, 'upgrade-master'];
|
||||
const aliases = development
|
||||
? ['upgrade-develop']
|
||||
: [`upgrade-v${version}`, 'upgrade-master'];
|
||||
for (const ref of aliases) {
|
||||
if (!currentSource()) {
|
||||
console.log(
|
||||
@@ -314,7 +330,7 @@ function publishBundles({
|
||||
}
|
||||
return true;
|
||||
} finally {
|
||||
fs.rmSync(staged.staged, { recursive: true, force: true });
|
||||
if (staged) fs.rmSync(staged.staged, { recursive: true, force: true });
|
||||
}
|
||||
}
|
||||
module.exports = { releaseIndex, platforms, publishBundles };
|
||||
|
||||
@@ -138,11 +138,7 @@ function fixture(t) {
|
||||
digest: `sha256:${sha256(bytes)}`,
|
||||
});
|
||||
}
|
||||
if (
|
||||
files.some((file) => path.basename(file) === 'latest.json') &&
|
||||
((tag === 'upgrade-develop' && state.corruptChannel) ||
|
||||
tag === state.corruptIndexTag)
|
||||
)
|
||||
if (tag === 'upgrade-develop' && state.corruptChannel)
|
||||
release.get('latest.json').digest = 'sha256:wrong';
|
||||
if (files.length > 1) {
|
||||
if (state.corruptUploadedBatch)
|
||||
@@ -181,15 +177,12 @@ test('publishes every platform and installer before advancing version and legacy
|
||||
assert.ok(index.packages['linux-armv7-musl']);
|
||||
assert.equal(index.packages['linux-armv7-glibc'], undefined);
|
||||
assert.deepEqual(index, f.index('upgrade-v2.24.0'));
|
||||
const tag = 'upgrade-v2.24.0';
|
||||
const tag = 'upgrade-build-v2.24.0-100-1';
|
||||
assert.equal(f.state.releases.get(tag).size, 24);
|
||||
assert.equal(f.state.releases.get('upgrade-master').size, 1);
|
||||
assert.ok(!f.state.releases.has('upgrade-build-v2.24.0-100-1'));
|
||||
for (const entry of [...Object.values(index.packages), index.installer]) {
|
||||
const url = new URL(entry.url),
|
||||
filename = path.basename(url.pathname);
|
||||
assert.ok(url.pathname.includes(`/releases/download/${tag}/`));
|
||||
assert.ok(filename.includes('-100-1.'));
|
||||
const bytes = f.state.releases.get(tag).get(filename).bytes;
|
||||
assert.equal(bytes.length, entry.size);
|
||||
assert.equal(sha256(bytes), entry.sha256);
|
||||
@@ -199,7 +192,7 @@ test('publishes every platform and installer before advancing version and legacy
|
||||
);
|
||||
assert.deepEqual(
|
||||
writes.map((a) => a[2]),
|
||||
[tag, tag, 'upgrade-master'],
|
||||
[tag, 'upgrade-v2.24.0', 'upgrade-master'],
|
||||
);
|
||||
assert.ok(writes.slice(1).every((a) => a.includes('--clobber')));
|
||||
});
|
||||
@@ -212,10 +205,10 @@ for (const sameCommit of [false, true])
|
||||
f.publish();
|
||||
const before = f.index();
|
||||
const oldAssets = new Map(
|
||||
Array.from(f.state.releases.get('upgrade-v2.24.0'), ([name, a]) => [
|
||||
name,
|
||||
a.bytes.toString('base64'),
|
||||
]).filter(([name]) => name !== 'latest.json'),
|
||||
Array.from(
|
||||
f.state.releases.get('upgrade-build-v2.24.0-100-1'),
|
||||
([name, a]) => [name, a.bytes.toString('base64')],
|
||||
),
|
||||
);
|
||||
const commit = sameCommit ? f.options.commit : 'b'.repeat(40);
|
||||
f.state.head = commit;
|
||||
@@ -237,14 +230,13 @@ for (const sameCommit of [false, true])
|
||||
assert.notEqual(after.installer.sha256, before.installer.sha256);
|
||||
assert.deepEqual(
|
||||
new Map(
|
||||
Array.from(f.state.releases.get('upgrade-v2.24.0'), ([name, a]) => [
|
||||
name,
|
||||
a.bytes.toString('base64'),
|
||||
]).filter(([name]) => oldAssets.has(name)),
|
||||
Array.from(
|
||||
f.state.releases.get('upgrade-build-v2.24.0-100-1'),
|
||||
([name, a]) => [name, a.bytes.toString('base64')],
|
||||
),
|
||||
),
|
||||
oldAssets,
|
||||
);
|
||||
assert.equal(f.state.releases.get('upgrade-v2.24.0').size, 47);
|
||||
});
|
||||
|
||||
test('retrying a publication is idempotent, but changed bytes require a new build ID', (t) => {
|
||||
@@ -255,8 +247,7 @@ test('retrying a publication is idempotent, but changed bytes require a new buil
|
||||
(a) =>
|
||||
a[0] === 'release' &&
|
||||
a[1] === 'upload' &&
|
||||
a[2] === 'upgrade-v2.24.0' &&
|
||||
!a.includes('--clobber'),
|
||||
a[2] === 'upgrade-build-v2.24.0-100-1',
|
||||
).length;
|
||||
assert.equal(count(), 1);
|
||||
f.publish();
|
||||
@@ -266,41 +257,6 @@ test('retrying a publication is idempotent, but changed bytes require a new buil
|
||||
assert.equal(f.index().buildId, '100-1');
|
||||
});
|
||||
|
||||
test('a new version keeps the previous version release while master advances', (t) => {
|
||||
const f = fixture(t);
|
||||
f.publish();
|
||||
const previous = f.index('upgrade-v2.24.0');
|
||||
const commit = 'b'.repeat(40);
|
||||
f.state.head = commit;
|
||||
f.write(commit, 'next version', '2.25.0');
|
||||
assert.equal(
|
||||
f.publish({ commit, version: '2.25.0', buildId: '101-1' }),
|
||||
true,
|
||||
);
|
||||
assert.deepEqual(f.index('upgrade-v2.24.0'), previous);
|
||||
assert.equal(f.state.releases.get('upgrade-v2.24.0').size, 24);
|
||||
assert.equal(f.state.releases.get('upgrade-v2.25.0').size, 24);
|
||||
assert.equal(f.state.releases.get('upgrade-master').size, 1);
|
||||
assert.equal(f.index().version, '2.25.0');
|
||||
assert.ok(!f.state.releases.has('upgrade-build-v2.25.0-101-1'));
|
||||
});
|
||||
|
||||
test('a failed version index verification leaves master on the previous version', (t) => {
|
||||
const f = fixture(t);
|
||||
f.publish();
|
||||
const previous = f.index();
|
||||
const commit = 'b'.repeat(40);
|
||||
f.state.head = commit;
|
||||
f.write(commit, 'next version', '2.25.0');
|
||||
f.state.corruptIndexTag = 'upgrade-v2.25.0';
|
||||
assert.throws(
|
||||
() => f.publish({ commit, version: '2.25.0', buildId: '101-1' }),
|
||||
/Published channel index verification failed/,
|
||||
);
|
||||
assert.deepEqual(f.index(), previous);
|
||||
assert.deepEqual(f.index('upgrade-v2.24.0'), previous);
|
||||
});
|
||||
|
||||
for (const fault of ['missing', 'corrupt', 'wrong-version'])
|
||||
test(`incomplete, corrupt or wrong-version packages never publish (${fault})`, (t) => {
|
||||
const f = fixture(t);
|
||||
@@ -340,7 +296,7 @@ test('stale commits do not publish; a source change during upload cannot advance
|
||||
const before = f.index();
|
||||
f.state.head = 'b'.repeat(40);
|
||||
assert.equal(f.publish({ buildId: '101-1' }), false);
|
||||
assert.equal(f.state.releases.get('upgrade-v2.24.0').size, 24);
|
||||
assert.ok(!f.state.releases.has('upgrade-build-v2.24.0-101-1'));
|
||||
f.state.head = f.options.commit;
|
||||
f.state.changeHeadAfterUpload = true;
|
||||
assert.equal(f.publish({ buildId: '102-1' }), false);
|
||||
@@ -436,7 +392,7 @@ test('automatic development publication keeps its prerelease version separate fr
|
||||
);
|
||||
assert.deepEqual(f.index(), production);
|
||||
assert.deepEqual(f.index('upgrade-v2.24.0'), production);
|
||||
assert.equal(f.state.releases.get('upgrade-v2.24.0').size, 24);
|
||||
assert.equal(f.state.releases.get('upgrade-build-v2.24.0-100-1').size, 24);
|
||||
assert.equal(f.state.releases.get('upgrade-develop').size, 24);
|
||||
assert.ok(!f.state.releases.has('upgrade-build-develop-100-1'));
|
||||
const archiveName = path.basename(development.packages['linux-x64-musl'].url);
|
||||
|
||||
Reference in New Issue
Block a user