@xcanwin/manyoyo 8.6.10 → 8.6.11
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/manyoyo.js +8 -2
- package/lib/app-update.js +15 -5
- package/lib/code-update.js +54 -7
- package/package.json +1 -1
package/bin/manyoyo.js
CHANGED
|
@@ -871,14 +871,20 @@ async function updateOfflineInstall(mode, options, globalConfig) {
|
|
|
871
871
|
// MANYOYO_TEST_CODE_BASE 只给测试用(指向本地下载站替身)
|
|
872
872
|
const outcome = await codeUpdate.installCodeUpdate({
|
|
873
873
|
appRoot, branch: options.code, installed, fetchImpl: github.fetch, targetOs: appUpdate.platformOs(),
|
|
874
|
-
downloadBase: process.env.MANYOYO_TEST_CODE_BASE || undefined, log: line => console.log(` ${line}`)
|
|
874
|
+
downloadBase: process.env.MANYOYO_TEST_CODE_BASE || undefined, log: line => console.log(` ${line}`),
|
|
875
|
+
// 校验通过后、下载或切换之前先说清楚装的是哪个提交
|
|
876
|
+
onInfo: info => console.log(`${CYAN} ${options.code}@${info.commit.slice(0, 7)} ${info.subject}(构建于 ${codeUpdate.formatBuiltAt(info.builtAt)})${NC}`)
|
|
875
877
|
});
|
|
876
878
|
const label = `${options.code}@${outcome.info.commit.slice(0, 7)}`;
|
|
877
879
|
if (outcome.status === 'same') {
|
|
878
880
|
console.log(`${GREEN}✅ 已是 ${label},无需更新${NC}`);
|
|
879
881
|
return;
|
|
880
882
|
}
|
|
881
|
-
|
|
883
|
+
if (outcome.status === 'same-release') {
|
|
884
|
+
console.log(`${GREEN}✅ ${label} 就是当前正式版 ${installed} 的代码,无需更新${NC}`);
|
|
885
|
+
return;
|
|
886
|
+
}
|
|
887
|
+
if (outcome.status === 'reused') console.log(`${CYAN} 使用本地已有的 ${outcome.version},无需下载${NC}`);
|
|
882
888
|
result = outcome;
|
|
883
889
|
} catch (error) {
|
|
884
890
|
if (error instanceof appUpdate.UpdateError && error.code === 'NETWORK') {
|
package/lib/app-update.js
CHANGED
|
@@ -206,6 +206,19 @@ function pruneApps(appRoot, keep) {
|
|
|
206
206
|
}
|
|
207
207
|
}
|
|
208
208
|
|
|
209
|
+
/**
|
|
210
|
+
* 切换 current 到 app/<version>/(目录必须已就位)、写 .update.json(记下上一版本)、清理旧版本。
|
|
211
|
+
* installFromArchive 与代码版的“本地已有就直接切换”共用;manifest 原样放进返回值,供调用方做收尾。
|
|
212
|
+
*/
|
|
213
|
+
function switchInstalled({ appRoot, version, manifest }) {
|
|
214
|
+
const previous = currentVersion(appRoot);
|
|
215
|
+
switchCurrent(appRoot, version);
|
|
216
|
+
writeUpdateRecord(appRoot, { previous: previous && previous !== version ? previous : readUpdateRecord(appRoot).previous || '', current: version, updatedAt: new Date().toISOString() });
|
|
217
|
+
const record = readUpdateRecord(appRoot);
|
|
218
|
+
pruneApps(appRoot, [version, record.previous].filter(Boolean));
|
|
219
|
+
return { version, previous: record.previous || '', manifest };
|
|
220
|
+
}
|
|
221
|
+
|
|
209
222
|
/**
|
|
210
223
|
* 解开已校验过的 -app.tar.gz 并切换 current(网络下载与 --file 共用)。包不完整/内容不符都在切换前中止。
|
|
211
224
|
*/
|
|
@@ -234,11 +247,7 @@ async function installFromArchive({ appRoot, archive, version, targetArch, targe
|
|
|
234
247
|
const target = path.join(appRoot, version);
|
|
235
248
|
if (version !== previous) fs.rmSync(target, { recursive: true, force: true });
|
|
236
249
|
if (version !== previous) fs.renameSync(staging, target);
|
|
237
|
-
|
|
238
|
-
writeUpdateRecord(appRoot, { previous: previous && previous !== version ? previous : readUpdateRecord(appRoot).previous || '', current: version, updatedAt: new Date().toISOString() });
|
|
239
|
-
const record = readUpdateRecord(appRoot);
|
|
240
|
-
pruneApps(appRoot, [version, record.previous].filter(Boolean));
|
|
241
|
-
return { version, previous: record.previous || '', manifest };
|
|
250
|
+
return switchInstalled({ appRoot, version, manifest });
|
|
242
251
|
} finally {
|
|
243
252
|
fs.rmSync(staging, { recursive: true, force: true });
|
|
244
253
|
}
|
|
@@ -397,6 +406,7 @@ module.exports = {
|
|
|
397
406
|
compareVersions,
|
|
398
407
|
isAppVersion,
|
|
399
408
|
installFromArchive,
|
|
409
|
+
switchInstalled,
|
|
400
410
|
detectInstallMode,
|
|
401
411
|
fetchLatestRelease,
|
|
402
412
|
parseSha256Sums,
|
package/lib/code-update.js
CHANGED
|
@@ -9,9 +9,9 @@ const fs = require('fs');
|
|
|
9
9
|
const os = require('os');
|
|
10
10
|
const path = require('path');
|
|
11
11
|
const appUpdate = require('./app-update');
|
|
12
|
-
const { downloadVerified } = require('./download-verified');
|
|
12
|
+
const { downloadVerified, sha256File } = require('./download-verified');
|
|
13
13
|
|
|
14
|
-
const { UpdateError, compareVersions, networkError, request, installFromArchive } = appUpdate;
|
|
14
|
+
const { UpdateError, compareVersions, networkError, request, installFromArchive, switchInstalled } = appUpdate;
|
|
15
15
|
|
|
16
16
|
const DOWNLOAD_BASE = 'https://github.com/xcanwin/manyoyo/releases/download/';
|
|
17
17
|
const BRANCH_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._/-]{0,100}$/;
|
|
@@ -102,23 +102,70 @@ async function fetchCodeInfo({ branch, fetchImpl = fetch, downloadBase = DOWNLOA
|
|
|
102
102
|
return validateCodeInfo(parsed, { branch });
|
|
103
103
|
}
|
|
104
104
|
|
|
105
|
+
// 当前版本的 manyoyo/package.json 里的 gitHead(正式包与代码版包构建时写入);读不到或格式不对当作没有
|
|
106
|
+
function readCurrentGitHead(appRoot) {
|
|
107
|
+
try {
|
|
108
|
+
const head = JSON.parse(fs.readFileSync(path.join(appRoot, 'current', 'manyoyo', 'package.json'), 'utf-8')).gitHead;
|
|
109
|
+
return typeof head === 'string' && /^[0-9a-f]{40}$/.test(head) ? head : '';
|
|
110
|
+
} catch (error) {
|
|
111
|
+
return '';
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* 本地 app/<版本>/ 已经是这个代码版时返回它的 manifest,否则 null(调用方照常下载,不报错)。
|
|
117
|
+
* --rollback 直接切到本地目录,所以这里的检查不能比它宽松:目录结构完整,并额外核对 manifest 与 .installed、提交号。
|
|
118
|
+
*/
|
|
119
|
+
async function findLocalCopy({ appRoot, info, targetArch, targetOs }) {
|
|
120
|
+
const dir = path.join(appRoot, info.version);
|
|
121
|
+
try {
|
|
122
|
+
for (const required of ['node/bin/node', 'manyoyo/bin/manyoyo.js', 'manifest.json', '.installed']) {
|
|
123
|
+
if (!fs.existsSync(path.join(dir, required))) return null;
|
|
124
|
+
}
|
|
125
|
+
const manifest = JSON.parse(fs.readFileSync(path.join(dir, 'manifest.json'), 'utf-8'));
|
|
126
|
+
if (!manifest || manifest.version !== info.version || manifest.kind !== 'app' || manifest.arch !== targetArch || manifest.os !== targetOs) return null;
|
|
127
|
+
if (!manifest.source || manifest.source.commit !== info.commit) return null;
|
|
128
|
+
// installFromArchive 安装时把 manifest.json 的哈希写进 .installed:对得上才说明目录没被动过
|
|
129
|
+
if (fs.readFileSync(path.join(dir, '.installed'), 'utf-8').trim() !== await sha256File(path.join(dir, 'manifest.json'))) return null;
|
|
130
|
+
return manifest;
|
|
131
|
+
} catch (error) {
|
|
132
|
+
return null;
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// 构建时间按本机时区显示成 YYYY-MM-DD HH:mm;timeZone 可注入(测试不依赖运行机器的时区)
|
|
137
|
+
function formatBuiltAt(builtAt, timeZone) {
|
|
138
|
+
const date = new Date(builtAt);
|
|
139
|
+
if (typeof builtAt !== 'string' || !builtAt || Number.isNaN(date.getTime())) return '未知时间';
|
|
140
|
+
const parts = Object.fromEntries(new Intl.DateTimeFormat('en-CA', { timeZone, year: 'numeric', month: '2-digit', day: '2-digit', hour: '2-digit', minute: '2-digit', hourCycle: 'h23' })
|
|
141
|
+
.formatToParts(date).map(part => [part.type, part.value]));
|
|
142
|
+
return `${parts.year}-${parts.month}-${parts.day} ${parts.hour}:${parts.minute}`;
|
|
143
|
+
}
|
|
144
|
+
|
|
105
145
|
/**
|
|
106
146
|
* 安装某个分支的代码版。installed 是当前版本号。
|
|
107
|
-
*
|
|
147
|
+
* 返回:{ status: 'same', info }(已是这个版本)、{ status: 'same-release', info }(与当前正式版同一个提交)、
|
|
148
|
+
* { status: 'reused' | 'installed', info, version, previous, manifest }(本地已有就直接切换 / 下载安装)。
|
|
108
149
|
* 校验失败 / 镜像未发布 / 版本更旧都在下载与切换之前中止,current 不变。
|
|
150
|
+
* onInfo(info):全部校验通过后、下载或切换之前调用,让调用方先告诉用户装的是哪个提交。
|
|
109
151
|
*/
|
|
110
|
-
async function installCodeUpdate({ appRoot, branch, installed, fetchImpl = fetch, downloadBase = DOWNLOAD_BASE, notFoundRetryMs, targetArch = appUpdate.arch(), targetOs = appUpdate.platformOs(), run, log = () => {}, tmpRoot = os.tmpdir() }) {
|
|
152
|
+
async function installCodeUpdate({ appRoot, branch, installed, fetchImpl = fetch, downloadBase = DOWNLOAD_BASE, notFoundRetryMs, onInfo, targetArch = appUpdate.arch(), targetOs = appUpdate.platformOs(), run, log = () => {}, tmpRoot = os.tmpdir() }) {
|
|
111
153
|
const info = await fetchCodeInfo({ branch, fetchImpl, downloadBase, notFoundRetryMs });
|
|
112
154
|
if (info.version === installed) return { status: 'same', info };
|
|
155
|
+
if (readCurrentGitHead(appRoot) === info.commit) return { status: 'same-release', info };
|
|
113
156
|
if (compareVersions(info.version, installed) < 0) {
|
|
114
157
|
throw new UpdateError('OLDER', `分支 ${branch} 的代码版是 ${info.version},比当前的 ${installed} 旧。${branch} 的最新提交可能还在构建,稍后再试;要换回正式版请用 manyoyo update --rollback 或重新运行安装命令。`);
|
|
115
158
|
}
|
|
116
159
|
if (!info.imageReady) {
|
|
117
160
|
throw new UpdateError('IMAGE_NOT_READY', `这个代码版需要镜像 ${info.imageVersion},但它还没有发布,现在升级会让容器拉不到镜像。请等镜像发布后再试,或换一个分支。`);
|
|
118
161
|
}
|
|
162
|
+
// 降级与镜像检查都排在本地复用之前,否则本地留着旧目录时会绕过它们
|
|
163
|
+
const local = await findLocalCopy({ appRoot, info, targetArch, targetOs });
|
|
119
164
|
const asset = info.assets[`${targetOs}-${targetArch}`];
|
|
120
|
-
|
|
121
|
-
if (
|
|
165
|
+
if (!local && !asset) throw new UpdateError('NO_ASSET', `代码版 ${info.version} 里没有适合这台机器(${targetOs}-${targetArch})的升级包。`);
|
|
166
|
+
if (typeof onInfo === 'function') onInfo(info);
|
|
167
|
+
if (local) return { status: 'reused', info, ...switchInstalled({ appRoot, version: info.version, manifest: local }) };
|
|
168
|
+
const url = `${tagBase(downloadBase, branch)}${asset.name}`;
|
|
122
169
|
// 包是按 CODE.json 里的固定命名拼出来的:先确认确实存在(404 就是没有)
|
|
123
170
|
const probe = await request(fetchImpl, url, { method: 'HEAD' });
|
|
124
171
|
if (probe.status === 404) throw new UpdateError('NO_ASSET', `代码版 ${info.version} 的升级包 ${asset.name} 还没有上传完整,请稍后再试。`);
|
|
@@ -162,4 +209,4 @@ function latestReleaseNotice(installed, latest) {
|
|
|
162
209
|
return `已是最新版本 ${installed}`;
|
|
163
210
|
}
|
|
164
211
|
|
|
165
|
-
module.exports = { isCodeVersion, latestReleaseNotice, DOWNLOAD_BASE, isValidBranch, codeTagForBranch, codeAssetName, validateCodeInfo, fetchCodeInfo, installCodeUpdate };
|
|
212
|
+
module.exports = { formatBuiltAt, isCodeVersion, latestReleaseNotice, DOWNLOAD_BASE, isValidBranch, codeTagForBranch, codeAssetName, validateCodeInfo, fetchCodeInfo, installCodeUpdate };
|