aaden-cli 1.6.121-rc.0 → 1.6.122-rc.0
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/AadenDeploy/appCenterUsage.py +61 -0
- package/AadenDeploy/lightUpdate.sh +30 -4
- package/dist/index.js +226 -1
- package/index.js +142 -0
- package/modules/release-channel.js +76 -0
- package/package.json +2 -2
- package/test/app_version_test.py +48 -0
|
@@ -13,7 +13,9 @@ local(localhost/127.x,收银机本机)、lan(局域网 IP,平板等)
|
|
|
13
13
|
|
|
14
14
|
用法: appCenterUsage.py [日志目录,默认 /var/log/apache2];需要能读 access.log*(root 或 adm 组)。
|
|
15
15
|
"""
|
|
16
|
+
import datetime
|
|
16
17
|
import glob
|
|
18
|
+
import hashlib
|
|
17
19
|
import gzip
|
|
18
20
|
import json
|
|
19
21
|
import os
|
|
@@ -62,6 +64,64 @@ def parse_ts(ts):
|
|
|
62
64
|
return day, f'{day}T{hms}'
|
|
63
65
|
|
|
64
66
|
|
|
67
|
+
# index.html 里引用的入口 js(Vue CLI:js/app.1234abcd.js;Vite:assets/index-AbCd1234.js,老版 Vite:assets/index.1234abcd.js)
|
|
68
|
+
MAIN_JS = re.compile(r'''src=["']?[^"'\s>]*?((?:js/(?:app|main|index)\.[0-9a-f]{8}|assets/(?:index|main)[.-][\w-]{8,})\.js)''')
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def app_version(rel):
|
|
72
|
+
try:
|
|
73
|
+
return _app_version(rel)
|
|
74
|
+
except Exception: # 兜底:版本信息只是附带的,任何意外都不能让整份访问量上报失败
|
|
75
|
+
return {'version': 'unknown'}
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def _app_version(rel):
|
|
79
|
+
"""
|
|
80
|
+
某个前端目录装的是哪个版本(前端版本遥测 A 层,云端 device_app_inventory.app_versions)。值一律是对象:
|
|
81
|
+
version / commit AppCenter 发布时写进产物的 version.json(innerken-gmbh/workflows appcenter-publish);
|
|
82
|
+
没有就读目录里的 package.json;都没有是 "unknown"
|
|
83
|
+
treeSha AppCenter 目录的 .appcenter-manifest 的 sha256,跟 S3 manifest 的 treeSha256 一致
|
|
84
|
+
fingerprint index.html 的 sha256 前 16 位 + 入口 js 文件名:根目录老前端(不走 AppCenter、没有 version.json)
|
|
85
|
+
mainJs 靠它区分是哪次构建,云端按已知构建换算成版本号
|
|
86
|
+
deployedAt index.html 的修改时间(≈ 装到这台机器的时间,不是构建时间)
|
|
87
|
+
读不到的字段就不带,任何异常都不能影响访问量上报。
|
|
88
|
+
"""
|
|
89
|
+
d = os.path.join(DOCROOT, rel)
|
|
90
|
+
info = {'version': 'unknown'}
|
|
91
|
+
try:
|
|
92
|
+
for name, keys in (('version.json', ('version', 'commit')), ('package.json', ('version',))):
|
|
93
|
+
f = os.path.join(d, name)
|
|
94
|
+
if os.path.isfile(f):
|
|
95
|
+
with open(f, encoding='utf-8', errors='replace') as fh:
|
|
96
|
+
data = json.load(fh)
|
|
97
|
+
for k in keys:
|
|
98
|
+
v = str(data.get(k) or '').strip()
|
|
99
|
+
if v:
|
|
100
|
+
info[k] = v[:40]
|
|
101
|
+
break
|
|
102
|
+
except (OSError, ValueError, AttributeError):
|
|
103
|
+
pass
|
|
104
|
+
try:
|
|
105
|
+
mf = os.path.join(d, '.appcenter-manifest')
|
|
106
|
+
if os.path.isfile(mf):
|
|
107
|
+
with open(mf, 'rb') as fh:
|
|
108
|
+
info['treeSha'] = hashlib.sha256(fh.read()).hexdigest()
|
|
109
|
+
except OSError:
|
|
110
|
+
pass
|
|
111
|
+
try:
|
|
112
|
+
idx = os.path.join(d, 'index.html')
|
|
113
|
+
with open(idx, 'rb') as fh:
|
|
114
|
+
html = fh.read()
|
|
115
|
+
info['fingerprint'] = hashlib.sha256(html).hexdigest()[:16]
|
|
116
|
+
m = MAIN_JS.search(html.decode('utf-8', 'replace'))
|
|
117
|
+
if m:
|
|
118
|
+
info['mainJs'] = m.group(1)[:64]
|
|
119
|
+
info['deployedAt'] = datetime.datetime.fromtimestamp(os.path.getmtime(idx)).strftime('%Y-%m-%dT%H:%M:%S')
|
|
120
|
+
except OSError:
|
|
121
|
+
pass
|
|
122
|
+
return info
|
|
123
|
+
|
|
124
|
+
|
|
65
125
|
def main():
|
|
66
126
|
installed = []
|
|
67
127
|
if os.path.isdir(APPCENTER):
|
|
@@ -135,6 +195,7 @@ def main():
|
|
|
135
195
|
'logTo': log_to,
|
|
136
196
|
'totalRequests': total,
|
|
137
197
|
'installedApps': installed,
|
|
198
|
+
'appVersions': {app: app_version(app) for app in installed},
|
|
138
199
|
'usage': [
|
|
139
200
|
{
|
|
140
201
|
'app': app, 'day': day, 'pageLoads': s['pageLoads'], 'requests': s['requests'],
|
|
@@ -10,6 +10,13 @@
|
|
|
10
10
|
# —— 光靠退出码分不清"TSE 忙跳过"和"产物陈旧",deviceLog 里就只剩一句含糊的失败。
|
|
11
11
|
set -uo pipefail
|
|
12
12
|
S3="https://aaden-backend.s3.eu-central-1.amazonaws.com"
|
|
13
|
+
# 参数只有 staging 通道(modules/release-channel.js,测试机)会传;不带参数 = 原来的 stable 路径,URL 和判断都不变。
|
|
14
|
+
# $1 = staging → 从 S3 的 staging/ 前缀拉(composer.lock 变了不退回全量包,直接跳过,由调用方这次改走 stable)
|
|
15
|
+
# $2 = semver → 版本带「-」后缀时按 semver 比(1.9.83-staging.15 < 1.9.83);纯 x.y.z 之间照旧
|
|
16
|
+
CHANNEL="${1:-}"
|
|
17
|
+
CMP="${2:-}"
|
|
18
|
+
PREFIX=""
|
|
19
|
+
[ "$CHANNEL" = staging ] && PREFIX="/staging"
|
|
13
20
|
STAGE=/var/tmp/aaden-lu-stage # 绝对路径:download 与 sudo cp 必须看到同一个目录(~ 在 sudo 下会变 /root)
|
|
14
21
|
BACKUP=/var/lib/aaden-php-backup
|
|
15
22
|
PW='Innerken322.'
|
|
@@ -29,13 +36,29 @@ if exec 9<"$LOCK" 2>/dev/null; then
|
|
|
29
36
|
fi
|
|
30
37
|
|
|
31
38
|
# 点分数字版本比较:a > b 返回 0。与 index.js 的 versionGt 同一套语义('1.9.61' > '1.7.908')。
|
|
32
|
-
|
|
39
|
+
# 只有 staging 通道传了 semver、并且两个版本里至少一个带「-」后缀(1.9.83-staging.15)时才改用 verGtSemver;
|
|
40
|
+
# 纯 x.y.z 之间、以及不带参数的 stable 路径,一律走下面原来的比较,结果和改动前逐个一致。
|
|
41
|
+
verGt(){ [ "$CMP" = semver ] && case "$1$2" in *-*) verGtSemver "$1" "$2"; return;; esac; python3 -c "
|
|
33
42
|
import sys
|
|
34
43
|
pa=[int(x) if x.isdigit() else 0 for x in sys.argv[1].split('.')]
|
|
35
44
|
pb=[int(x) if x.isdigit() else 0 for x in sys.argv[2].split('.')]
|
|
36
45
|
n=max(len(pa),len(pb)); pa+=[0]*(n-len(pa)); pb+=[0]*(n-len(pb))
|
|
37
46
|
sys.exit(0 if pa>pb else 1)" "$1" "$2" 2>/dev/null; }
|
|
38
47
|
|
|
48
|
+
# semver:x.y.z[-预发布],有预发布的 < 没有的,预发布逐段比(数字段按数值、数字段 < 字母段);不合法一律「不大于」(不更新)
|
|
49
|
+
verGtSemver(){ python3 -c "
|
|
50
|
+
import re,sys
|
|
51
|
+
def p(v):
|
|
52
|
+
m=re.fullmatch(r'(\d+)\.(\d+)\.(\d+)(?:-([0-9A-Za-z.-]+))?',v.strip())
|
|
53
|
+
if not m: return None
|
|
54
|
+
return [int(x) for x in m.groups()[:3]], (m.group(4).split('.') if m.group(4) else [])
|
|
55
|
+
def key(pre):
|
|
56
|
+
if not pre: return (1,)
|
|
57
|
+
return (0,)+tuple((0,int(x),'') if x.isdigit() else (1,0,x) for x in pre)+((-1,0,''),)
|
|
58
|
+
a,b=p(sys.argv[1]),p(sys.argv[2])
|
|
59
|
+
if a is None or b is None: sys.exit(1)
|
|
60
|
+
sys.exit(0 if (a[0],key(a[1]))>(b[0],key(b[1])) else 1)" "$1" "$2" 2>/dev/null; }
|
|
61
|
+
|
|
39
62
|
# 读 package.json 的 version;读不到输出空串。
|
|
40
63
|
pkgVer(){ python3 -c "
|
|
41
64
|
import json,sys
|
|
@@ -48,12 +71,15 @@ except Exception: print('')" "$1" 2>/dev/null; }
|
|
|
48
71
|
if ! echo "$PW" | sudo -S true 2>/dev/null; then finish "ABORT_SUDO_AUTH (aaden sudo 密码不符?)" 1; fi
|
|
49
72
|
|
|
50
73
|
# ---- composer.lock 门 ----
|
|
51
|
-
REMOTE_LOCK=$(curl -s "$S3/raw/PHP/composer.lock" --max-time 30)
|
|
74
|
+
REMOTE_LOCK=$(curl -s "$S3$PREFIX/raw/PHP/composer.lock" --max-time 30)
|
|
52
75
|
LOCAL_LOCK=$(cat /var/www/html/PHP/composer.lock 2>/dev/null)
|
|
53
76
|
RH=$(printf '%s' "$REMOTE_LOCK" | shasum -a 256 | cut -d' ' -f1)
|
|
54
77
|
LH=$(printf '%s' "$LOCAL_LOCK" | shasum -a 256 | cut -d' ' -f1)
|
|
55
78
|
if [ -n "$REMOTE_LOCK" ] && [ "$RH" = "$LH" ]; then
|
|
56
79
|
ARTIFACT="code.zip"; echo "composer.lock 一致 → 轻量 code.zip"
|
|
80
|
+
elif [ "$CHANNEL" = staging ]; then
|
|
81
|
+
# staging/ 下没有全量包:vendor 变了就这次不走 staging(调用方改走 stable),不拿 stable 的全量包冒充
|
|
82
|
+
finish "SKIP_STAGING_LOCK (staging 的 composer.lock 和本机不一致或取不到,这次不走 staging)" 1
|
|
57
83
|
else
|
|
58
84
|
ARTIFACT="latest.zip"; echo "composer.lock 变化(或取不到) → 全量 latest.zip"
|
|
59
85
|
fi
|
|
@@ -69,7 +95,7 @@ ZIP="$STAGE/$ARTIFACT"
|
|
|
69
95
|
DL=/var/tmp/aaden-lu-dl
|
|
70
96
|
PART="$DL/$ARTIFACT.part"
|
|
71
97
|
sudoS "mkdir -p $DL && chmod 777 $DL && { chown -f $(id -un):$(id -gn) $DL/* 2>/dev/null; true; }"
|
|
72
|
-
HEAD=$(curl -sfI "$S3/$ARTIFACT" --max-time 30 | tr -d '\r')
|
|
98
|
+
HEAD=$(curl -sfI "$S3$PREFIX/$ARTIFACT" --max-time 30 | tr -d '\r')
|
|
73
99
|
ETAG=$(printf '%s\n' "$HEAD" | awk 'tolower($1)=="etag:"{print $2}' | tr -d '"')
|
|
74
100
|
SIZE=$(printf '%s\n' "$HEAD" | awk 'tolower($1)=="content-length:"{print $2}')
|
|
75
101
|
if [ -f "$PART" ] && [ "$(cat "$DL/$ARTIFACT.etag" 2>/dev/null)" != "$ETAG" ]; then
|
|
@@ -77,7 +103,7 @@ if [ -f "$PART" ] && [ "$(cat "$DL/$ARTIFACT.etag" 2>/dev/null)" != "$ETAG" ]; t
|
|
|
77
103
|
fi
|
|
78
104
|
echo "$ETAG" > "$DL/$ARTIFACT.etag"
|
|
79
105
|
[ -f "$PART" ] && echo "接着上次续传:已有 $(( $(stat -c %s "$PART") / 1048576 )) MB"
|
|
80
|
-
curl -fsSL -C - "$S3/$ARTIFACT" -o "$PART" --max-time 1200
|
|
106
|
+
curl -fsSL -C - "$S3$PREFIX/$ARTIFACT" -o "$PART" --max-time 1200
|
|
81
107
|
GOT=$(stat -c %s "$PART" 2>/dev/null || echo 0)
|
|
82
108
|
if [ -n "$SIZE" ] && [ "$GOT" -lt "$SIZE" ]; then
|
|
83
109
|
# 没下完:半截留着下次接着下,这次什么都不动
|
package/dist/index.js
CHANGED
|
@@ -570,6 +570,89 @@ function collectHostInfo() {
|
|
|
570
570
|
module.exports = {collectHostInfo}
|
|
571
571
|
|
|
572
572
|
|
|
573
|
+
/***/ }),
|
|
574
|
+
|
|
575
|
+
/***/ 6096:
|
|
576
|
+
/***/ ((module, __unused_webpack_exports, __nccwpck_require__) => {
|
|
577
|
+
|
|
578
|
+
/**
|
|
579
|
+
* 后端发布通道(staging 预发布)。只给测试机用:ik0001(设备号 1)和测试门店农场 9020–9099。
|
|
580
|
+
*
|
|
581
|
+
* - 通道写在本机文件 /etc/aaden/config/release-channel(`aaden-cli release-channel staging|stable` 写,root 写、644);
|
|
582
|
+
* - light-update 只有在「文件里写的是 staging」并且「本机设备号是测试机」时才走 staging,其他一律是原来的 stable 路径;
|
|
583
|
+
* - staging 的包由 php 仓库 staging 分支的 workflow 发到 S3 的 staging/ 前缀,版本号形如 1.9.83-staging.15。
|
|
584
|
+
*
|
|
585
|
+
* 不是测试机、没有文件、文件内容不是 staging:light-update 的代码路径和请求的 URL 跟原来完全一样(只多读一次本地文件)。
|
|
586
|
+
* 兼容 node 10(门店上最老的 node),不用可选链等新语法。
|
|
587
|
+
*/
|
|
588
|
+
const fs = __nccwpck_require__(7147)
|
|
589
|
+
|
|
590
|
+
const CHANNEL_FILE = '/etc/aaden/config/release-channel'
|
|
591
|
+
const STAGING = 'staging'
|
|
592
|
+
const STABLE = 'stable'
|
|
593
|
+
|
|
594
|
+
/** 测试机:1 号机,或测试门店农场号段 9020–9099(和云端 TestStores 默认值一致,写死在这里,不信任何外部配置) */
|
|
595
|
+
function isTestDevice(deviceId) {
|
|
596
|
+
const id = parseInt(String(deviceId).trim(), 10)
|
|
597
|
+
if (isNaN(id)) return false
|
|
598
|
+
return id === 1 || (id >= 9020 && id <= 9099)
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
/** 本机文件里写的通道;读不到、不认识一律 stable */
|
|
602
|
+
function readChannelFile() {
|
|
603
|
+
try {
|
|
604
|
+
const v = fs.readFileSync(CHANNEL_FILE, 'utf8').trim().toLowerCase()
|
|
605
|
+
return v === STAGING ? STAGING : STABLE
|
|
606
|
+
} catch (e) {
|
|
607
|
+
return STABLE
|
|
608
|
+
}
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
/**
|
|
612
|
+
* semver 解析:只认 x.y.z 和 x.y.z-<预发布标识>。不合法返回 null。
|
|
613
|
+
* 预发布标识按 semver:逐段比,纯数字段按数值、数字段 < 字母段,段少的小;有预发布标识的 < 没有的。
|
|
614
|
+
*/
|
|
615
|
+
function parseSemver(v) {
|
|
616
|
+
const m = /^(\d+)\.(\d+)\.(\d+)(?:-([0-9A-Za-z.-]+))?$/.exec(String(v || '').trim())
|
|
617
|
+
if (!m) return null
|
|
618
|
+
return {core: [parseInt(m[1], 10), parseInt(m[2], 10), parseInt(m[3], 10)], pre: m[4] ? m[4].split('.') : []}
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
/** a 比 b 大返回 1,小返回 -1,相等 0;任一不合法返回 null(调用方按「不更新」处理) */
|
|
622
|
+
function semverCompare(a, b) {
|
|
623
|
+
const pa = parseSemver(a), pb = parseSemver(b)
|
|
624
|
+
if (!pa || !pb) return null
|
|
625
|
+
for (let i = 0; i < 3; i++) {
|
|
626
|
+
if (pa.core[i] !== pb.core[i]) return pa.core[i] > pb.core[i] ? 1 : -1
|
|
627
|
+
}
|
|
628
|
+
if (pa.pre.length === 0 && pb.pre.length === 0) return 0
|
|
629
|
+
if (pa.pre.length === 0) return 1
|
|
630
|
+
if (pb.pre.length === 0) return -1
|
|
631
|
+
for (let i = 0; i < Math.max(pa.pre.length, pb.pre.length); i++) {
|
|
632
|
+
const x = pa.pre[i], y = pb.pre[i]
|
|
633
|
+
if (x === undefined) return -1
|
|
634
|
+
if (y === undefined) return 1
|
|
635
|
+
const xn = /^\d+$/.test(x), yn = /^\d+$/.test(y)
|
|
636
|
+
if (xn && yn) {
|
|
637
|
+
const d = parseInt(x, 10) - parseInt(y, 10)
|
|
638
|
+
if (d !== 0) return d > 0 ? 1 : -1
|
|
639
|
+
} else if (xn !== yn) {
|
|
640
|
+
return xn ? -1 : 1
|
|
641
|
+
} else if (x !== y) {
|
|
642
|
+
return x > y ? 1 : -1
|
|
643
|
+
}
|
|
644
|
+
}
|
|
645
|
+
return 0
|
|
646
|
+
}
|
|
647
|
+
|
|
648
|
+
/** a > b;不合法按 false(不更新) */
|
|
649
|
+
function semverGt(a, b) {
|
|
650
|
+
return semverCompare(a, b) === 1
|
|
651
|
+
}
|
|
652
|
+
|
|
653
|
+
module.exports = {CHANNEL_FILE, STAGING, STABLE, isTestDevice, readChannelFile, parseSemver, semverCompare, semverGt}
|
|
654
|
+
|
|
655
|
+
|
|
573
656
|
/***/ }),
|
|
574
657
|
|
|
575
658
|
/***/ 5906:
|
|
@@ -19269,7 +19352,7 @@ module.exports = JSON.parse('{"name":"axios","version":"0.19.2","description":"P
|
|
|
19269
19352
|
/***/ ((module) => {
|
|
19270
19353
|
|
|
19271
19354
|
"use strict";
|
|
19272
|
-
module.exports = JSON.parse('{"i8":"1.6.
|
|
19355
|
+
module.exports = JSON.parse('{"i8":"1.6.122-rc.0"}');
|
|
19273
19356
|
|
|
19274
19357
|
/***/ })
|
|
19275
19358
|
|
|
@@ -19355,6 +19438,7 @@ const os = __nccwpck_require__(2037)
|
|
|
19355
19438
|
const InnerKenTimeStampFormat = 'YYYYMMDDHHmmss'
|
|
19356
19439
|
const prompts = __nccwpck_require__(9646)
|
|
19357
19440
|
const {deviceEcho, logAction, getCurrentDeviceId} = __nccwpck_require__(7415)
|
|
19441
|
+
const releaseChannel = __nccwpck_require__(6096)
|
|
19358
19442
|
const {collectHostInfo} = __nccwpck_require__(3727)
|
|
19359
19443
|
const {collectTseInfo} = __nccwpck_require__(6677)
|
|
19360
19444
|
const {parseSnapListVersion} = __nccwpck_require__(5906)
|
|
@@ -19379,6 +19463,7 @@ program
|
|
|
19379
19463
|
.option('-d, --debug', 'use Debug Out put')
|
|
19380
19464
|
program.command('update [channel]').action(update)
|
|
19381
19465
|
program.command('light-update').action(lightUpdate) // 轻量+带闸门的后端部署,供 auto-upgrade/patch 调用
|
|
19466
|
+
program.command('release-channel [channel]').action(setReleaseChannel) // 测试机切后端发布通道 staging|stable(不带参数 = 查看)
|
|
19382
19467
|
program.command('appcenter-usage [force]').action(appCenterUsage) // 门店前端目录(AppCenter 各应用 + 根目录老前端)访问量上报云端,供 auto-upgrade 每天调一次
|
|
19383
19468
|
program.command('appcenter-sync [channel]').action(appCenterSync) // AppCenter 前端按应用同步,供 auto-upgrade 调用
|
|
19384
19469
|
program.command('rollback').action(rollback)
|
|
@@ -19883,6 +19968,13 @@ async function update(channel = '') {
|
|
|
19883
19968
|
* 具体状态由 lightUpdate.sh 写在 /var/tmp/aaden-lu-status,这里读出来一并上报
|
|
19884
19969
|
*/
|
|
19885
19970
|
async function lightUpdate() {
|
|
19971
|
+
// staging 通道(modules/release-channel.js):本机文件写着 staging 并且是测试机(1、9020–9099)才走;
|
|
19972
|
+
// 其他一律是下面原来的 stable 路径,只多读一次本地文件
|
|
19973
|
+
if (releaseChannel.readChannelFile() === releaseChannel.STAGING) {
|
|
19974
|
+
const deviceId = await getCurrentDeviceId()
|
|
19975
|
+
if (releaseChannel.isTestDevice(deviceId)) return lightUpdateStaging(deviceId)
|
|
19976
|
+
console.log(('通道文件写着 staging,但设备号 ' + deviceId + ' 不是测试机,按 stable 走').yellow)
|
|
19977
|
+
}
|
|
19886
19978
|
try {
|
|
19887
19979
|
const localVersion = getBackendVersionOffline()
|
|
19888
19980
|
const remoteVersion = (await hillo.get('https://aaden-backend.s3.eu-central-1.amazonaws.com/raw/package.json')).version
|
|
@@ -20015,6 +20107,139 @@ async function appCenterSync(channel = 'stable') {
|
|
|
20015
20107
|
}
|
|
20016
20108
|
}
|
|
20017
20109
|
|
|
20110
|
+
/**
|
|
20111
|
+
* staging 通道的 light-update(只有测试机会进来):
|
|
20112
|
+
* - 取 stable(raw/package.json)和 staging(staging/raw/package.json)两份清单,选更高的那个;
|
|
20113
|
+
* stable 清单拿不到 → 整次中止;staging 清单拿不到或格式不对 → 这一次按 stable;
|
|
20114
|
+
* - maxVersion 灰度门照旧;
|
|
20115
|
+
* - 选中 staging 时 lightUpdate.sh 带 staging 参数(拉 staging/ 前缀);下载之前就失败(staging 的 composer.lock 变了、
|
|
20116
|
+
* 下载失败、包有问题)→ 这一次改走 stable;部署后回滚、TSE 忙、别的 light-update 在跑 → 不改走;
|
|
20117
|
+
* - 版本比较见 versionGtChannel:带「-」后缀才按 semver(1.9.83-staging.15 < 1.9.83,stable 发版后测试机自动跟上正式版),
|
|
20118
|
+
* 纯 x.y.z 之间和原来的 versionGt 完全一样。
|
|
20119
|
+
*/
|
|
20120
|
+
async function lightUpdateStaging(deviceId) {
|
|
20121
|
+
const S3 = 'https://aaden-backend.s3.eu-central-1.amazonaws.com'
|
|
20122
|
+
const tag = 'light-update[staging]'
|
|
20123
|
+
try {
|
|
20124
|
+
const localVersion = getBackendVersionOffline()
|
|
20125
|
+
let stableVersion = null
|
|
20126
|
+
try {
|
|
20127
|
+
stableVersion = (await hillo.get(S3 + '/raw/package.json')).version
|
|
20128
|
+
} catch (e) {
|
|
20129
|
+
console.log('取 stable 清单失败:', e.message)
|
|
20130
|
+
}
|
|
20131
|
+
if (!releaseChannel.parseSemver(stableVersion)) {
|
|
20132
|
+
process.exitCode = 1
|
|
20133
|
+
await logAction(tag + ' 中止:取不到 stable 清单(' + stableVersion + ')', 1)
|
|
20134
|
+
return
|
|
20135
|
+
}
|
|
20136
|
+
let stagingVersion = null
|
|
20137
|
+
try {
|
|
20138
|
+
stagingVersion = (await hillo.get(S3 + '/staging/raw/package.json')).version
|
|
20139
|
+
} catch (e) {
|
|
20140
|
+
console.log('取 staging 清单失败,这次按 stable:', e.message)
|
|
20141
|
+
}
|
|
20142
|
+
if (stagingVersion && !releaseChannel.parseSemver(stagingVersion)) {
|
|
20143
|
+
console.log(('staging 清单版本格式不对(' + stagingVersion + '),这次按 stable').yellow)
|
|
20144
|
+
stagingVersion = null
|
|
20145
|
+
}
|
|
20146
|
+
const useStaging = !!stagingVersion && versionGtChannel(stagingVersion, stableVersion)
|
|
20147
|
+
const target = useStaging ? stagingVersion : stableVersion
|
|
20148
|
+
if (!versionGtChannel(target, localVersion)) {
|
|
20149
|
+
console.log((tag + ' 已是最新或更高 (本机 ' + localVersion + ' / stable ' + stableVersion + ' / staging ' + (stagingVersion || '-') + '),跳过').green)
|
|
20150
|
+
return
|
|
20151
|
+
}
|
|
20152
|
+
const maxVersion = await getDeviceMaxVersion(deviceId)
|
|
20153
|
+
if (maxVersion && maxVersion !== '-1' && versionGtChannel(target, maxVersion)) {
|
|
20154
|
+
const msg = tag + ' 跳过:灰度闸门 target=' + target + ' > maxVersion=' + maxVersion
|
|
20155
|
+
console.log(msg.yellow)
|
|
20156
|
+
await logAction(msg, 0)
|
|
20157
|
+
return
|
|
20158
|
+
}
|
|
20159
|
+
|
|
20160
|
+
let run = await runLightUpdateScript(useStaging ? 'staging' : 'stable')
|
|
20161
|
+
let channelUsed = useStaging ? 'staging' : 'stable'
|
|
20162
|
+
// 下载之前就失败的这几种,这一次改走 stable(前提是 stable 比本机新);部署后回滚 / TSE 忙等不改走。
|
|
20163
|
+
// SKIP_DOWNLOAD_PARTIAL(半截包留着下次续传)退出码是 0,本来就不回落,不列在这里
|
|
20164
|
+
const FALLBACK = ['SKIP_STAGING_LOCK', 'ABORT_DOWNLOAD', 'ABORT_BAD_ARTIFACT', 'ABORT_NO_VERSION']
|
|
20165
|
+
if (useStaging && run.code !== 0 && FALLBACK.some(s => run.status.indexOf(s) === 0) &&
|
|
20166
|
+
versionGtChannel(stableVersion, localVersion)) {
|
|
20167
|
+
console.log(('staging 这次没走通(' + run.status + '),改走 stable').yellow)
|
|
20168
|
+
run = await runLightUpdateScript('stable')
|
|
20169
|
+
channelUsed = 'stable(staging 回落)'
|
|
20170
|
+
}
|
|
20171
|
+
const suffix = run.status ? ' [' + run.status + ']' : ''
|
|
20172
|
+
const newVersion = readBackendVersionFresh()
|
|
20173
|
+
if (run.code !== 0) {
|
|
20174
|
+
process.exitCode = run.code || 1
|
|
20175
|
+
await logAction(tag + ' 失败/未部署 (仍 ' + newVersion + ',通道 ' + channelUsed + ')' + suffix, run.code || 1)
|
|
20176
|
+
} else if (newVersion !== localVersion) {
|
|
20177
|
+
await logAction(tag + ' 完成 ' + localVersion + ' -> ' + newVersion + '(通道 ' + channelUsed + ')' + suffix, 0)
|
|
20178
|
+
} else {
|
|
20179
|
+
await logAction(tag + ' 无实际变化 (' + newVersion + ',通道 ' + channelUsed + ')' + suffix, 0)
|
|
20180
|
+
}
|
|
20181
|
+
} catch (e) {
|
|
20182
|
+
console.error(e)
|
|
20183
|
+
try { await logAction(tag + ' 异常: ' + (e.message || e), 1) } catch (_) {}
|
|
20184
|
+
}
|
|
20185
|
+
}
|
|
20186
|
+
|
|
20187
|
+
/** staging 通道跑一次 lightUpdate.sh:channel = staging | stable,带 semver 参数(脚本里只在版本带「-」时才按 semver 比)。返回退出码和末行状态 */
|
|
20188
|
+
async function runLightUpdateScript(channel) {
|
|
20189
|
+
try { fs.unlinkSync(LU_STATUS_FILE) } catch (e) { /* 本来就没有,正常 */ }
|
|
20190
|
+
const command = [
|
|
20191
|
+
...getPrivilegeCMD('AadenDeploy/lightUpdate.sh'),
|
|
20192
|
+
'cd AadenDeploy',
|
|
20193
|
+
'./lightUpdate.sh ' + (channel === 'staging' ? 'staging' : 'stable') + ' semver'
|
|
20194
|
+
]
|
|
20195
|
+
const {code} = await cli.executeInShell(command, program.debug)
|
|
20196
|
+
return {code, status: readLightUpdateStatus()}
|
|
20197
|
+
}
|
|
20198
|
+
|
|
20199
|
+
/** 测试机切后端发布通道:写 /etc/aaden/config/release-channel(root 写、644);不带参数 = 查看当前通道 */
|
|
20200
|
+
async function setReleaseChannel(channel) {
|
|
20201
|
+
const deviceId = await getCurrentDeviceId()
|
|
20202
|
+
const test = releaseChannel.isTestDevice(deviceId)
|
|
20203
|
+
if (!channel) {
|
|
20204
|
+
console.log('设备 ' + deviceId + '(' + (test ? '测试机' : '不是测试机,通道文件不生效') + '),通道文件:' + releaseChannel.readChannelFile())
|
|
20205
|
+
return
|
|
20206
|
+
}
|
|
20207
|
+
const v = String(channel).trim().toLowerCase()
|
|
20208
|
+
if (v !== releaseChannel.STAGING && v !== releaseChannel.STABLE) {
|
|
20209
|
+
console.log('通道只能是 staging 或 stable'.red)
|
|
20210
|
+
process.exitCode = 1
|
|
20211
|
+
return
|
|
20212
|
+
}
|
|
20213
|
+
if (v === releaseChannel.STAGING && !test) {
|
|
20214
|
+
console.log(('设备 ' + deviceId + ' 不是测试机(只有 1 和 9020–9099 能切 staging)').red)
|
|
20215
|
+
process.exitCode = 1
|
|
20216
|
+
return
|
|
20217
|
+
}
|
|
20218
|
+
const file = releaseChannel.CHANNEL_FILE
|
|
20219
|
+
// 密码走 sudo -S 的标准输入,写文件放在 bash -c 里(不能把内容也从管道喂给 sudo:会被当成密码吃掉);v 只可能是 staging / stable
|
|
20220
|
+
const {code} = await cli.executeInShell([
|
|
20221
|
+
'echo "Innerken322." | sudo -S -p "" bash -c "mkdir -p ' + path.dirname(file) + ' && printf \'%s\\n\' ' + v + ' > ' + file + ' && chmod 644 ' + file + '"',
|
|
20222
|
+
], program.debug)
|
|
20223
|
+
if (code !== 0) {
|
|
20224
|
+
process.exitCode = code
|
|
20225
|
+
console.log('写通道文件失败'.red)
|
|
20226
|
+
return
|
|
20227
|
+
}
|
|
20228
|
+
console.log(('后端发布通道已设为 ' + v + '(下一次 light-update 生效)' +
|
|
20229
|
+
(v === releaseChannel.STABLE ? ';本机版本如果比 stable 新,按现有规则不会降级' : '')).green)
|
|
20230
|
+
await logAction('release-channel → ' + v, 0)
|
|
20231
|
+
}
|
|
20232
|
+
|
|
20233
|
+
/**
|
|
20234
|
+
* staging 通道用的版本比较:两个版本里至少一个带「-」后缀(1.9.83-staging.15)才按 semver 比;
|
|
20235
|
+
* 纯 x.y.z 之间一律走原来的 versionGt,结果和改动前逐个一致。
|
|
20236
|
+
*/
|
|
20237
|
+
function versionGtChannel(a, b) {
|
|
20238
|
+
return (String(a).indexOf('-') >= 0 || String(b).indexOf('-') >= 0)
|
|
20239
|
+
? releaseChannel.semverGt(a, b)
|
|
20240
|
+
: versionGt(a, b)
|
|
20241
|
+
}
|
|
20242
|
+
|
|
20018
20243
|
/** 比较点分数字版本,a > b 返回 true。'1.9.61' > '1.7.908' === true。 */
|
|
20019
20244
|
function versionGt(a, b) {
|
|
20020
20245
|
const pa = String(a).split('.').map(n => parseInt(n) || 0)
|
package/index.js
CHANGED
|
@@ -12,6 +12,7 @@ const os = require('os')
|
|
|
12
12
|
const InnerKenTimeStampFormat = 'YYYYMMDDHHmmss'
|
|
13
13
|
const prompts = require('prompts')
|
|
14
14
|
const {deviceEcho, logAction, getCurrentDeviceId} = require('./modules/firebase')
|
|
15
|
+
const releaseChannel = require('./modules/release-channel')
|
|
15
16
|
const {collectHostInfo} = require('./modules/host-info')
|
|
16
17
|
const {collectTseInfo} = require('./modules/tse-info')
|
|
17
18
|
const {parseSnapListVersion} = require('./modules/snap-version')
|
|
@@ -36,6 +37,7 @@ program
|
|
|
36
37
|
.option('-d, --debug', 'use Debug Out put')
|
|
37
38
|
program.command('update [channel]').action(update)
|
|
38
39
|
program.command('light-update').action(lightUpdate) // 轻量+带闸门的后端部署,供 auto-upgrade/patch 调用
|
|
40
|
+
program.command('release-channel [channel]').action(setReleaseChannel) // 测试机切后端发布通道 staging|stable(不带参数 = 查看)
|
|
39
41
|
program.command('appcenter-usage [force]').action(appCenterUsage) // 门店前端目录(AppCenter 各应用 + 根目录老前端)访问量上报云端,供 auto-upgrade 每天调一次
|
|
40
42
|
program.command('appcenter-sync [channel]').action(appCenterSync) // AppCenter 前端按应用同步,供 auto-upgrade 调用
|
|
41
43
|
program.command('rollback').action(rollback)
|
|
@@ -540,6 +542,13 @@ async function update(channel = '') {
|
|
|
540
542
|
* 具体状态由 lightUpdate.sh 写在 /var/tmp/aaden-lu-status,这里读出来一并上报
|
|
541
543
|
*/
|
|
542
544
|
async function lightUpdate() {
|
|
545
|
+
// staging 通道(modules/release-channel.js):本机文件写着 staging 并且是测试机(1、9020–9099)才走;
|
|
546
|
+
// 其他一律是下面原来的 stable 路径,只多读一次本地文件
|
|
547
|
+
if (releaseChannel.readChannelFile() === releaseChannel.STAGING) {
|
|
548
|
+
const deviceId = await getCurrentDeviceId()
|
|
549
|
+
if (releaseChannel.isTestDevice(deviceId)) return lightUpdateStaging(deviceId)
|
|
550
|
+
console.log(('通道文件写着 staging,但设备号 ' + deviceId + ' 不是测试机,按 stable 走').yellow)
|
|
551
|
+
}
|
|
543
552
|
try {
|
|
544
553
|
const localVersion = getBackendVersionOffline()
|
|
545
554
|
const remoteVersion = (await hillo.get('https://aaden-backend.s3.eu-central-1.amazonaws.com/raw/package.json')).version
|
|
@@ -672,6 +681,139 @@ async function appCenterSync(channel = 'stable') {
|
|
|
672
681
|
}
|
|
673
682
|
}
|
|
674
683
|
|
|
684
|
+
/**
|
|
685
|
+
* staging 通道的 light-update(只有测试机会进来):
|
|
686
|
+
* - 取 stable(raw/package.json)和 staging(staging/raw/package.json)两份清单,选更高的那个;
|
|
687
|
+
* stable 清单拿不到 → 整次中止;staging 清单拿不到或格式不对 → 这一次按 stable;
|
|
688
|
+
* - maxVersion 灰度门照旧;
|
|
689
|
+
* - 选中 staging 时 lightUpdate.sh 带 staging 参数(拉 staging/ 前缀);下载之前就失败(staging 的 composer.lock 变了、
|
|
690
|
+
* 下载失败、包有问题)→ 这一次改走 stable;部署后回滚、TSE 忙、别的 light-update 在跑 → 不改走;
|
|
691
|
+
* - 版本比较见 versionGtChannel:带「-」后缀才按 semver(1.9.83-staging.15 < 1.9.83,stable 发版后测试机自动跟上正式版),
|
|
692
|
+
* 纯 x.y.z 之间和原来的 versionGt 完全一样。
|
|
693
|
+
*/
|
|
694
|
+
async function lightUpdateStaging(deviceId) {
|
|
695
|
+
const S3 = 'https://aaden-backend.s3.eu-central-1.amazonaws.com'
|
|
696
|
+
const tag = 'light-update[staging]'
|
|
697
|
+
try {
|
|
698
|
+
const localVersion = getBackendVersionOffline()
|
|
699
|
+
let stableVersion = null
|
|
700
|
+
try {
|
|
701
|
+
stableVersion = (await hillo.get(S3 + '/raw/package.json')).version
|
|
702
|
+
} catch (e) {
|
|
703
|
+
console.log('取 stable 清单失败:', e.message)
|
|
704
|
+
}
|
|
705
|
+
if (!releaseChannel.parseSemver(stableVersion)) {
|
|
706
|
+
process.exitCode = 1
|
|
707
|
+
await logAction(tag + ' 中止:取不到 stable 清单(' + stableVersion + ')', 1)
|
|
708
|
+
return
|
|
709
|
+
}
|
|
710
|
+
let stagingVersion = null
|
|
711
|
+
try {
|
|
712
|
+
stagingVersion = (await hillo.get(S3 + '/staging/raw/package.json')).version
|
|
713
|
+
} catch (e) {
|
|
714
|
+
console.log('取 staging 清单失败,这次按 stable:', e.message)
|
|
715
|
+
}
|
|
716
|
+
if (stagingVersion && !releaseChannel.parseSemver(stagingVersion)) {
|
|
717
|
+
console.log(('staging 清单版本格式不对(' + stagingVersion + '),这次按 stable').yellow)
|
|
718
|
+
stagingVersion = null
|
|
719
|
+
}
|
|
720
|
+
const useStaging = !!stagingVersion && versionGtChannel(stagingVersion, stableVersion)
|
|
721
|
+
const target = useStaging ? stagingVersion : stableVersion
|
|
722
|
+
if (!versionGtChannel(target, localVersion)) {
|
|
723
|
+
console.log((tag + ' 已是最新或更高 (本机 ' + localVersion + ' / stable ' + stableVersion + ' / staging ' + (stagingVersion || '-') + '),跳过').green)
|
|
724
|
+
return
|
|
725
|
+
}
|
|
726
|
+
const maxVersion = await getDeviceMaxVersion(deviceId)
|
|
727
|
+
if (maxVersion && maxVersion !== '-1' && versionGtChannel(target, maxVersion)) {
|
|
728
|
+
const msg = tag + ' 跳过:灰度闸门 target=' + target + ' > maxVersion=' + maxVersion
|
|
729
|
+
console.log(msg.yellow)
|
|
730
|
+
await logAction(msg, 0)
|
|
731
|
+
return
|
|
732
|
+
}
|
|
733
|
+
|
|
734
|
+
let run = await runLightUpdateScript(useStaging ? 'staging' : 'stable')
|
|
735
|
+
let channelUsed = useStaging ? 'staging' : 'stable'
|
|
736
|
+
// 下载之前就失败的这几种,这一次改走 stable(前提是 stable 比本机新);部署后回滚 / TSE 忙等不改走。
|
|
737
|
+
// SKIP_DOWNLOAD_PARTIAL(半截包留着下次续传)退出码是 0,本来就不回落,不列在这里
|
|
738
|
+
const FALLBACK = ['SKIP_STAGING_LOCK', 'ABORT_DOWNLOAD', 'ABORT_BAD_ARTIFACT', 'ABORT_NO_VERSION']
|
|
739
|
+
if (useStaging && run.code !== 0 && FALLBACK.some(s => run.status.indexOf(s) === 0) &&
|
|
740
|
+
versionGtChannel(stableVersion, localVersion)) {
|
|
741
|
+
console.log(('staging 这次没走通(' + run.status + '),改走 stable').yellow)
|
|
742
|
+
run = await runLightUpdateScript('stable')
|
|
743
|
+
channelUsed = 'stable(staging 回落)'
|
|
744
|
+
}
|
|
745
|
+
const suffix = run.status ? ' [' + run.status + ']' : ''
|
|
746
|
+
const newVersion = readBackendVersionFresh()
|
|
747
|
+
if (run.code !== 0) {
|
|
748
|
+
process.exitCode = run.code || 1
|
|
749
|
+
await logAction(tag + ' 失败/未部署 (仍 ' + newVersion + ',通道 ' + channelUsed + ')' + suffix, run.code || 1)
|
|
750
|
+
} else if (newVersion !== localVersion) {
|
|
751
|
+
await logAction(tag + ' 完成 ' + localVersion + ' -> ' + newVersion + '(通道 ' + channelUsed + ')' + suffix, 0)
|
|
752
|
+
} else {
|
|
753
|
+
await logAction(tag + ' 无实际变化 (' + newVersion + ',通道 ' + channelUsed + ')' + suffix, 0)
|
|
754
|
+
}
|
|
755
|
+
} catch (e) {
|
|
756
|
+
console.error(e)
|
|
757
|
+
try { await logAction(tag + ' 异常: ' + (e.message || e), 1) } catch (_) {}
|
|
758
|
+
}
|
|
759
|
+
}
|
|
760
|
+
|
|
761
|
+
/** staging 通道跑一次 lightUpdate.sh:channel = staging | stable,带 semver 参数(脚本里只在版本带「-」时才按 semver 比)。返回退出码和末行状态 */
|
|
762
|
+
async function runLightUpdateScript(channel) {
|
|
763
|
+
try { fs.unlinkSync(LU_STATUS_FILE) } catch (e) { /* 本来就没有,正常 */ }
|
|
764
|
+
const command = [
|
|
765
|
+
...getPrivilegeCMD('AadenDeploy/lightUpdate.sh'),
|
|
766
|
+
'cd AadenDeploy',
|
|
767
|
+
'./lightUpdate.sh ' + (channel === 'staging' ? 'staging' : 'stable') + ' semver'
|
|
768
|
+
]
|
|
769
|
+
const {code} = await cli.executeInShell(command, program.debug)
|
|
770
|
+
return {code, status: readLightUpdateStatus()}
|
|
771
|
+
}
|
|
772
|
+
|
|
773
|
+
/** 测试机切后端发布通道:写 /etc/aaden/config/release-channel(root 写、644);不带参数 = 查看当前通道 */
|
|
774
|
+
async function setReleaseChannel(channel) {
|
|
775
|
+
const deviceId = await getCurrentDeviceId()
|
|
776
|
+
const test = releaseChannel.isTestDevice(deviceId)
|
|
777
|
+
if (!channel) {
|
|
778
|
+
console.log('设备 ' + deviceId + '(' + (test ? '测试机' : '不是测试机,通道文件不生效') + '),通道文件:' + releaseChannel.readChannelFile())
|
|
779
|
+
return
|
|
780
|
+
}
|
|
781
|
+
const v = String(channel).trim().toLowerCase()
|
|
782
|
+
if (v !== releaseChannel.STAGING && v !== releaseChannel.STABLE) {
|
|
783
|
+
console.log('通道只能是 staging 或 stable'.red)
|
|
784
|
+
process.exitCode = 1
|
|
785
|
+
return
|
|
786
|
+
}
|
|
787
|
+
if (v === releaseChannel.STAGING && !test) {
|
|
788
|
+
console.log(('设备 ' + deviceId + ' 不是测试机(只有 1 和 9020–9099 能切 staging)').red)
|
|
789
|
+
process.exitCode = 1
|
|
790
|
+
return
|
|
791
|
+
}
|
|
792
|
+
const file = releaseChannel.CHANNEL_FILE
|
|
793
|
+
// 密码走 sudo -S 的标准输入,写文件放在 bash -c 里(不能把内容也从管道喂给 sudo:会被当成密码吃掉);v 只可能是 staging / stable
|
|
794
|
+
const {code} = await cli.executeInShell([
|
|
795
|
+
'echo "Innerken322." | sudo -S -p "" bash -c "mkdir -p ' + path.dirname(file) + ' && printf \'%s\\n\' ' + v + ' > ' + file + ' && chmod 644 ' + file + '"',
|
|
796
|
+
], program.debug)
|
|
797
|
+
if (code !== 0) {
|
|
798
|
+
process.exitCode = code
|
|
799
|
+
console.log('写通道文件失败'.red)
|
|
800
|
+
return
|
|
801
|
+
}
|
|
802
|
+
console.log(('后端发布通道已设为 ' + v + '(下一次 light-update 生效)' +
|
|
803
|
+
(v === releaseChannel.STABLE ? ';本机版本如果比 stable 新,按现有规则不会降级' : '')).green)
|
|
804
|
+
await logAction('release-channel → ' + v, 0)
|
|
805
|
+
}
|
|
806
|
+
|
|
807
|
+
/**
|
|
808
|
+
* staging 通道用的版本比较:两个版本里至少一个带「-」后缀(1.9.83-staging.15)才按 semver 比;
|
|
809
|
+
* 纯 x.y.z 之间一律走原来的 versionGt,结果和改动前逐个一致。
|
|
810
|
+
*/
|
|
811
|
+
function versionGtChannel(a, b) {
|
|
812
|
+
return (String(a).indexOf('-') >= 0 || String(b).indexOf('-') >= 0)
|
|
813
|
+
? releaseChannel.semverGt(a, b)
|
|
814
|
+
: versionGt(a, b)
|
|
815
|
+
}
|
|
816
|
+
|
|
675
817
|
/** 比较点分数字版本,a > b 返回 true。'1.9.61' > '1.7.908' === true。 */
|
|
676
818
|
function versionGt(a, b) {
|
|
677
819
|
const pa = String(a).split('.').map(n => parseInt(n) || 0)
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 后端发布通道(staging 预发布)。只给测试机用:ik0001(设备号 1)和测试门店农场 9020–9099。
|
|
3
|
+
*
|
|
4
|
+
* - 通道写在本机文件 /etc/aaden/config/release-channel(`aaden-cli release-channel staging|stable` 写,root 写、644);
|
|
5
|
+
* - light-update 只有在「文件里写的是 staging」并且「本机设备号是测试机」时才走 staging,其他一律是原来的 stable 路径;
|
|
6
|
+
* - staging 的包由 php 仓库 staging 分支的 workflow 发到 S3 的 staging/ 前缀,版本号形如 1.9.83-staging.15。
|
|
7
|
+
*
|
|
8
|
+
* 不是测试机、没有文件、文件内容不是 staging:light-update 的代码路径和请求的 URL 跟原来完全一样(只多读一次本地文件)。
|
|
9
|
+
* 兼容 node 10(门店上最老的 node),不用可选链等新语法。
|
|
10
|
+
*/
|
|
11
|
+
const fs = require('fs')
|
|
12
|
+
|
|
13
|
+
const CHANNEL_FILE = '/etc/aaden/config/release-channel'
|
|
14
|
+
const STAGING = 'staging'
|
|
15
|
+
const STABLE = 'stable'
|
|
16
|
+
|
|
17
|
+
/** 测试机:1 号机,或测试门店农场号段 9020–9099(和云端 TestStores 默认值一致,写死在这里,不信任何外部配置) */
|
|
18
|
+
function isTestDevice(deviceId) {
|
|
19
|
+
const id = parseInt(String(deviceId).trim(), 10)
|
|
20
|
+
if (isNaN(id)) return false
|
|
21
|
+
return id === 1 || (id >= 9020 && id <= 9099)
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/** 本机文件里写的通道;读不到、不认识一律 stable */
|
|
25
|
+
function readChannelFile() {
|
|
26
|
+
try {
|
|
27
|
+
const v = fs.readFileSync(CHANNEL_FILE, 'utf8').trim().toLowerCase()
|
|
28
|
+
return v === STAGING ? STAGING : STABLE
|
|
29
|
+
} catch (e) {
|
|
30
|
+
return STABLE
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* semver 解析:只认 x.y.z 和 x.y.z-<预发布标识>。不合法返回 null。
|
|
36
|
+
* 预发布标识按 semver:逐段比,纯数字段按数值、数字段 < 字母段,段少的小;有预发布标识的 < 没有的。
|
|
37
|
+
*/
|
|
38
|
+
function parseSemver(v) {
|
|
39
|
+
const m = /^(\d+)\.(\d+)\.(\d+)(?:-([0-9A-Za-z.-]+))?$/.exec(String(v || '').trim())
|
|
40
|
+
if (!m) return null
|
|
41
|
+
return {core: [parseInt(m[1], 10), parseInt(m[2], 10), parseInt(m[3], 10)], pre: m[4] ? m[4].split('.') : []}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** a 比 b 大返回 1,小返回 -1,相等 0;任一不合法返回 null(调用方按「不更新」处理) */
|
|
45
|
+
function semverCompare(a, b) {
|
|
46
|
+
const pa = parseSemver(a), pb = parseSemver(b)
|
|
47
|
+
if (!pa || !pb) return null
|
|
48
|
+
for (let i = 0; i < 3; i++) {
|
|
49
|
+
if (pa.core[i] !== pb.core[i]) return pa.core[i] > pb.core[i] ? 1 : -1
|
|
50
|
+
}
|
|
51
|
+
if (pa.pre.length === 0 && pb.pre.length === 0) return 0
|
|
52
|
+
if (pa.pre.length === 0) return 1
|
|
53
|
+
if (pb.pre.length === 0) return -1
|
|
54
|
+
for (let i = 0; i < Math.max(pa.pre.length, pb.pre.length); i++) {
|
|
55
|
+
const x = pa.pre[i], y = pb.pre[i]
|
|
56
|
+
if (x === undefined) return -1
|
|
57
|
+
if (y === undefined) return 1
|
|
58
|
+
const xn = /^\d+$/.test(x), yn = /^\d+$/.test(y)
|
|
59
|
+
if (xn && yn) {
|
|
60
|
+
const d = parseInt(x, 10) - parseInt(y, 10)
|
|
61
|
+
if (d !== 0) return d > 0 ? 1 : -1
|
|
62
|
+
} else if (xn !== yn) {
|
|
63
|
+
return xn ? -1 : 1
|
|
64
|
+
} else if (x !== y) {
|
|
65
|
+
return x > y ? 1 : -1
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
return 0
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/** a > b;不合法按 false(不更新) */
|
|
72
|
+
function semverGt(a, b) {
|
|
73
|
+
return semverCompare(a, b) === 1
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
module.exports = {CHANNEL_FILE, STAGING, STABLE, isTestDevice, readChannelFile, parseSemver, semverCompare, semverGt}
|
package/package.json
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "aaden-cli",
|
|
3
|
-
"version": "1.6.
|
|
3
|
+
"version": "1.6.122-rc.0",
|
|
4
4
|
"description": "",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"scripts": {
|
|
7
|
-
"test": "node test/snap-version.test.js",
|
|
7
|
+
"test": "node test/snap-version.test.js && python3 test/app_version_test.py",
|
|
8
8
|
"update": "git add . && release-it --ci"
|
|
9
9
|
},
|
|
10
10
|
"bin": {
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
# python3 test/app_version_test.py:appCenterUsage.app_version 在临时 docroot 上的行为
|
|
2
|
+
import importlib.util, json, os, sys, tempfile
|
|
3
|
+
sys.dont_write_bytecode = True # 不在仓库里留 __pycache__
|
|
4
|
+
|
|
5
|
+
spec = importlib.util.spec_from_file_location('usage', os.path.join(os.path.dirname(__file__), '..', 'AadenDeploy', 'appCenterUsage.py'))
|
|
6
|
+
usage = importlib.util.module_from_spec(spec)
|
|
7
|
+
sys.argv = [sys.argv[0], '/nonexistent']
|
|
8
|
+
spec.loader.exec_module(usage)
|
|
9
|
+
|
|
10
|
+
root = tempfile.mkdtemp()
|
|
11
|
+
usage.DOCROOT = root
|
|
12
|
+
def mk(rel, html, **files):
|
|
13
|
+
d = os.path.join(root, rel); os.makedirs(d)
|
|
14
|
+
open(os.path.join(d, 'index.html'), 'w').write(html)
|
|
15
|
+
for name, content in files.items():
|
|
16
|
+
open(os.path.join(d, name.replace('_', '.', 1) if name != 'appcenter_manifest' else '.appcenter-manifest'), 'w').write(content)
|
|
17
|
+
|
|
18
|
+
# AppCenter 应用:有 version.json 和 .appcenter-manifest
|
|
19
|
+
mk('AppCenter/AdminV3', '<script src=js/chunk-vendors.11111111.js></script><script src=js/app.3960d167.js></script>',
|
|
20
|
+
version_json=json.dumps({'version': '3.0.2', 'commit': 'abcdef123456'}), appcenter_manifest='x index.html\n')
|
|
21
|
+
v = usage.app_version('AppCenter/AdminV3')
|
|
22
|
+
assert v['version'] == '3.0.2' and v['commit'] == 'abcdef123456', v
|
|
23
|
+
assert len(v['treeSha']) == 64 and v['mainJs'] == 'js/app.3960d167.js' and len(v['fingerprint']) == 16, v
|
|
24
|
+
|
|
25
|
+
# 根目录老前端:没有 version.json,只有指纹;老版 Vite 入口
|
|
26
|
+
mk('Board', '<script type="module" crossorigin src="./assets/index.ce3abdc3.js"></script>')
|
|
27
|
+
v = usage.app_version('Board')
|
|
28
|
+
assert v['version'] == 'unknown' and 'treeSha' not in v and v['mainJs'] == 'assets/index.ce3abdc3.js', v
|
|
29
|
+
|
|
30
|
+
# 新版 Vite 入口 + package.json 兜底
|
|
31
|
+
mk('KDS3', '<script src="/assets/index-AbCd1234.js"></script>', package_json='{"version":"0.6.1"}')
|
|
32
|
+
v = usage.app_version('KDS3')
|
|
33
|
+
assert v['version'] == '0.6.1' and v['mainJs'] == 'assets/index-AbCd1234.js', v
|
|
34
|
+
|
|
35
|
+
# 坏掉的 version.json 不影响其它字段;没有 index.html 只报 unknown
|
|
36
|
+
mk('Broken', '<p>x</p>', version_json='{not json')
|
|
37
|
+
v = usage.app_version('Broken')
|
|
38
|
+
assert v['version'] == 'unknown' and 'fingerprint' in v and 'mainJs' not in v, v
|
|
39
|
+
assert usage.app_version('Missing') == {'version': 'unknown'}
|
|
40
|
+
|
|
41
|
+
print('app-version: ok')
|
|
42
|
+
|
|
43
|
+
# 兜底:内部抛任何异常都只返回 unknown
|
|
44
|
+
orig = usage._app_version
|
|
45
|
+
usage._app_version = lambda rel: 1 / 0
|
|
46
|
+
assert usage.app_version('AppCenter/AdminV3') == {'version': 'unknown'}
|
|
47
|
+
usage._app_version = orig
|
|
48
|
+
print('app-version fallback: ok')
|