Compare commits
2
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6b04d8dd0a | ||
|
|
fae8225299 |
@@ -98,7 +98,7 @@ jobs:
|
|||||||
RELEASE_SIGNING_KEY_B64: ${{ secrets.RELEASE_SIGNING_KEY_B64 }}
|
RELEASE_SIGNING_KEY_B64: ${{ secrets.RELEASE_SIGNING_KEY_B64 }}
|
||||||
RELEASE_VERSION: ${{ steps.release_meta.outputs.version }}
|
RELEASE_VERSION: ${{ steps.release_meta.outputs.version }}
|
||||||
KAIDI_TRUSTED_RELEASE_PUBLIC_KEY_SHA256: ${{ vars.KAIDI_RELEASE_PUBLIC_KEY_SHA256 }}
|
KAIDI_TRUSTED_RELEASE_PUBLIC_KEY_SHA256: ${{ vars.KAIDI_RELEASE_PUBLIC_KEY_SHA256 }}
|
||||||
KAIDI_RELEASE_NOTES: Kaidi Finance ${{ steps.release_meta.outputs.ref }}
|
KAIDI_REQUIRE_RELEASE_NOTES: 'true'
|
||||||
KAIDI_SOURCE_REVISION: ${{ steps.release_meta.outputs.revision }}
|
KAIDI_SOURCE_REVISION: ${{ steps.release_meta.outputs.revision }}
|
||||||
KAIDI_SOURCE_REF: ${{ steps.release_meta.outputs.ref }}
|
KAIDI_SOURCE_REF: ${{ steps.release_meta.outputs.ref }}
|
||||||
KAIDI_SOURCE_DIRTY: 'false'
|
KAIDI_SOURCE_DIRTY: 'false'
|
||||||
|
|||||||
@@ -83,7 +83,7 @@ PostgreSQL 18 兼容工作继续冻结。
|
|||||||
先在宝塔面板停止并删除当前错误的 Java 项目,再执行:
|
先在宝塔面板停止并删除当前错误的 Java 项目,再执行:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
curl -fsSL https://git.awaioi.com/ERP-Team/kaidi/releases/download/v1.0.0-preview.45/install.sh | sudo env KAIDI_APP_PORT=18080 bash
|
curl -fsSL https://git.awaioi.com/ERP-Team/kaidi/releases/download/v1.0.0-preview.47/install.sh | sudo env KAIDI_APP_PORT=18080 bash
|
||||||
```
|
```
|
||||||
|
|
||||||
该命令只安装程序运行所需的 systemd 单元,自动创建 `kaidi` 用户并检测现有 Java 17(包括
|
该命令只安装程序运行所需的 systemd 单元,自动创建 `kaidi` 用户并检测现有 Java 17(包括
|
||||||
@@ -96,7 +96,7 @@ curl -fsSL https://git.awaioi.com/ERP-Team/kaidi/releases/download/v1.0.0-previe
|
|||||||
随后执行一键清理:
|
随后执行一键清理:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
curl -fsSL 'https://git.awaioi.com/ERP-Team/kaidi/releases/download/v1.0.0-preview.45/purge.sh' | sudo env KAIDI_PURGE_CONFIRM=DELETE_LOCAL_KAIDI_INSTALLATION bash
|
curl -fsSL 'https://git.awaioi.com/ERP-Team/kaidi/releases/download/v1.0.0-preview.47/purge.sh' | sudo env KAIDI_PURGE_CONFIRM=DELETE_LOCAL_KAIDI_INSTALLATION bash
|
||||||
```
|
```
|
||||||
|
|
||||||
上面是一条完整命令:脚本通过管道直接交给 root 执行,不创建临时安装文件,避免终端自动换行导致 `-o` 参数丢失。
|
上面是一条完整命令:脚本通过管道直接交给 root 执行,不创建临时安装文件,避免终端自动换行导致 `-o` 参数丢失。
|
||||||
@@ -117,9 +117,9 @@ Spring Boot 项目。数据库、JDK、Nginx 和宝塔本身都由运维人员
|
|||||||
|
|
||||||
下载地址:
|
下载地址:
|
||||||
|
|
||||||
`https://git.awaioi.com/ERP-Team/kaidi/releases/download/v1.0.0-preview.45/kaidi-finance-1.0.0-preview.45.tar.gz`
|
`https://git.awaioi.com/ERP-Team/kaidi/releases/download/v1.0.0-preview.47/kaidi-finance-1.0.0-preview.47.tar.gz`
|
||||||
|
|
||||||
校验文件:`https://git.awaioi.com/ERP-Team/kaidi/releases/download/v1.0.0-preview.45/SHA256SUMS`
|
校验文件:`https://git.awaioi.com/ERP-Team/kaidi/releases/download/v1.0.0-preview.47/SHA256SUMS`
|
||||||
|
|
||||||
服务器要求:Linux、Java 17(宝塔项目选择 JDK 17)、可访问外部 MySQL 8.4.x;**systemd 一键安装**还需要
|
服务器要求:Linux、Java 17(宝塔项目选择 JDK 17)、可访问外部 MySQL 8.4.x;**systemd 一键安装**还需要
|
||||||
systemd/systemd-analyze,**宝塔手动部署**不要求 systemd。32 位 Linux 需要宿主机
|
systemd/systemd-analyze,**宝塔手动部署**不要求 systemd。32 位 Linux 需要宿主机
|
||||||
@@ -136,7 +136,7 @@ systemd/systemd-analyze,**宝塔手动部署**不要求 systemd。32 位 Linux
|
|||||||
必须直接位于 `RELEASE_ROOT`,不能再嵌套一层目录。
|
必须直接位于 `RELEASE_ROOT`,不能再嵌套一层目录。
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
VERSION=1.0.0-preview.45
|
VERSION=1.0.0-preview.47
|
||||||
APP_ROOT=/www/wwwroot/kaidi
|
APP_ROOT=/www/wwwroot/kaidi
|
||||||
RELEASE_ROOT="$APP_ROOT/releases/$VERSION"
|
RELEASE_ROOT="$APP_ROOT/releases/$VERSION"
|
||||||
sudo install -d -m 0755 "$RELEASE_ROOT"
|
sudo install -d -m 0755 "$RELEASE_ROOT"
|
||||||
@@ -245,7 +245,7 @@ MySQL 8.4;提前准备外部 MySQL,完成上述向导即可。systemd 在线
|
|||||||
`KAIDI_PURGE_DELETE_BACKUP=true`,实现本地受管文件和恢复包一并清除:
|
`KAIDI_PURGE_DELETE_BACKUP=true`,实现本地受管文件和恢复包一并清除:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
curl -fsSL 'https://git.awaioi.com/ERP-Team/kaidi/releases/download/v1.0.0-preview.45/purge.sh' | sudo env KAIDI_PURGE_CONFIRM=DELETE_LOCAL_KAIDI_INSTALLATION KAIDI_PURGE_DELETE_BACKUP=true bash
|
curl -fsSL 'https://git.awaioi.com/ERP-Team/kaidi/releases/download/v1.0.0-preview.47/purge.sh' | sudo env KAIDI_PURGE_CONFIRM=DELETE_LOCAL_KAIDI_INSTALLATION KAIDI_PURGE_DELETE_BACKUP=true bash
|
||||||
```
|
```
|
||||||
|
|
||||||
执行前先在宝塔停止并删除 `kaidi-finance` Java 项目;宝塔面板元数据属于外部资源,必须由面板先停用,
|
执行前先在宝塔停止并删除 `kaidi-finance` Java 项目;宝塔面板元数据属于外部资源,必须由面板先停用,
|
||||||
@@ -268,11 +268,13 @@ Actions 页面显示 “No matching online runner”,先启动并注册该标
|
|||||||
|
|
||||||
工作流先建立不可见草稿,再显式上传并核对 10 个资产的名称和大小,最后才发布为 `/releases/latest`。Gitea 的
|
工作流先建立不可见草稿,再显式上传并核对 10 个资产的名称和大小,最后才发布为 `/releases/latest`。Gitea 的
|
||||||
`latest` 会排除 `prerelease=true`,因此即使 tag 名含 `preview`,发布记录的 `prerelease` 也固定为 `false`;
|
`latest` 会排除 `prerelease=true`,因此即使 tag 名含 `preview`,发布记录的 `prerelease` 也固定为 `false`;
|
||||||
Preview 属性由 SemVer 版本名表达。之后推送 tag 即会构建、测试、签名并发布:
|
Preview 属性由 SemVer 版本名表达。每个版本必须先在 `release-notes/<version>.md` 写好面向用户的更新日志。
|
||||||
|
打包器会把同一份内容写入签名 `release-manifest.json`,后台历史从该清单读取;发布脚本也会用它生成
|
||||||
|
Gitea Release 正文,避免三处内容不一致。之后推送 tag 即会构建、测试、签名并发布:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
git tag v1.0.0-preview.45
|
git tag v1.0.0-preview.47
|
||||||
git push origin v1.0.0-preview.45
|
git push origin v1.0.0-preview.47
|
||||||
```
|
```
|
||||||
|
|
||||||
在线更新仍使用独立的 TDesign 页面:系统管理员进入“系统治理 → 系统更新”。更新源由 root 在
|
在线更新仍使用独立的 TDesign 页面:系统管理员进入“系统治理 → 系统更新”。更新源由 root 在
|
||||||
@@ -324,7 +326,7 @@ cat /var/lib/kaidi-update/status.json
|
|||||||
```bash
|
```bash
|
||||||
KAIDI_RELEASE_SIGNING_KEY=/secure/release-signing-private.pem \
|
KAIDI_RELEASE_SIGNING_KEY=/secure/release-signing-private.pem \
|
||||||
KAIDI_TRUSTED_RELEASE_PUBLIC_KEY_SHA256=807c6aec1dc3f7ce494db16aa9d763c66f292033c38f328afd0390d2715a8cd9 \
|
KAIDI_TRUSTED_RELEASE_PUBLIC_KEY_SHA256=807c6aec1dc3f7ce494db16aa9d763c66f292033c38f328afd0390d2715a8cd9 \
|
||||||
./scripts/package-release.sh 1.0.0-preview.45
|
./scripts/package-release.sh 1.0.0-preview.47
|
||||||
KAIDI_TRUSTED_RELEASE_PUBLIC_KEY_SHA256=807c6aec1dc3f7ce494db16aa9d763c66f292033c38f328afd0390d2715a8cd9 \
|
KAIDI_TRUSTED_RELEASE_PUBLIC_KEY_SHA256=807c6aec1dc3f7ce494db16aa9d763c66f292033c38f328afd0390d2715a8cd9 \
|
||||||
./scripts/verify-release.sh dist/release
|
./scripts/verify-release.sh dist/release
|
||||||
```
|
```
|
||||||
|
|||||||
@@ -77,6 +77,18 @@ public class AuditService {
|
|||||||
*/
|
*/
|
||||||
public String recordSystemUpdateTerminal(String updateRequestId, String updateAction, String state,
|
public String recordSystemUpdateTerminal(String updateRequestId, String updateAction, String state,
|
||||||
String targetVersion, String message, Instant updatedAt) {
|
String targetVersion, String message, Instant updatedAt) {
|
||||||
|
return recordSystemUpdateTerminal(updateRequestId, updateAction, state, targetVersion, message, updatedAt,
|
||||||
|
null, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Persists a terminal update event together with the release metadata that was verified before the switch.
|
||||||
|
* Keeping the signed release notes in the audit snapshot lets the history page work after a restart or when
|
||||||
|
* the release host is temporarily unavailable.
|
||||||
|
*/
|
||||||
|
public String recordSystemUpdateTerminal(String updateRequestId, String updateAction, String state,
|
||||||
|
String targetVersion, String message, Instant updatedAt,
|
||||||
|
String releaseNotes, Instant publishedAt) {
|
||||||
String normalizedState = normalizeTerminalState(state);
|
String normalizedState = normalizeTerminalState(state);
|
||||||
String normalizedVersion = clean(targetVersion, 128);
|
String normalizedVersion = clean(targetVersion, 128);
|
||||||
if (normalizedState == null || normalizedVersion == null || updatedAt == null) return null;
|
if (normalizedState == null || normalizedVersion == null || updatedAt == null) return null;
|
||||||
@@ -110,6 +122,9 @@ public class AuditService {
|
|||||||
terminal.put("targetVersion", normalizedVersion);
|
terminal.put("targetVersion", normalizedVersion);
|
||||||
terminal.put("message", terminalReason == null ? "" : terminalReason);
|
terminal.put("message", terminalReason == null ? "" : terminalReason);
|
||||||
terminal.put("updatedAt", updatedAt);
|
terminal.put("updatedAt", updatedAt);
|
||||||
|
String normalizedNotes = clean(releaseNotes, 4000);
|
||||||
|
if (normalizedNotes != null) terminal.put("releaseNotes", normalizedNotes);
|
||||||
|
if (publishedAt != null) terminal.put("publishedAt", publishedAt);
|
||||||
if (normalizedAction != null) terminal.put("action", normalizedAction);
|
if (normalizedAction != null) terminal.put("action", normalizedAction);
|
||||||
if (normalizedRequestId != null) terminal.put("requestId", normalizedRequestId);
|
if (normalizedRequestId != null) terminal.put("requestId", normalizedRequestId);
|
||||||
|
|
||||||
|
|||||||
+14
-7
@@ -444,7 +444,8 @@ public class SystemUpdateApplicationService {
|
|||||||
nonNegativeLong(root, "downloadedBytes"), nonNegativeLong(root, "totalBytes"),
|
nonNegativeLong(root, "downloadedBytes"), nonNegativeLong(root, "totalBytes"),
|
||||||
nonNegativeLong(root, "bytesPerSecond"), boundedInteger(root, "downloadPercent", 0, 100),
|
nonNegativeLong(root, "bytesPerSecond"), boundedInteger(root, "downloadPercent", 0, 100),
|
||||||
boundedInteger(root, "restartExpectedSeconds", 0, 300),
|
boundedInteger(root, "restartExpectedSeconds", 0, 300),
|
||||||
boundedText(root, "requestId", 64), updateAction(root.path("action").asText(null)));
|
boundedText(root, "requestId", 64), updateAction(root.path("action").asText(null)),
|
||||||
|
boundedText(root, "releaseNotes", 4000), parseInstant(root.path("publishedAt").asText(null)));
|
||||||
} catch (IOException exception) {
|
} catch (IOException exception) {
|
||||||
return new UpdateStatus("UNKNOWN", "更新状态读取失败", null, null);
|
return new UpdateStatus("UNKNOWN", "更新状态读取失败", null, null);
|
||||||
}
|
}
|
||||||
@@ -466,9 +467,13 @@ public class SystemUpdateApplicationService {
|
|||||||
&& authorizationService.hasPermission("admin:update:execute")) {
|
&& authorizationService.hasPermission("admin:update:execute")) {
|
||||||
actions.add(ready ? "INSTALL" : "DOWNLOAD");
|
actions.add(ready ? "INSTALL" : "DOWNLOAD");
|
||||||
}
|
}
|
||||||
|
String releaseNotes = manifest != null && manifest.releaseNotes() != null && !manifest.releaseNotes().isBlank()
|
||||||
|
? manifest.releaseNotes() : status.releaseNotes();
|
||||||
|
Instant publishedAt = manifest != null && manifest.publishedAt() != null
|
||||||
|
? manifest.publishedAt() : status.publishedAt();
|
||||||
return new SystemUpdateView(enabled, current, candidateVersion,
|
return new SystemUpdateView(enabled, current, candidateVersion,
|
||||||
available, status.state(), status.message(), manifest == null ? null : manifest.releaseNotes(),
|
available, status.state(), status.message(), releaseNotes,
|
||||||
manifest == null ? null : manifest.publishedAt(), lastCheckedAt, status.updatedAt(),
|
publishedAt, lastCheckedAt, status.updatedAt(),
|
||||||
status.downloadedBytes(), status.totalBytes(), status.bytesPerSecond(), status.downloadPercent(),
|
status.downloadedBytes(), status.totalBytes(), status.bytesPerSecond(), status.downloadPercent(),
|
||||||
status.restartExpectedSeconds(), readUpdateEvents(), List.copyOf(actions));
|
status.restartExpectedSeconds(), readUpdateEvents(), List.copyOf(actions));
|
||||||
}
|
}
|
||||||
@@ -672,7 +677,8 @@ public class SystemUpdateApplicationService {
|
|||||||
if (fingerprint.equals(lastTerminalAuditFingerprint)) return;
|
if (fingerprint.equals(lastTerminalAuditFingerprint)) return;
|
||||||
try {
|
try {
|
||||||
String persistedKey = auditService.recordSystemUpdateTerminal(status.requestId(), status.action(),
|
String persistedKey = auditService.recordSystemUpdateTerminal(status.requestId(), status.action(),
|
||||||
status.state(), status.targetVersion(), status.message(), status.updatedAt());
|
status.state(), status.targetVersion(), status.message(), status.updatedAt(),
|
||||||
|
status.releaseNotes(), status.publishedAt());
|
||||||
if (persistedKey != null) lastTerminalAuditFingerprint = fingerprint;
|
if (persistedKey != null) lastTerminalAuditFingerprint = fingerprint;
|
||||||
} catch (RuntimeException exception) {
|
} catch (RuntimeException exception) {
|
||||||
log.warn("系统更新终态审计写入失败:state={}, targetVersion={}", status.state(),
|
log.warn("系统更新终态审计写入失败:state={}, targetVersion={}", status.state(),
|
||||||
@@ -699,14 +705,15 @@ public class SystemUpdateApplicationService {
|
|||||||
private record UpdateStatus(String state, String message, String targetVersion, Instant updatedAt,
|
private record UpdateStatus(String state, String message, String targetVersion, Instant updatedAt,
|
||||||
Long downloadedBytes, Long totalBytes, Long bytesPerSecond,
|
Long downloadedBytes, Long totalBytes, Long bytesPerSecond,
|
||||||
Integer downloadPercent, Integer restartExpectedSeconds,
|
Integer downloadPercent, Integer restartExpectedSeconds,
|
||||||
String requestId, String action) {
|
String requestId, String action, String releaseNotes, Instant publishedAt) {
|
||||||
private UpdateStatus(String state, String message, String targetVersion, Instant updatedAt) {
|
private UpdateStatus(String state, String message, String targetVersion, Instant updatedAt) {
|
||||||
this(state, message, targetVersion, updatedAt, null, null, null, null, null, null, null);
|
this(state, message, targetVersion, updatedAt, null, null, null, null, null, null, null, null, null);
|
||||||
}
|
}
|
||||||
|
|
||||||
private UpdateStatus(String state, String message, String targetVersion, Instant updatedAt,
|
private UpdateStatus(String state, String message, String targetVersion, Instant updatedAt,
|
||||||
String requestId, String action) {
|
String requestId, String action) {
|
||||||
this(state, message, targetVersion, updatedAt, null, null, null, null, null, requestId, action);
|
this(state, message, targetVersion, updatedAt, null, null, null, null, null, requestId, action,
|
||||||
|
null, null);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -27,7 +27,8 @@ class AuditServiceTest {
|
|||||||
"01M00000000000000000000092", "SYSTEM_UPDATE_REQUEST"))
|
"01M00000000000000000000092", "SYSTEM_UPDATE_REQUEST"))
|
||||||
.thenReturn(new AuditMapper.SystemUpdateAuditSource(
|
.thenReturn(new AuditMapper.SystemUpdateAuditSource(
|
||||||
"01M00000000000000000000092", "01M00000000000000000000001", "admin", "SYSTEM_ADMIN",
|
"01M00000000000000000000092", "01M00000000000000000000001", "admin", "SYSTEM_ADMIN",
|
||||||
null, null, "{\"state\":\"INSTALL_QUEUED\",\"targetVersion\":\"1.0.0-preview.44\"}",
|
null, null, "{\"state\":\"INSTALL_QUEUED\",\"targetVersion\":\"1.0.0-preview.44\","
|
||||||
|
+ "\"releaseNotes\":\"Preview update\"}",
|
||||||
"127.0.0.1", "fixture-agent"));
|
"127.0.0.1", "fixture-agent"));
|
||||||
when(mapper.insertSystemUpdateTerminal(any(), anyString(), any())).thenReturn(1);
|
when(mapper.insertSystemUpdateTerminal(any(), anyString(), any())).thenReturn(1);
|
||||||
AuditService service = new AuditService(mapper, ulids, new ObjectMapper().findAndRegisterModules(),
|
AuditService service = new AuditService(mapper, ulids, new ObjectMapper().findAndRegisterModules(),
|
||||||
@@ -35,9 +36,11 @@ class AuditServiceTest {
|
|||||||
Instant completedAt = Instant.parse("2026-08-19T00:10:00Z");
|
Instant completedAt = Instant.parse("2026-08-19T00:10:00Z");
|
||||||
|
|
||||||
String firstKey = service.recordSystemUpdateTerminal("01M00000000000000000000092", "INSTALL",
|
String firstKey = service.recordSystemUpdateTerminal("01M00000000000000000000092", "INSTALL",
|
||||||
"SUCCEEDED", "1.0.0-preview.44", "新版本已通过健康检查", completedAt);
|
"SUCCEEDED", "1.0.0-preview.44", "新版本已通过健康检查", completedAt,
|
||||||
|
"Preview update", Instant.parse("2026-08-16T00:00:00Z"));
|
||||||
String secondKey = service.recordSystemUpdateTerminal("01M00000000000000000000092", "INSTALL",
|
String secondKey = service.recordSystemUpdateTerminal("01M00000000000000000000092", "INSTALL",
|
||||||
"SUCCEEDED", "1.0.0-preview.44", "新版本已通过健康检查", completedAt);
|
"SUCCEEDED", "1.0.0-preview.44", "新版本已通过健康检查", completedAt,
|
||||||
|
"Preview update", Instant.parse("2026-08-16T00:00:00Z"));
|
||||||
|
|
||||||
assertEquals(firstKey, secondKey);
|
assertEquals(firstKey, secondKey);
|
||||||
assertTrue(firstKey.startsWith("SYSUPD:"));
|
assertTrue(firstKey.startsWith("SYSUPD:"));
|
||||||
@@ -52,6 +55,8 @@ class AuditServiceTest {
|
|||||||
assertTrue(persisted.beforeJson().contains("INSTALL_QUEUED"));
|
assertTrue(persisted.beforeJson().contains("INSTALL_QUEUED"));
|
||||||
assertTrue(persisted.afterJson().contains("1.0.0-preview.44"));
|
assertTrue(persisted.afterJson().contains("1.0.0-preview.44"));
|
||||||
assertTrue(persisted.afterJson().contains("SUCCEEDED"));
|
assertTrue(persisted.afterJson().contains("SUCCEEDED"));
|
||||||
|
assertTrue(persisted.afterJson().contains("Preview update"));
|
||||||
|
assertTrue(persisted.afterJson().contains("publishedAt"));
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
|||||||
+12
-5
@@ -294,21 +294,26 @@ class SystemUpdateApplicationServiceTest {
|
|||||||
Path statusFile = tempDir.resolve("terminal-status.json");
|
Path statusFile = tempDir.resolve("terminal-status.json");
|
||||||
Files.writeString(statusFile, """
|
Files.writeString(statusFile, """
|
||||||
{"state":"SUCCEEDED","message":"新版本已通过健康检查","targetVersion":"1.0.0-preview.44",
|
{"state":"SUCCEEDED","message":"新版本已通过健康检查","targetVersion":"1.0.0-preview.44",
|
||||||
"updatedAt":"2026-08-19T00:10:00Z","requestId":"01M00000000000000000000092",
|
"updatedAt":"2026-08-19T00:10:00Z","releaseNotes":"Preview update",
|
||||||
|
"publishedAt":"2026-08-16T00:00:00Z","requestId":"01M00000000000000000000092",
|
||||||
"action":"INSTALL"}
|
"action":"INSTALL"}
|
||||||
""");
|
""");
|
||||||
AuditService auditService = mock(AuditService.class);
|
AuditService auditService = mock(AuditService.class);
|
||||||
when(auditService.recordSystemUpdateTerminal("01M00000000000000000000092", "INSTALL", "SUCCEEDED",
|
when(auditService.recordSystemUpdateTerminal("01M00000000000000000000092", "INSTALL", "SUCCEEDED",
|
||||||
"1.0.0-preview.44", "新版本已通过健康检查", java.time.Instant.parse("2026-08-19T00:10:00Z")))
|
"1.0.0-preview.44", "新版本已通过健康检查", java.time.Instant.parse("2026-08-19T00:10:00Z"),
|
||||||
|
"Preview update", java.time.Instant.parse("2026-08-16T00:00:00Z")))
|
||||||
.thenReturn("SYSUPD:terminal");
|
.thenReturn("SYSUPD:terminal");
|
||||||
SystemUpdateApplicationService service = serviceForStatus(inbox, statusFile, auditService);
|
SystemUpdateApplicationService service = serviceForStatus(inbox, statusFile, auditService);
|
||||||
|
|
||||||
assertEquals("SUCCEEDED", service.status().state());
|
assertEquals("SUCCEEDED", service.status().state());
|
||||||
|
assertEquals("Preview update", service.status().releaseNotes());
|
||||||
|
assertEquals(java.time.Instant.parse("2026-08-16T00:00:00Z"), service.status().publishedAt());
|
||||||
assertEquals("SUCCEEDED", service.status().state());
|
assertEquals("SUCCEEDED", service.status().state());
|
||||||
|
|
||||||
verify(auditService, times(1)).recordSystemUpdateTerminal("01M00000000000000000000092", "INSTALL",
|
verify(auditService, times(1)).recordSystemUpdateTerminal("01M00000000000000000000092", "INSTALL",
|
||||||
"SUCCEEDED", "1.0.0-preview.44", "新版本已通过健康检查",
|
"SUCCEEDED", "1.0.0-preview.44", "新版本已通过健康检查",
|
||||||
java.time.Instant.parse("2026-08-19T00:10:00Z"));
|
java.time.Instant.parse("2026-08-19T00:10:00Z"), "Preview update",
|
||||||
|
java.time.Instant.parse("2026-08-16T00:00:00Z"));
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@@ -321,14 +326,14 @@ class SystemUpdateApplicationServiceTest {
|
|||||||
""");
|
""");
|
||||||
AuditService auditService = mock(AuditService.class);
|
AuditService auditService = mock(AuditService.class);
|
||||||
when(auditService.recordSystemUpdateTerminal(null, null, "SUCCEEDED", "1.0.0-preview.43",
|
when(auditService.recordSystemUpdateTerminal(null, null, "SUCCEEDED", "1.0.0-preview.43",
|
||||||
"Release 1.0.0-preview.43 is running", java.time.Instant.parse("2026-08-18T23:50:00Z")))
|
"Release 1.0.0-preview.43 is running", java.time.Instant.parse("2026-08-18T23:50:00Z"), null, null))
|
||||||
.thenReturn("SYSUPD:legacy");
|
.thenReturn("SYSUPD:legacy");
|
||||||
SystemUpdateApplicationService service = serviceForStatus(inbox, statusFile, auditService);
|
SystemUpdateApplicationService service = serviceForStatus(inbox, statusFile, auditService);
|
||||||
|
|
||||||
assertEquals("SUCCEEDED", service.status().state());
|
assertEquals("SUCCEEDED", service.status().state());
|
||||||
|
|
||||||
verify(auditService).recordSystemUpdateTerminal(null, null, "SUCCEEDED", "1.0.0-preview.43",
|
verify(auditService).recordSystemUpdateTerminal(null, null, "SUCCEEDED", "1.0.0-preview.43",
|
||||||
"Release 1.0.0-preview.43 is running", java.time.Instant.parse("2026-08-18T23:50:00Z"));
|
"Release 1.0.0-preview.43 is running", java.time.Instant.parse("2026-08-18T23:50:00Z"), null, null);
|
||||||
}
|
}
|
||||||
|
|
||||||
private SystemUpdateApplicationService serviceForStatus(Path inbox, Path statusFile, AuditService auditService) {
|
private SystemUpdateApplicationService serviceForStatus(Path inbox, Path statusFile, AuditService auditService) {
|
||||||
@@ -382,6 +387,8 @@ class SystemUpdateApplicationServiceTest {
|
|||||||
var checked = service.check();
|
var checked = service.check();
|
||||||
assertTrue(checked.updateAvailable());
|
assertTrue(checked.updateAvailable());
|
||||||
assertEquals("1.0.0-preview.2+build.7", checked.latestVersion());
|
assertEquals("1.0.0-preview.2+build.7", checked.latestVersion());
|
||||||
|
assertEquals("Preview update", checked.releaseNotes());
|
||||||
|
assertEquals(java.time.Instant.parse("2026-08-16T00:00:00Z"), checked.publishedAt());
|
||||||
assertTrue(checked.allowedActions().contains("DOWNLOAD"));
|
assertTrue(checked.allowedActions().contains("DOWNLOAD"));
|
||||||
assertFalse(checked.allowedActions().contains("INSTALL"));
|
assertFalse(checked.allowedActions().contains("INSTALL"));
|
||||||
|
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ FILE_SCANNER_ENABLED=true
|
|||||||
FINANCE_BOOTSTRAP_ENABLED=false
|
FINANCE_BOOTSTRAP_ENABLED=false
|
||||||
FINANCE_BOOTSTRAP_PASSWORD=
|
FINANCE_BOOTSTRAP_PASSWORD=
|
||||||
|
|
||||||
APP_VERSION=1.0.0-preview.45
|
APP_VERSION=1.0.0-preview.46
|
||||||
UPDATE_CURRENT_VERSION_FILE=/opt/kaidi/current/VERSION
|
UPDATE_CURRENT_VERSION_FILE=/opt/kaidi/current/VERSION
|
||||||
FINANCE_UPDATE_ENABLED=true
|
FINANCE_UPDATE_ENABLED=true
|
||||||
# Use either a stable direct asset base URL or the public Gitea latest-release API.
|
# Use either a stable direct asset base URL or the public Gitea latest-release API.
|
||||||
|
|||||||
@@ -43,6 +43,8 @@ RELEASE_AUTH_HEADER_FILE=
|
|||||||
TARGET_VERSION=
|
TARGET_VERSION=
|
||||||
REQUEST_ID=
|
REQUEST_ID=
|
||||||
REQUEST_ACTION=
|
REQUEST_ACTION=
|
||||||
|
RELEASE_NOTES=
|
||||||
|
RELEASE_PUBLISHED_AT=
|
||||||
TERMINAL_STATUS_WRITTEN=false
|
TERMINAL_STATUS_WRITTEN=false
|
||||||
DOWNLOAD_PID=
|
DOWNLOAD_PID=
|
||||||
DOWNLOAD_PGID=
|
DOWNLOAD_PGID=
|
||||||
@@ -297,6 +299,8 @@ status() {
|
|||||||
previous_message=
|
previous_message=
|
||||||
previous_request_id=
|
previous_request_id=
|
||||||
previous_action=
|
previous_action=
|
||||||
|
previous_release_notes=
|
||||||
|
previous_published_at=
|
||||||
if [ -f "$STATUS_FILE" ] && [ ! -L "$STATUS_FILE" ]; then
|
if [ -f "$STATUS_FILE" ] && [ ! -L "$STATUS_FILE" ]; then
|
||||||
previous_state=$(jq -r '.state // empty' "$STATUS_FILE" 2>/dev/null || true)
|
previous_state=$(jq -r '.state // empty' "$STATUS_FILE" 2>/dev/null || true)
|
||||||
previous_message=$(jq -r '.message // empty' "$STATUS_FILE" 2>/dev/null || true)
|
previous_message=$(jq -r '.message // empty' "$STATUS_FILE" 2>/dev/null || true)
|
||||||
@@ -304,6 +308,10 @@ status() {
|
|||||||
"$STATUS_FILE" 2>/dev/null || true)
|
"$STATUS_FILE" 2>/dev/null || true)
|
||||||
previous_action=$(jq -r '.action // empty | strings | ascii_upcase \
|
previous_action=$(jq -r '.action // empty | strings | ascii_upcase \
|
||||||
| select(. == "DOWNLOAD" or . == "INSTALL")' "$STATUS_FILE" 2>/dev/null || true)
|
| select(. == "DOWNLOAD" or . == "INSTALL")' "$STATUS_FILE" 2>/dev/null || true)
|
||||||
|
previous_release_notes=$(jq -r '(.releaseNotes // "") | strings | .[0:4000]' \
|
||||||
|
"$STATUS_FILE" 2>/dev/null || true)
|
||||||
|
previous_published_at=$(jq -r '(.publishedAt // "") | strings | .[0:128]' \
|
||||||
|
"$STATUS_FILE" 2>/dev/null || true)
|
||||||
fi
|
fi
|
||||||
status_request_id=
|
status_request_id=
|
||||||
status_action=
|
status_action=
|
||||||
@@ -315,6 +323,8 @@ status() {
|
|||||||
fi
|
fi
|
||||||
[ -n "$status_request_id" ] || status_request_id=${REQUEST_ID:-$previous_request_id}
|
[ -n "$status_request_id" ] || status_request_id=${REQUEST_ID:-$previous_request_id}
|
||||||
[ -n "$status_action" ] || status_action=${REQUEST_ACTION:-$previous_action}
|
[ -n "$status_action" ] || status_action=${REQUEST_ACTION:-$previous_action}
|
||||||
|
status_release_notes=${RELEASE_NOTES:-$previous_release_notes}
|
||||||
|
status_published_at=${RELEASE_PUBLISHED_AT:-$previous_published_at}
|
||||||
tmp="$STATUS_FILE.tmp.$$"
|
tmp="$STATUS_FILE.tmp.$$"
|
||||||
jq -n \
|
jq -n \
|
||||||
--arg state "$state" \
|
--arg state "$state" \
|
||||||
@@ -327,11 +337,15 @@ status() {
|
|||||||
--arg restartExpectedSeconds "$restart_expected_seconds" \
|
--arg restartExpectedSeconds "$restart_expected_seconds" \
|
||||||
--arg requestId "$status_request_id" \
|
--arg requestId "$status_request_id" \
|
||||||
--arg action "$status_action" \
|
--arg action "$status_action" \
|
||||||
|
--arg releaseNotes "$status_release_notes" \
|
||||||
|
--arg publishedAt "$status_published_at" \
|
||||||
--arg updatedAt "$(date -u +%Y-%m-%dT%H:%M:%SZ)" \
|
--arg updatedAt "$(date -u +%Y-%m-%dT%H:%M:%SZ)" \
|
||||||
'{state:$state,message:$message,updatedAt:$updatedAt}
|
'{state:$state,message:$message,updatedAt:$updatedAt}
|
||||||
+ (if ($version | length) > 0 then {targetVersion:$version} else {} end)
|
+ (if ($version | length) > 0 then {targetVersion:$version} else {} end)
|
||||||
+ (if ($requestId | length) > 0 then {requestId:$requestId} else {} end)
|
+ (if ($requestId | length) > 0 then {requestId:$requestId} else {} end)
|
||||||
+ (if ($action == "DOWNLOAD" or $action == "INSTALL") then {action:$action} else {} end)
|
+ (if ($action == "DOWNLOAD" or $action == "INSTALL") then {action:$action} else {} end)
|
||||||
|
+ (if ($releaseNotes | length) > 0 then {releaseNotes:$releaseNotes} else {} end)
|
||||||
|
+ (if ($publishedAt | length) > 0 then {publishedAt:$publishedAt} else {} end)
|
||||||
+ (if ($downloadedBytes | test("^[0-9]+$")) then {downloadedBytes:($downloadedBytes | tonumber)} else {} end)
|
+ (if ($downloadedBytes | test("^[0-9]+$")) then {downloadedBytes:($downloadedBytes | tonumber)} else {} end)
|
||||||
+ (if ($totalBytes | test("^[0-9]+$")) then {totalBytes:($totalBytes | tonumber)} else {} end)
|
+ (if ($totalBytes | test("^[0-9]+$")) then {totalBytes:($totalBytes | tonumber)} else {} end)
|
||||||
+ (if ($bytesPerSecond | test("^[0-9]+$")) then {bytesPerSecond:($bytesPerSecond | tonumber)} else {} end)
|
+ (if ($bytesPerSecond | test("^[0-9]+$")) then {bytesPerSecond:($bytesPerSecond | tonumber)} else {} end)
|
||||||
@@ -1119,6 +1133,10 @@ EXPECTED_SHA=$(jq -er '.sha256 | strings | ascii_downcase | select(test("^[0-9a-
|
|||||||
"$WORK_DIR/release-manifest.json") || fail "Release SHA-256 is invalid"
|
"$WORK_DIR/release-manifest.json") || fail "Release SHA-256 is invalid"
|
||||||
EXPECTED_SIZE=$(jq -er '(.artifactSizeBytes // 0) | numbers | floor | select(. >= 0)' \
|
EXPECTED_SIZE=$(jq -er '(.artifactSizeBytes // 0) | numbers | floor | select(. >= 0)' \
|
||||||
"$WORK_DIR/release-manifest.json") || fail "Release artifact size is invalid"
|
"$WORK_DIR/release-manifest.json") || fail "Release artifact size is invalid"
|
||||||
|
RELEASE_NOTES=$(jq -r '(.releaseNotes // "") | strings | .[0:4000]' \
|
||||||
|
"$WORK_DIR/release-manifest.json") || fail "Release notes are invalid"
|
||||||
|
RELEASE_PUBLISHED_AT=$(jq -r '(.publishedAt // "") | strings | .[0:128]' \
|
||||||
|
"$WORK_DIR/release-manifest.json") || fail "Release publish time is invalid"
|
||||||
|
|
||||||
CURRENT_VERSION=$(cat "$APP_ROOT/current/VERSION" 2>/dev/null || true)
|
CURRENT_VERSION=$(cat "$APP_ROOT/current/VERSION" 2>/dev/null || true)
|
||||||
if [ "$CURRENT_VERSION" = "$TARGET_VERSION" ]; then
|
if [ "$CURRENT_VERSION" = "$TARGET_VERSION" ]; then
|
||||||
|
|||||||
@@ -138,8 +138,12 @@ async function installFixture(
|
|||||||
beforeJson: null,
|
beforeJson: null,
|
||||||
afterJson: JSON.stringify({
|
afterJson: JSON.stringify({
|
||||||
targetVersion,
|
targetVersion,
|
||||||
...(actionCode === 'SYSTEM_UPDATE_CHECK' ? { releaseNotes: 'Preview update' } : {}),
|
...(actionCode === 'SYSTEM_UPDATE_CHECK'
|
||||||
...(actionCode === 'SYSTEM_UPDATE_SUCCEEDED' ? { state: 'SUCCEEDED' } : {}),
|
? { releaseNotes: 'Preview update', publishedAt: '2026-08-16T00:00:00Z' }
|
||||||
|
: {}),
|
||||||
|
...(actionCode === 'SYSTEM_UPDATE_SUCCEEDED'
|
||||||
|
? { state: 'SUCCEEDED', releaseNotes: 'Preview update', publishedAt: '2026-08-16T00:00:00Z' }
|
||||||
|
: {}),
|
||||||
}),
|
}),
|
||||||
occurredAt: '2026-08-16T00:02:00Z',
|
occurredAt: '2026-08-16T00:02:00Z',
|
||||||
allowedActions: [],
|
allowedActions: [],
|
||||||
@@ -214,6 +218,7 @@ async function installFixture(
|
|||||||
const query = new URL(request.url()).searchParams;
|
const query = new URL(request.url()).searchParams;
|
||||||
expect(query.get('occurredFrom')).toBe('1970-01-01T00:00:00Z');
|
expect(query.get('occurredFrom')).toBe('1970-01-01T00:00:00Z');
|
||||||
expect(query.get('objectType')).toBe('SYSTEM_UPDATE');
|
expect(query.get('objectType')).toBe('SYSTEM_UPDATE');
|
||||||
|
expect(query.get('size')).toBe('100');
|
||||||
return route.fulfill({
|
return route.fulfill({
|
||||||
json: {
|
json: {
|
||||||
data: history,
|
data: history,
|
||||||
@@ -418,15 +423,18 @@ test('successful installation starts the ten-second automatic refresh countdown'
|
|||||||
const historyPanel = page.locator('.history-panel');
|
const historyPanel = page.locator('.history-panel');
|
||||||
const timelineItem = historyPanel.locator('.update-timeline__item[data-version="1.0.0-preview.2"]');
|
const timelineItem = historyPanel.locator('.update-timeline__item[data-version="1.0.0-preview.2"]');
|
||||||
await expect(timelineItem).toHaveCount(1);
|
await expect(timelineItem).toHaveCount(1);
|
||||||
await expect(timelineItem).toContainText('更新完成');
|
await expect(timelineItem).toContainText('成功');
|
||||||
await expect(timelineItem).toContainText('新版本已通过健康检查');
|
await expect(timelineItem).toContainText('新版本已通过健康检查');
|
||||||
await expect(timelineItem).toContainText('Preview update');
|
await expect(timelineItem).toContainText('Preview update');
|
||||||
await timelineItem.getByRole('button', { name: '查看完整记录', exact: true }).click();
|
await timelineItem.getByRole('button', { name: '查看更新日志', exact: true }).click();
|
||||||
const historyDialog = page.getByRole('dialog', { name: '更新日志详情' });
|
const historyDialog = page.getByRole('dialog', { name: '更新日志详情' });
|
||||||
await expect(historyDialog).toContainText('获取版本');
|
await expect(historyDialog).toContainText('更新日志');
|
||||||
await expect(historyDialog).toContainText('下载更新包');
|
await expect(historyDialog).toContainText('Preview update');
|
||||||
await expect(historyDialog).toContainText('立即更新并重启');
|
await expect(historyDialog).toContainText('发布时间');
|
||||||
await expect(historyDialog).toContainText('更新完成');
|
await expect(historyDialog.locator('.history-detail__steps')).toHaveCount(0);
|
||||||
|
await expect(historyDialog).not.toContainText('获取版本');
|
||||||
|
await expect(historyDialog).not.toContainText('下载更新包');
|
||||||
|
await expect(historyDialog).not.toContainText('立即更新并重启');
|
||||||
});
|
});
|
||||||
|
|
||||||
test('reloading during installation resumes polling and starts the refresh countdown', async ({ page }) => {
|
test('reloading during installation resumes polling and starts the refresh countdown', async ({ page }) => {
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
<section class="update-panel" aria-label="系统版本与更新操作">
|
<section class="update-panel" aria-label="系统版本与更新操作">
|
||||||
<header class="update-toolbar" aria-label="系统更新工具栏">
|
<header class="update-toolbar" aria-label="系统更新工具栏">
|
||||||
<div class="update-toolbar__copy">
|
<div class="update-toolbar__copy">
|
||||||
<h2>系统更新</h2>
|
<div class="update-toolbar__title">系统更新</div>
|
||||||
<p>获取版本、下载校验并安全重启应用</p>
|
<p>获取版本、下载校验并安全重启应用</p>
|
||||||
</div>
|
</div>
|
||||||
<t-button variant="outline" :loading="statusLoading || historyLoading" @click="refreshPage">
|
<t-button variant="outline" :loading="statusLoading || historyLoading" @click="refreshPage">
|
||||||
@@ -111,9 +111,9 @@
|
|||||||
<div class="section-heading">
|
<div class="section-heading">
|
||||||
<div>
|
<div>
|
||||||
<h2>历史更新记录</h2>
|
<h2>历史更新记录</h2>
|
||||||
<p>按版本汇总获取、下载、重启安装和最终执行结果。</p>
|
<p>按版本展示签名 Release 提供的更新日志与最终结果。</p>
|
||||||
</div>
|
</div>
|
||||||
<span>最近 {{ historyTimeline.length }} 个版本 · {{ historyRows.length }} 条记录</span>
|
<span>已记录 {{ historyTimeline.length }} 个版本</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<t-alert v-if="historyError" theme="error" :close-btn="false">
|
<t-alert v-if="historyError" theme="error" :close-btn="false">
|
||||||
@@ -146,37 +146,19 @@
|
|||||||
<t-tag :theme="resultTheme(item.resultCode)" variant="light">{{ resultLabel(item.resultCode) }}</t-tag>
|
<t-tag :theme="resultTheme(item.resultCode)" variant="light">{{ resultLabel(item.resultCode) }}</t-tag>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="timeline-card__summary">
|
<div class="timeline-card__notes">
|
||||||
<span class="timeline-card__label">执行流程</span>
|
<span>更新日志</span>
|
||||||
<span>{{ item.operations.join(' · ') }}</span>
|
<p>{{ item.releaseNotes || '该版本的签名 Release 未提供更新日志。' }}</p>
|
||||||
<span class="timeline-card__count">{{ item.steps.length }} 条记录</span>
|
|
||||||
</div>
|
|
||||||
<p class="timeline-card__reason">{{ item.reason || '已记录本次版本更新操作。' }}</p>
|
|
||||||
|
|
||||||
<div v-if="item.releaseNotes" class="timeline-card__notes">
|
|
||||||
<span>版本说明</span>
|
|
||||||
<p>{{ item.releaseNotes }}</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="timeline-steps" aria-label="更新步骤">
|
|
||||||
<div v-for="step in item.steps" :key="step.row.publicId" class="timeline-step">
|
|
||||||
<span class="timeline-step__dot" :class="`timeline-step__dot--${resultTheme(step.row.resultCode)}`" />
|
|
||||||
<div class="timeline-step__body">
|
|
||||||
<div class="timeline-step__heading">
|
|
||||||
<strong>{{ actionLabel(step.row.actionCode) }}</strong>
|
|
||||||
<t-tag size="small" :theme="resultTheme(step.row.resultCode)" variant="light">
|
|
||||||
{{ resultLabel(step.row.resultCode) }}
|
|
||||||
</t-tag>
|
|
||||||
<time>{{ formatDateTime(step.row.occurredAt) }}</time>
|
|
||||||
</div>
|
|
||||||
<p>{{ step.row.reason || '操作已记录' }} · {{ step.row.username || '系统' }}</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
<p class="timeline-card__reason">{{ item.reason || '更新结果已记录。' }}</p>
|
||||||
|
<div class="timeline-card__published">
|
||||||
|
<span v-if="item.publishedAt">发布时间:{{ formatDateTime(item.publishedAt) }}</span>
|
||||||
|
<span>记录时间:{{ formatDateTime(item.occurredAt) }} · {{ item.username || '系统' }}</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="timeline-card__footer">
|
<div class="timeline-card__footer">
|
||||||
<span>最后更新:{{ formatDateTime(item.occurredAt) }}</span>
|
<span>目标版本 {{ item.version }}</span>
|
||||||
<t-button variant="text" size="small" @click="openHistoryDetail(item)">查看完整记录</t-button>
|
<t-button variant="text" size="small" @click="openHistoryDetail(item)">查看更新日志</t-button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</article>
|
</article>
|
||||||
@@ -283,23 +265,17 @@
|
|||||||
{{ resultLabel(selectedHistory.resultCode) }}
|
{{ resultLabel(selectedHistory.resultCode) }}
|
||||||
</t-tag>
|
</t-tag>
|
||||||
</div>
|
</div>
|
||||||
<p class="history-detail__reason">{{ selectedHistory.reason || '已记录本次版本更新操作。' }}</p>
|
<div class="history-detail__metadata">
|
||||||
<div v-if="selectedHistory.releaseNotes" class="history-detail__notes">
|
<span v-if="selectedHistory.publishedAt">发布时间:{{ formatDateTime(selectedHistory.publishedAt) }}</span>
|
||||||
<span>版本说明</span>
|
<span
|
||||||
<p>{{ selectedHistory.releaseNotes }}</p>
|
>记录时间:{{ formatDateTime(selectedHistory.occurredAt) }} · {{ selectedHistory.username || '系统' }}</span
|
||||||
</div>
|
>
|
||||||
<div class="history-detail__steps" aria-label="更新日志">
|
|
||||||
<div v-for="step in selectedHistory.steps" :key="step.row.publicId" class="history-detail__step">
|
|
||||||
<div>
|
|
||||||
<strong>{{ actionLabel(step.row.actionCode) }}</strong>
|
|
||||||
<t-tag size="small" :theme="resultTheme(step.row.resultCode)" variant="light">
|
|
||||||
{{ resultLabel(step.row.resultCode) }}
|
|
||||||
</t-tag>
|
|
||||||
</div>
|
|
||||||
<time>{{ formatDateTime(step.row.occurredAt) }} · {{ step.row.username || '系统' }}</time>
|
|
||||||
<p>{{ step.row.reason || '操作已记录' }}</p>
|
|
||||||
</div>
|
</div>
|
||||||
|
<div class="history-detail__notes">
|
||||||
|
<span>更新日志</span>
|
||||||
|
<p>{{ selectedHistory.releaseNotes || '该版本的签名 Release 未提供更新日志。' }}</p>
|
||||||
</div>
|
</div>
|
||||||
|
<p class="history-detail__reason">{{ selectedHistory.reason || '更新结果已记录。' }}</p>
|
||||||
</div>
|
</div>
|
||||||
</t-dialog>
|
</t-dialog>
|
||||||
</div>
|
</div>
|
||||||
@@ -322,10 +298,6 @@ import {
|
|||||||
defineOptions({ name: 'SystemUpdatePage' });
|
defineOptions({ name: 'SystemUpdatePage' });
|
||||||
|
|
||||||
type TagTheme = 'default' | 'primary' | 'success' | 'warning' | 'danger';
|
type TagTheme = 'default' | 'primary' | 'success' | 'warning' | 'danger';
|
||||||
interface HistoryTimelineStep {
|
|
||||||
row: AuditLog;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface HistoryTimelineItem {
|
interface HistoryTimelineItem {
|
||||||
key: string;
|
key: string;
|
||||||
version: string;
|
version: string;
|
||||||
@@ -333,9 +305,8 @@ interface HistoryTimelineItem {
|
|||||||
username: string;
|
username: string;
|
||||||
resultCode: string;
|
resultCode: string;
|
||||||
reason: string;
|
reason: string;
|
||||||
operations: string[];
|
|
||||||
releaseNotes: string;
|
releaseNotes: string;
|
||||||
steps: HistoryTimelineStep[];
|
publishedAt: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
const UPDATE_PENDING_KEY = 'kaidi-system-update-pending';
|
const UPDATE_PENDING_KEY = 'kaidi-system-update-pending';
|
||||||
@@ -498,9 +469,8 @@ const historyTimeline = computed<HistoryTimelineItem[]>(() => {
|
|||||||
const newestFirst = rows.slice().sort((left, right) => compareHistoryDates(right.occurredAt, left.occurredAt));
|
const newestFirst = rows.slice().sort((left, right) => compareHistoryDates(right.occurredAt, left.occurredAt));
|
||||||
const terminal = newestFirst.find((row) => isTerminalHistoryAction(row.actionCode));
|
const terminal = newestFirst.find((row) => isTerminalHistoryAction(row.actionCode));
|
||||||
const representative = terminal || newestFirst[0];
|
const representative = terminal || newestFirst[0];
|
||||||
const chronological = newestFirst.slice().reverse();
|
|
||||||
const operations = [...new Set(chronological.map((row) => actionLabel(row.actionCode)).filter(Boolean))];
|
|
||||||
const releaseNotes = newestFirst.map((row) => historyReleaseNotes(row)).find(Boolean) || '';
|
const releaseNotes = newestFirst.map((row) => historyReleaseNotes(row)).find(Boolean) || '';
|
||||||
|
const publishedAt = newestFirst.map((row) => historyPublishedAt(row)).find(Boolean) || '';
|
||||||
|
|
||||||
return {
|
return {
|
||||||
key,
|
key,
|
||||||
@@ -509,9 +479,8 @@ const historyTimeline = computed<HistoryTimelineItem[]>(() => {
|
|||||||
username: representative?.username || '',
|
username: representative?.username || '',
|
||||||
resultCode: representative?.resultCode || 'UNKNOWN',
|
resultCode: representative?.resultCode || 'UNKNOWN',
|
||||||
reason: representative?.reason || '',
|
reason: representative?.reason || '',
|
||||||
operations,
|
|
||||||
releaseNotes,
|
releaseNotes,
|
||||||
steps: chronological.map((row) => ({ row })),
|
publishedAt,
|
||||||
};
|
};
|
||||||
})
|
})
|
||||||
.sort((left, right) => compareHistoryDates(right.occurredAt, left.occurredAt));
|
.sort((left, right) => compareHistoryDates(right.occurredAt, left.occurredAt));
|
||||||
@@ -571,7 +540,7 @@ async function loadHistory(silent = false) {
|
|||||||
objectType: 'SYSTEM_UPDATE',
|
objectType: 'SYSTEM_UPDATE',
|
||||||
sort: 'occurredAt,desc',
|
sort: 'occurredAt,desc',
|
||||||
page: 1,
|
page: 1,
|
||||||
size: 20,
|
size: 100,
|
||||||
});
|
});
|
||||||
historyRows.value = result.items;
|
historyRows.value = result.items;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -859,7 +828,24 @@ function historyVersion(row: AuditLog) {
|
|||||||
function historyReleaseNotes(row: AuditLog) {
|
function historyReleaseNotes(row: AuditLog) {
|
||||||
for (const source of [row.afterJson, row.beforeJson]) {
|
for (const source of [row.afterJson, row.beforeJson]) {
|
||||||
const parsed = parseHistoryPayload(source);
|
const parsed = parseHistoryPayload(source);
|
||||||
if (typeof parsed?.releaseNotes === 'string' && parsed.releaseNotes.trim()) return parsed.releaseNotes;
|
for (const key of ['releaseNotes', 'changelog', 'updateLog', 'notes']) {
|
||||||
|
const value = parsed?.[key];
|
||||||
|
if (typeof value === 'string' && value.trim()) return value.trim();
|
||||||
|
if (Array.isArray(value)) {
|
||||||
|
const lines = value
|
||||||
|
.filter((item): item is string => typeof item === 'string' && Boolean(item.trim()))
|
||||||
|
.map((item) => item.trim());
|
||||||
|
if (lines.length) return lines.join('\n');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
function historyPublishedAt(row: AuditLog) {
|
||||||
|
for (const source of [row.afterJson, row.beforeJson]) {
|
||||||
|
const parsed = parseHistoryPayload(source);
|
||||||
|
if (typeof parsed?.publishedAt === 'string' && parsed.publishedAt.trim()) return parsed.publishedAt;
|
||||||
}
|
}
|
||||||
return '';
|
return '';
|
||||||
}
|
}
|
||||||
@@ -869,19 +855,6 @@ function openHistoryDetail(item: HistoryTimelineItem) {
|
|||||||
historyDetailVisible.value = true;
|
historyDetailVisible.value = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
function actionLabel(action: string) {
|
|
||||||
return (
|
|
||||||
{
|
|
||||||
SYSTEM_UPDATE_CHECK: '获取版本',
|
|
||||||
SYSTEM_UPDATE_DOWNLOAD_REQUEST: '下载更新包',
|
|
||||||
SYSTEM_UPDATE_REQUEST: '立即更新并重启',
|
|
||||||
SYSTEM_UPDATE_SUCCEEDED: '更新完成',
|
|
||||||
SYSTEM_UPDATE_FAILED: '更新失败',
|
|
||||||
SYSTEM_UPDATE_RECOVERY_REQUIRED: '更新需人工恢复',
|
|
||||||
}[action] || action
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function resultLabel(result: string) {
|
function resultLabel(result: string) {
|
||||||
return { SUCCESS: '成功', FAILED: '失败', DENIED: '已拒绝', BLOCKED: '已阻断' }[result] || result;
|
return { SUCCESS: '成功', FAILED: '失败', DENIED: '已拒绝', BLOCKED: '已阻断' }[result] || result;
|
||||||
}
|
}
|
||||||
@@ -983,7 +956,7 @@ onBeforeUnmount(() => {
|
|||||||
min-width: 0;
|
min-width: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.update-toolbar__copy h2 {
|
.update-toolbar__title {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
color: var(--td-text-color-primary);
|
color: var(--td-text-color-primary);
|
||||||
font-size: 18px;
|
font-size: 18px;
|
||||||
@@ -1155,9 +1128,7 @@ onBeforeUnmount(() => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.timeline-card__header,
|
.timeline-card__header,
|
||||||
.timeline-card__summary,
|
|
||||||
.timeline-card__footer,
|
.timeline-card__footer,
|
||||||
.timeline-step__heading,
|
|
||||||
.history-detail__header {
|
.history-detail__header {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
@@ -1188,35 +1159,13 @@ onBeforeUnmount(() => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.timeline-card__identity span,
|
.timeline-card__identity span,
|
||||||
.timeline-card__count,
|
|
||||||
.timeline-card__footer,
|
.timeline-card__footer,
|
||||||
.timeline-step__heading time,
|
|
||||||
.history-detail__header span,
|
.history-detail__header span,
|
||||||
.history-detail__step time {
|
.history-detail__metadata {
|
||||||
color: var(--td-text-color-secondary);
|
color: var(--td-text-color-secondary);
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.timeline-card__summary {
|
|
||||||
flex-wrap: wrap;
|
|
||||||
margin-top: 12px;
|
|
||||||
padding: 9px 10px;
|
|
||||||
color: var(--td-text-color-primary);
|
|
||||||
font-size: 13px;
|
|
||||||
line-height: 20px;
|
|
||||||
background: var(--td-bg-color-secondarycontainer);
|
|
||||||
border-radius: 4px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.timeline-card__label {
|
|
||||||
color: var(--td-text-color-secondary);
|
|
||||||
}
|
|
||||||
|
|
||||||
.timeline-card__count {
|
|
||||||
margin-left: auto;
|
|
||||||
white-space: nowrap;
|
|
||||||
}
|
|
||||||
|
|
||||||
.timeline-card__reason,
|
.timeline-card__reason,
|
||||||
.history-detail__reason {
|
.history-detail__reason {
|
||||||
margin: 12px 0 0;
|
margin: 12px 0 0;
|
||||||
@@ -1250,65 +1199,15 @@ onBeforeUnmount(() => {
|
|||||||
overflow-wrap: anywhere;
|
overflow-wrap: anywhere;
|
||||||
}
|
}
|
||||||
|
|
||||||
.timeline-steps {
|
.timeline-card__published,
|
||||||
|
.history-detail__metadata {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
|
||||||
gap: 10px;
|
|
||||||
margin-top: 14px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.timeline-step {
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: 10px minmax(0, 1fr);
|
|
||||||
gap: 10px;
|
|
||||||
min-width: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.timeline-step__dot {
|
|
||||||
width: 8px;
|
|
||||||
height: 8px;
|
|
||||||
margin-top: 7px;
|
|
||||||
background: var(--td-component-border);
|
|
||||||
border-radius: 50%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.timeline-step__dot--success {
|
|
||||||
background: var(--td-success-color);
|
|
||||||
}
|
|
||||||
|
|
||||||
.timeline-step__dot--danger {
|
|
||||||
background: var(--td-error-color);
|
|
||||||
}
|
|
||||||
|
|
||||||
.timeline-step__dot--warning {
|
|
||||||
background: var(--td-warning-color);
|
|
||||||
}
|
|
||||||
|
|
||||||
.timeline-step__body {
|
|
||||||
min-width: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.timeline-step__heading {
|
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
}
|
gap: 6px 16px;
|
||||||
|
margin-top: 10px;
|
||||||
.timeline-step__heading strong {
|
|
||||||
color: var(--td-text-color-primary);
|
|
||||||
font-size: 13px;
|
|
||||||
font-weight: 600;
|
|
||||||
}
|
|
||||||
|
|
||||||
.timeline-step__heading time {
|
|
||||||
margin-left: auto;
|
|
||||||
white-space: nowrap;
|
|
||||||
}
|
|
||||||
|
|
||||||
.timeline-step__body p {
|
|
||||||
margin: 3px 0 0;
|
|
||||||
color: var(--td-text-color-secondary);
|
color: var(--td-text-color-secondary);
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
line-height: 20px;
|
line-height: 20px;
|
||||||
overflow-wrap: anywhere;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.timeline-card__footer {
|
.timeline-card__footer {
|
||||||
@@ -1336,53 +1235,6 @@ onBeforeUnmount(() => {
|
|||||||
line-height: 26px;
|
line-height: 26px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.history-detail__steps {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 12px;
|
|
||||||
max-height: 420px;
|
|
||||||
padding: 12px;
|
|
||||||
overflow: auto;
|
|
||||||
background: var(--td-bg-color-secondarycontainer);
|
|
||||||
border: 1px solid var(--td-component-border);
|
|
||||||
border-radius: 6px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.history-detail__step {
|
|
||||||
padding-bottom: 12px;
|
|
||||||
border-bottom: 1px solid var(--td-component-border);
|
|
||||||
}
|
|
||||||
|
|
||||||
.history-detail__step:last-child {
|
|
||||||
padding-bottom: 0;
|
|
||||||
border-bottom: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.history-detail__step > div {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
gap: 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.history-detail__step strong {
|
|
||||||
color: var(--td-text-color-primary);
|
|
||||||
font-size: 13px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.history-detail__step time {
|
|
||||||
display: block;
|
|
||||||
margin-top: 4px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.history-detail__step p {
|
|
||||||
margin: 4px 0 0;
|
|
||||||
color: var(--td-text-color-secondary);
|
|
||||||
font-size: 13px;
|
|
||||||
line-height: 20px;
|
|
||||||
overflow-wrap: anywhere;
|
|
||||||
}
|
|
||||||
|
|
||||||
.alert-content {
|
.alert-content {
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
gap: 12px;
|
gap: 12px;
|
||||||
@@ -1552,16 +1404,6 @@ onBeforeUnmount(() => {
|
|||||||
padding: 12px;
|
padding: 12px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.timeline-card__count {
|
|
||||||
width: 100%;
|
|
||||||
margin-left: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.timeline-step__heading time {
|
|
||||||
width: 100%;
|
|
||||||
margin-left: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.timeline-card__notes,
|
.timeline-card__notes,
|
||||||
.history-detail__notes {
|
.history-detail__notes {
|
||||||
grid-template-columns: 1fr;
|
grid-template-columns: 1fr;
|
||||||
|
|||||||
@@ -0,0 +1,11 @@
|
|||||||
|
更新日志(Preview 47)
|
||||||
|
|
||||||
|
本版本聚焦在线更新的可追溯性与发布说明展示,不改变财务业务数据和审批规则。
|
||||||
|
|
||||||
|
• 更新历史改为按版本展示 Release 更新日志,不再把获取、下载、重启等内部执行步骤当作历史内容。
|
||||||
|
• 历史详情直接展示签名 Release 的发布说明、发布时间、目标版本和最终结果。
|
||||||
|
• 更新器在验签后把 releaseNotes 与 publishedAt 写入状态文件;应用重启后仍能显示完整发布说明。
|
||||||
|
• 终态审计记录保存发布说明并保持幂等,历史记录不再只显示“更新成功”一条信息。
|
||||||
|
• 发布打包、验签和 Gitea Release 正文统一读取 release-notes/<version>.md,避免页面、清单和 Release 描述不一致。
|
||||||
|
|
||||||
|
升级说明:本版本不要求安装 MySQL,也不会自动修改数据库服务;更新器默认跳过数据库备份,仅使用已验签的 Release 制品完成应用切换。
|
||||||
@@ -187,7 +187,20 @@ KAIDI_PURGER_SHA256=$PURGER_SHA256
|
|||||||
EOF
|
EOF
|
||||||
BOOTSTRAP_SHA256=$(sha256_file "$OUTPUT_DIR/bootstrap-checksums.txt")
|
BOOTSTRAP_SHA256=$(sha256_file "$OUTPUT_DIR/bootstrap-checksums.txt")
|
||||||
|
|
||||||
RELEASE_NOTES_VALUE=${KAIDI_RELEASE_NOTES:-"Kaidi Finance Preview $VERSION"}
|
RELEASE_NOTES_FILE=${KAIDI_RELEASE_NOTES_FILE:-$ROOT/release-notes/$VERSION.md}
|
||||||
|
if [ -n "${KAIDI_RELEASE_NOTES+x}" ]; then
|
||||||
|
RELEASE_NOTES_VALUE=$KAIDI_RELEASE_NOTES
|
||||||
|
elif [ -f "$RELEASE_NOTES_FILE" ] && [ ! -L "$RELEASE_NOTES_FILE" ]; then
|
||||||
|
RELEASE_NOTES_VALUE=$(sed 's/\r$//' "$RELEASE_NOTES_FILE")
|
||||||
|
elif [ "$SOURCE_REF" != local ] || [ "${KAIDI_REQUIRE_RELEASE_NOTES:-false}" = true ]; then
|
||||||
|
printf 'Release notes file is missing: %s\n' "$RELEASE_NOTES_FILE" >&2
|
||||||
|
exit 1
|
||||||
|
else
|
||||||
|
RELEASE_NOTES_VALUE="Kaidi Finance Preview $VERSION"
|
||||||
|
fi
|
||||||
|
RELEASE_NOTES_BYTES=$(printf '%s' "$RELEASE_NOTES_VALUE" | wc -c | tr -d '[:space:]')
|
||||||
|
[ "$RELEASE_NOTES_BYTES" -gt 0 ] && [ "$RELEASE_NOTES_BYTES" -le 4000 ] \
|
||||||
|
|| { printf 'Release notes must contain 1 to 4000 bytes\n' >&2; exit 1; }
|
||||||
VERSION="$VERSION" ARTIFACT="$ARTIFACT" SHA256="$SHA256" ARTIFACT_SIZE_BYTES="$ARTIFACT_SIZE_BYTES" \
|
VERSION="$VERSION" ARTIFACT="$ARTIFACT" SHA256="$SHA256" ARTIFACT_SIZE_BYTES="$ARTIFACT_SIZE_BYTES" \
|
||||||
RELEASE_NOTES="$RELEASE_NOTES_VALUE" \
|
RELEASE_NOTES="$RELEASE_NOTES_VALUE" \
|
||||||
BACKEND_SBOM_SHA256="$BACKEND_SBOM_SHA256" FRONTEND_SBOM_SHA256="$FRONTEND_SBOM_SHA256" \
|
BACKEND_SBOM_SHA256="$BACKEND_SBOM_SHA256" FRONTEND_SBOM_SHA256="$FRONTEND_SBOM_SHA256" \
|
||||||
|
|||||||
@@ -10,9 +10,7 @@ SOURCE_SHA=${GITEA_SHA:-}
|
|||||||
TOKEN=${GITEA_TOKEN:-}
|
TOKEN=${GITEA_TOKEN:-}
|
||||||
RELEASE_DIR=${KAIDI_RELEASE_DIR:-dist/release}
|
RELEASE_DIR=${KAIDI_RELEASE_DIR:-dist/release}
|
||||||
RELEASE_NAME=${KAIDI_RELEASE_NAME:-Kaidi Finance $TAG}
|
RELEASE_NAME=${KAIDI_RELEASE_NAME:-Kaidi Finance $TAG}
|
||||||
RELEASE_BODY=${KAIDI_RELEASE_BODY:-Kaidi Finance $TAG
|
RELEASE_BODY=${KAIDI_RELEASE_BODY-}
|
||||||
|
|
||||||
Source: $SOURCE_SHA}
|
|
||||||
WORK=$(mktemp -d)
|
WORK=$(mktemp -d)
|
||||||
AUTH_HEADER=$WORK/gitea-auth-header
|
AUTH_HEADER=$WORK/gitea-auth-header
|
||||||
|
|
||||||
@@ -38,6 +36,15 @@ esac
|
|||||||
|| fail 'GITEA_TOKEN is invalid'
|
|| fail 'GITEA_TOKEN is invalid'
|
||||||
[ -d "$RELEASE_DIR" ] || fail 'release directory is missing'
|
[ -d "$RELEASE_DIR" ] || fail 'release directory is missing'
|
||||||
|
|
||||||
|
if [ -z "${KAIDI_RELEASE_BODY+x}" ]; then
|
||||||
|
RELEASE_NOTES=$(jq -er '.releaseNotes | strings | select(length > 0 and length <= 4000)' \
|
||||||
|
"$RELEASE_DIR/release-manifest.json") \
|
||||||
|
|| fail 'release manifest notes are missing or invalid'
|
||||||
|
RELEASE_BODY="$RELEASE_NOTES
|
||||||
|
|
||||||
|
Source: $SOURCE_SHA"
|
||||||
|
fi
|
||||||
|
|
||||||
printf 'Authorization: token %s\nAccept: application/json\n' "$TOKEN" > "$AUTH_HEADER"
|
printf 'Authorization: token %s\nAccept: application/json\n' "$TOKEN" > "$AUTH_HEADER"
|
||||||
chmod 0600 "$AUTH_HEADER"
|
chmod 0600 "$AUTH_HEADER"
|
||||||
|
|
||||||
|
|||||||
@@ -29,6 +29,7 @@ for name in \
|
|||||||
SHA256SUMS; do
|
SHA256SUMS; do
|
||||||
printf 'fixture asset %s\n' "$name" > "$RELEASE_DIR/$name"
|
printf 'fixture asset %s\n' "$name" > "$RELEASE_DIR/$name"
|
||||||
done
|
done
|
||||||
|
printf '%s\n' '{"releaseNotes":"Fixture release notes"}' > "$RELEASE_DIR/release-manifest.json"
|
||||||
|
|
||||||
cat > "$MOCK_BIN/curl" <<'SH'
|
cat > "$MOCK_BIN/curl" <<'SH'
|
||||||
#!/usr/bin/env bash
|
#!/usr/bin/env bash
|
||||||
|
|||||||
@@ -315,6 +315,10 @@ download_and_prepare_install() {
|
|||||||
[ "$(jq -r '.requestId' "$fixture/state/status.json")" = 01M00000000000000000000091 ] \
|
[ "$(jq -r '.requestId' "$fixture/state/status.json")" = 01M00000000000000000000091 ] \
|
||||||
&& [ "$(jq -r '.action' "$fixture/state/status.json")" = DOWNLOAD ] \
|
&& [ "$(jq -r '.action' "$fixture/state/status.json")" = DOWNLOAD ] \
|
||||||
|| fail 'download phase did not preserve request correlation'
|
|| fail 'download phase did not preserve request correlation'
|
||||||
|
[ "$(jq -r '.releaseNotes' "$fixture/state/status.json")" = fixture ] \
|
||||||
|
|| fail 'download phase did not persist the signed release notes'
|
||||||
|
[ "$(jq -r '.publishedAt' "$fixture/state/status.json")" = 2026-08-16T00:00:00Z ] \
|
||||||
|
|| fail 'download phase did not persist the signed release publish time'
|
||||||
[ "$(readlink "$fixture/app/current")" = "$fixture/app/releases/1.0.0-preview.1" ] \
|
[ "$(readlink "$fixture/app/current")" = "$fixture/app/releases/1.0.0-preview.1" ] \
|
||||||
|| fail 'download phase changed the active application'
|
|| fail 'download phase changed the active application'
|
||||||
[ -s "$fixture/state/cache/$version/release.tar.gz" ] \
|
[ -s "$fixture/state/cache/$version/release.tar.gz" ] \
|
||||||
@@ -456,6 +460,8 @@ assert_success_case() {
|
|||||||
[ "$(jq -r '.requestId' "$fixture/state/status.json")" = 01M00000000000000000000092 ] \
|
[ "$(jq -r '.requestId' "$fixture/state/status.json")" = 01M00000000000000000000092 ] \
|
||||||
&& [ "$(jq -r '.action' "$fixture/state/status.json")" = INSTALL ] \
|
&& [ "$(jq -r '.action' "$fixture/state/status.json")" = INSTALL ] \
|
||||||
|| fail 'success case did not preserve install request correlation'
|
|| fail 'success case did not preserve install request correlation'
|
||||||
|
[ "$(jq -r '.releaseNotes' "$fixture/state/status.json")" = fixture ] \
|
||||||
|
|| fail 'success case did not retain the signed release notes'
|
||||||
[ ! -e "$fixture/state/processing/request.json" ] \
|
[ ! -e "$fixture/state/processing/request.json" ] \
|
||||||
|| fail 'success case left a claimed request behind'
|
|| fail 'success case left a claimed request behind'
|
||||||
grep -qx 'daemon-reload' "$fixture/systemctl.log" || fail 'systemd units were not reloaded'
|
grep -qx 'daemon-reload' "$fixture/systemctl.log" || fail 'systemd units were not reloaded'
|
||||||
|
|||||||
@@ -30,6 +30,11 @@ openssl dgst -sha256 -verify release-public.pem \
|
|||||||
VERSION=$(jq -er '.version | strings | select(length > 0)' release-manifest.json)
|
VERSION=$(jq -er '.version | strings | select(length > 0)' release-manifest.json)
|
||||||
"$ROOT/scripts/check-semver.sh" "$VERSION" \
|
"$ROOT/scripts/check-semver.sh" "$VERSION" \
|
||||||
|| { printf 'Release version is not valid SemVer\n' >&2; exit 1; }
|
|| { printf 'Release version is not valid SemVer\n' >&2; exit 1; }
|
||||||
|
RELEASE_NOTES=$(jq -er '.releaseNotes | strings | select(length > 0 and length <= 4000)' release-manifest.json) \
|
||||||
|
|| { printf 'Release manifest must contain 1 to 4000 bytes of release notes\n' >&2; exit 1; }
|
||||||
|
RELEASE_NOTES_BYTES=$(printf '%s' "$RELEASE_NOTES" | wc -c | tr -d '[:space:]')
|
||||||
|
[ "$RELEASE_NOTES_BYTES" -le 4000 ] \
|
||||||
|
|| { printf 'Release manifest release notes exceed 4000 bytes\n' >&2; exit 1; }
|
||||||
ARTIFACT=$(jq -er '.artifact | strings | select(length > 0)' release-manifest.json)
|
ARTIFACT=$(jq -er '.artifact | strings | select(length > 0)' release-manifest.json)
|
||||||
[ -s "$ARTIFACT" ] || { printf 'Release artifact is missing\n' >&2; exit 1; }
|
[ -s "$ARTIFACT" ] || { printf 'Release artifact is missing\n' >&2; exit 1; }
|
||||||
[ "$ARTIFACT" = "kaidi-finance-$VERSION.tar.gz" ] \
|
[ "$ARTIFACT" = "kaidi-finance-$VERSION.tar.gz" ] \
|
||||||
|
|||||||
Reference in New Issue
Block a user