fix: synchronize release notes after update restart
Release / release (push) Canceled after 0s

This commit is contained in:
Qiufeng
2026-08-19 09:04:33 +08:00
parent 6b04d8dd0a
commit 1d45be4e97
7 changed files with 181 additions and 36 deletions
+15 -12
View File
@@ -70,7 +70,8 @@ PostgreSQL 18 兼容工作继续冻结。
- MySQL 是**已有数据库**,应用只通过向导写入的 `DB_URL`、`DB_USERNAME`、`DB_PASSWORD` 连接它;安装器不安装 MySQL、不创建数据库、不修改数据库服务。填写 `127.0.0.1` 表示 MySQL 与 Java 应用在同一台服务器,端口以实际监听端口为准,不默认假设 `3307`。 - MySQL 是**已有数据库**,应用只通过向导写入的 `DB_URL`、`DB_USERNAME`、`DB_PASSWORD` 连接它;安装器不安装 MySQL、不创建数据库、不修改数据库服务。填写 `127.0.0.1` 表示 MySQL 与 Java 应用在同一台服务器,端口以实际监听端口为准,不默认假设 `3307`。
- 安装阶段只校验 JDBC 配置格式,不调用 `mysql`/`mariadb` 客户端,也不执行 `CREATE`、`INSERT`、`UPDATE`、`DELETE` 或 `DROP`。首次向导点击“完成安装”后,应用才会在**专用空 schema**中运行 Flyway 建表并初始化管理员;这是业务初始化,不是安装 MySQL 服务。在线更新默认跳过数据库备份;需要备份时由运维显式设置 `KAIDI_DB_BACKUP_MODE=mysqldump`,更新器只调用已有工具,不会安装数据库。 - 安装阶段只校验 JDBC 配置格式,不调用 `mysql`/`mariadb` 客户端,也不执行 `CREATE`、`INSERT`、`UPDATE`、`DELETE` 或 `DROP`。首次向导点击“完成安装”后,应用才会在**专用空 schema**中运行 Flyway 建表并初始化管理员;这是业务初始化,不是安装 MySQL 服务。在线更新默认跳过数据库备份;需要备份时由运维显式设置 `KAIDI_DB_BACKUP_MODE=mysqldump`,更新器只调用已有工具,不会安装数据库。
- `KAIDI_APP_PORT` 只决定 Java 回环监听端口,默认 `18080`;反向代理必须指向安装器输出的 `PROXY_TARGET`。安装器不会替你修改 Nginx 或宝塔站点配置。 - `KAIDI_APP_PORT` 只决定 Java 回环监听端口,默认 `18080`;反向代理必须指向安装器输出的 `PROXY_TARGET`。安装器不会替你修改 Nginx 或宝塔站点配置。
- 发布归档使用无顶层目录的 `tar.gz`,只包含 `app.jar`、`public/`、`ops/`、`VERSION`;打包阶段禁用 macOS 扩展属性并拒绝开发数据库回退值。 - 发布归档使用无顶层目录的 `tar.gz`,包含 `app.jar`、`public/`、`ops/`、`VERSION` 和签名绑定的
`release-metadata.json`;打包阶段禁用 macOS 扩展属性并拒绝开发数据库回退值。
### 32 位 Linux 支持边界 ### 32 位 Linux 支持边界
@@ -83,7 +84,7 @@ PostgreSQL 18 兼容工作继续冻结。
先在宝塔面板停止并删除当前错误的 Java 项目,再执行: 先在宝塔面板停止并删除当前错误的 Java 项目,再执行:
```bash ```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 curl -fsSL https://git.awaioi.com/ERP-Team/kaidi/releases/download/v1.0.0-preview.48/install.sh | sudo env KAIDI_APP_PORT=18080 bash
``` ```
该命令只安装程序运行所需的 systemd 单元,自动创建 `kaidi` 用户并检测现有 Java 17(包括 该命令只安装程序运行所需的 systemd 单元,自动创建 `kaidi` 用户并检测现有 Java 17(包括
@@ -96,7 +97,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.47/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.48/purge.sh' | sudo env KAIDI_PURGE_CONFIRM=DELETE_LOCAL_KAIDI_INSTALLATION bash
``` ```
上面是一条完整命令:脚本通过管道直接交给 root 执行,不创建临时安装文件,避免终端自动换行导致 `-o` 参数丢失。 上面是一条完整命令:脚本通过管道直接交给 root 执行,不创建临时安装文件,避免终端自动换行导致 `-o` 参数丢失。
@@ -117,9 +118,9 @@ Spring Boot 项目。数据库、JDK、Nginx 和宝塔本身都由运维人员
下载地址: 下载地址:
`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.48/kaidi-finance-1.0.0-preview.48.tar.gz`
校验文件:`https://git.awaioi.com/ERP-Team/kaidi/releases/download/v1.0.0-preview.47/SHA256SUMS` 校验文件:`https://git.awaioi.com/ERP-Team/kaidi/releases/download/v1.0.0-preview.48/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 +137,7 @@ systemd/systemd-analyze,**宝塔手动部署**不要求 systemd。32 位 Linux
必须直接位于 `RELEASE_ROOT`,不能再嵌套一层目录。 必须直接位于 `RELEASE_ROOT`,不能再嵌套一层目录。
```bash ```bash
VERSION=1.0.0-preview.47 VERSION=1.0.0-preview.48
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 +246,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.47/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.48/purge.sh' | sudo env KAIDI_PURGE_CONFIRM=DELETE_LOCAL_KAIDI_INSTALLATION KAIDI_PURGE_DELETE_BACKUP=true bash
``` ```
执行前先在宝塔停止并删除 `kaidi-finance` Java 项目;宝塔面板元数据属于外部资源,必须由面板先停用, 执行前先在宝塔停止并删除 `kaidi-finance` Java 项目;宝塔面板元数据属于外部资源,必须由面板先停用,
@@ -269,12 +270,14 @@ 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 版本名表达。每个版本必须先在 `release-notes/<version>.md` 写好面向用户的更新日志。 Preview 属性由 SemVer 版本名表达。每个版本必须先在 `release-notes/<version>.md` 写好面向用户的更新日志。
打包器会把同一份内容写入签名 `release-manifest.json`,后台历史从该清单读取;发布脚本也会用它生成 打包器会把同一份内容写入签名 `release-manifest.json`,并把同一份签名元数据随应用制品写入
Gitea Release 正文,避免三处内容不一致。之后推送 tag 即会构建、测试、签名并发布: `release-metadata.json`。后台在线检查优先读取 Release 清单;应用重启后若旧版更新器没有把说明写入状态文件,
则从当前已验签制品中的元数据恢复,再写入终态审计,因此不依赖重启后的 Gitea 网络请求。发布脚本也会用
清单生成 Gitea Release 正文,避免页面、清单和制品三处内容不一致。之后推送 tag 即会构建、测试、签名并发布:
```bash ```bash
git tag v1.0.0-preview.47 git tag v1.0.0-preview.48
git push origin v1.0.0-preview.47 git push origin v1.0.0-preview.48
``` ```
在线更新仍使用独立的 TDesign 页面:系统管理员进入“系统治理 → 系统更新”。更新源由 root 在 在线更新仍使用独立的 TDesign 页面:系统管理员进入“系统治理 → 系统更新”。更新源由 root 在
@@ -326,7 +329,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.47 ./scripts/package-release.sh 1.0.0-preview.48
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
``` ```
@@ -34,6 +34,7 @@ import java.time.Instant;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
import java.util.Locale; import java.util.Locale;
import java.util.Objects;
import java.util.regex.Matcher; import java.util.regex.Matcher;
import java.util.regex.Pattern; import java.util.regex.Pattern;
import org.slf4j.Logger; import org.slf4j.Logger;
@@ -47,6 +48,7 @@ public class SystemUpdateApplicationService {
private static final Logger log = LoggerFactory.getLogger(SystemUpdateApplicationService.class); private static final Logger log = LoggerFactory.getLogger(SystemUpdateApplicationService.class);
private static final int MAX_MANIFEST_BYTES = 64 * 1024; private static final int MAX_MANIFEST_BYTES = 64 * 1024;
private static final int MAX_RELEASE_METADATA_BYTES = 16 * 1024;
private static final int MAX_RELEASE_API_BYTES = 256 * 1024; private static final int MAX_RELEASE_API_BYTES = 256 * 1024;
private static final int MAX_STATUS_BYTES = 64 * 1024; private static final int MAX_STATUS_BYTES = 64 * 1024;
private static final int MAX_EVENT_LOG_BYTES = 256 * 1024; private static final int MAX_EVENT_LOG_BYTES = 256 * 1024;
@@ -418,15 +420,71 @@ public class SystemUpdateApplicationService {
} }
private UpdateStatus readStatus() { private UpdateStatus readStatus() {
UpdateStatus resolved;
if (!effectiveEnabled()) { if (!effectiveEnabled()) {
return new UpdateStatus("DISABLED", "在线更新未配置", null, null); resolved = new UpdateStatus("DISABLED", "在线更新未配置", null, null);
} else {
UpdateStatus persisted = readPersistedStatus();
if (persisted != null && BUSY_STATES.contains(persisted.state())) {
resolved = persisted;
} else if (persisted != null && "FAILED".equals(persisted.state()) && hasProcessingRequest()) {
resolved = persisted;
} else {
UpdateStatus queued = pendingStatus();
resolved = queued != null
? queued
: (persisted == null ? new UpdateStatus("IDLE", "尚未执行在线更新", null, null) : persisted);
}
}
return enrichWithEmbeddedReleaseMetadata(resolved);
}
/**
* The updater which performed an older release switch may not have known about release notes yet.
* New artifacts therefore carry the signed notes inside the release directory as well. Reading that
* immutable, artifact-hashed file lets the first application process after an upgrade reconstruct the
* terminal audit snapshot without a network request or a database migration.
*/
private UpdateStatus enrichWithEmbeddedReleaseMetadata(UpdateStatus status) {
if (status == null) return null;
ReleaseMetadata metadata = readEmbeddedReleaseMetadata();
if (metadata == null) return status;
String current = currentVersion();
boolean matchesCurrent = metadata.version().equals(current);
boolean matchesTarget = status.targetVersion() != null && metadata.version().equals(status.targetVersion());
if (!matchesCurrent && !matchesTarget) return status;
String notes = status.releaseNotes() == null || status.releaseNotes().isBlank()
? metadata.releaseNotes() : status.releaseNotes();
Instant publishedAt = status.publishedAt() == null ? metadata.publishedAt() : status.publishedAt();
if (notes.equals(status.releaseNotes()) && Objects.equals(publishedAt, status.publishedAt())) return status;
return new UpdateStatus(status.state(), status.message(), status.targetVersion(), status.updatedAt(),
status.downloadedBytes(), status.totalBytes(), status.bytesPerSecond(), status.downloadPercent(),
status.restartExpectedSeconds(), status.requestId(), status.action(), notes, publishedAt);
}
private ReleaseMetadata readEmbeddedReleaseMetadata() {
Path versionFile = properties.currentVersionFile();
if (versionFile == null) return null;
Path releaseDirectory = versionFile.toAbsolutePath().normalize().getParent();
if (releaseDirectory == null) return null;
Path metadataFile = releaseDirectory.resolve("release-metadata.json").normalize();
if (!metadataFile.startsWith(releaseDirectory)
|| !Files.isRegularFile(metadataFile, LinkOption.NOFOLLOW_LINKS)
|| Files.isSymbolicLink(metadataFile)) {
return null;
}
try {
if (Files.size(metadataFile) > MAX_RELEASE_METADATA_BYTES) return null;
JsonNode root = objectMapper.readTree(Files.readAllBytes(metadataFile));
String version = blank(root.path("version").asText(null));
String notes = boundedText(root, "releaseNotes", 4000);
Instant publishedAt = parseInstant(root.path("publishedAt").asText(null));
if (version == null || !VERSION.matcher(version).matches() || notes == null) return null;
return new ReleaseMetadata(version, notes, publishedAt);
} catch (IOException | RuntimeException exception) {
log.debug("嵌入式 Release 更新日志读取失败:{}", metadataFile, exception);
return null;
} }
UpdateStatus persisted = readPersistedStatus();
if (persisted != null && BUSY_STATES.contains(persisted.state())) return persisted;
if (persisted != null && "FAILED".equals(persisted.state()) && hasProcessingRequest()) return persisted;
UpdateStatus queued = pendingStatus();
if (queued != null) return queued;
return persisted == null ? new UpdateStatus("IDLE", "尚未执行在线更新", null, null) : persisted;
} }
private UpdateStatus readPersistedStatus() { private UpdateStatus readPersistedStatus() {
@@ -702,6 +760,9 @@ public class SystemUpdateApplicationService {
String releaseNotes) { String releaseNotes) {
} }
private record ReleaseMetadata(String version, String releaseNotes, Instant publishedAt) {
}
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,
@@ -336,8 +336,44 @@ class SystemUpdateApplicationServiceTest {
"Release 1.0.0-preview.43 is running", java.time.Instant.parse("2026-08-18T23:50:00Z"), null, null); "Release 1.0.0-preview.43 is running", java.time.Instant.parse("2026-08-18T23:50:00Z"), null, null);
} }
@Test
void restoresEmbeddedReleaseNotesAfterLegacyUpdaterSwitch() throws Exception {
Path inbox = Files.createDirectory(tempDir.resolve("embedded-notes-inbox"));
Path versionFile = tempDir.resolve("current/VERSION");
Files.createDirectories(versionFile.getParent());
Files.writeString(versionFile, "1.0.0-preview.47\n");
Files.writeString(versionFile.resolveSibling("release-metadata.json"), """
{"version":"1.0.0-preview.47","publishedAt":"2026-08-19T00:28:41.701Z",
"releaseNotes":"从已签名制品恢复的更新日志"}
""");
Path statusFile = tempDir.resolve("embedded-notes-status.json");
Files.writeString(statusFile, """
{"state":"SUCCEEDED","message":"Release 1.0.0-preview.47 is running",
"targetVersion":"1.0.0-preview.47","updatedAt":"2026-08-19T00:30:00Z"}
""");
AuditService auditService = mock(AuditService.class);
when(auditService.recordSystemUpdateTerminal(null, null, "SUCCEEDED", "1.0.0-preview.47",
"Release 1.0.0-preview.47 is running", java.time.Instant.parse("2026-08-19T00:30:00Z"),
"从已签名制品恢复的更新日志", java.time.Instant.parse("2026-08-19T00:28:41.701Z")))
.thenReturn("SYSUPD:embedded-notes");
SystemUpdateApplicationService service = serviceForStatus(inbox, statusFile, auditService, versionFile);
var status = service.status();
assertEquals("从已签名制品恢复的更新日志", status.releaseNotes());
assertEquals(java.time.Instant.parse("2026-08-19T00:28:41.701Z"), status.publishedAt());
verify(auditService).recordSystemUpdateTerminal(null, null, "SUCCEEDED", "1.0.0-preview.47",
"Release 1.0.0-preview.47 is running", java.time.Instant.parse("2026-08-19T00:30:00Z"),
"从已签名制品恢复的更新日志", java.time.Instant.parse("2026-08-19T00:28:41.701Z"));
}
private SystemUpdateApplicationService serviceForStatus(Path inbox, Path statusFile, AuditService auditService) { private SystemUpdateApplicationService serviceForStatus(Path inbox, Path statusFile, AuditService auditService) {
SystemUpdateProperties properties = new SystemUpdateProperties(true, "1.0.0-preview.43", null, return serviceForStatus(inbox, statusFile, auditService, null);
}
private SystemUpdateApplicationService serviceForStatus(Path inbox, Path statusFile, AuditService auditService,
Path versionFile) {
SystemUpdateProperties properties = new SystemUpdateProperties(true, "1.0.0-preview.43", versionFile,
"https://release.fixture.invalid/", null, null, inbox.resolve("request.json"), statusFile, "https://release.fixture.invalid/", null, null, inbox.resolve("request.json"), statusFile,
Duration.ofSeconds(2), Duration.ofSeconds(2), true); Duration.ofSeconds(2), Duration.ofSeconds(2), true);
AuthorizationService authorization = mock(AuthorizationService.class); AuthorizationService authorization = mock(AuthorizationService.class);
+1 -1
View File
@@ -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.46 APP_VERSION=1.0.0-preview.48
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.
+11
View File
@@ -0,0 +1,11 @@
更新日志(Preview 48)
本版本修复在线更新完成后后台看不到 Release 更新日志的问题。
• 更新日志继续以签名 release-manifest.json 为唯一发布来源,Gitea Release 正文与清单使用同一份内容。
• 发布制品新增 release-metadata.json,并由签名制品携带版本、发布时间和完整更新说明。
• 应用重启后会从当前已验签制品恢复更新说明;即使由旧版更新器完成版本切换,也不会再丢失日志。
• 更新完成后的终态审计会保存 Release 更新说明,系统更新页按版本显示完整日志,而不是只显示“更新成功”。
• 增加发布清单与制品内元数据的一致性校验,避免版本、发布时间和更新说明发生漂移。
升级说明:本版本不要求安装 MySQL,也不会自动修改数据库服务;更新器默认跳过数据库备份,仅使用已验签的 Release 制品完成应用切换。
+28 -15
View File
@@ -94,6 +94,22 @@ if grep -Eq '127\.0\.0\.1:3307|kaidi_local_2026' \
exit 1 exit 1
fi fi
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; }
PUBLISHED_AT=$(node -e 'process.stdout.write(new Date().toISOString())')
rm -rf "$OUTPUT_DIR" rm -rf "$OUTPUT_DIR"
mkdir -p "$OUTPUT_DIR" mkdir -p "$OUTPUT_DIR"
STAGE="$WORK/stage" STAGE="$WORK/stage"
@@ -112,6 +128,15 @@ cp "$ROOT/deploy/systemd/kaidi-finance.service" "$STAGE/ops/kaidi-finance.servic
cp "$ROOT/deploy/systemd/kaidi-update.service" "$STAGE/ops/kaidi-update.service" cp "$ROOT/deploy/systemd/kaidi-update.service" "$STAGE/ops/kaidi-update.service"
cp "$ROOT/deploy/systemd/kaidi-update.path" "$STAGE/ops/kaidi-update.path" cp "$ROOT/deploy/systemd/kaidi-update.path" "$STAGE/ops/kaidi-update.path"
chmod 0755 "$STAGE/ops/update.sh" "$STAGE/ops/baota-start.sh" "$STAGE/ops/baota-init.sh" chmod 0755 "$STAGE/ops/update.sh" "$STAGE/ops/baota-start.sh" "$STAGE/ops/baota-init.sh"
VERSION="$VERSION" RELEASE_NOTES="$RELEASE_NOTES_VALUE" PUBLISHED_AT="$PUBLISHED_AT" \
node <<'NODE' > "$STAGE/release-metadata.json"
const metadata = {
version: process.env.VERSION,
publishedAt: process.env.PUBLISHED_AT,
releaseNotes: process.env.RELEASE_NOTES,
};
process.stdout.write(`${JSON.stringify(metadata, null, 2)}\n`);
NODE
if find "$STAGE" -type l -print -quit | grep -q .; then if find "$STAGE" -type l -print -quit | grep -q .; then
printf 'Release stage contains a symbolic link\n' >&2 printf 'Release stage contains a symbolic link\n' >&2
@@ -187,22 +212,9 @@ 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_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" \
PUBLISHED_AT="$PUBLISHED_AT" \
BACKEND_SBOM_SHA256="$BACKEND_SBOM_SHA256" FRONTEND_SBOM_SHA256="$FRONTEND_SBOM_SHA256" \ BACKEND_SBOM_SHA256="$BACKEND_SBOM_SHA256" FRONTEND_SBOM_SHA256="$FRONTEND_SBOM_SHA256" \
BACKEND_BUILD_VERSION="$BACKEND_BUILD_VERSION" FRONTEND_BUILD_VERSION="$FRONTEND_BUILD_VERSION" \ BACKEND_BUILD_VERSION="$BACKEND_BUILD_VERSION" FRONTEND_BUILD_VERSION="$FRONTEND_BUILD_VERSION" \
INSTALLER_SHA256="$INSTALLER_SHA256" PURGER_SHA256="$PURGER_SHA256" \ INSTALLER_SHA256="$INSTALLER_SHA256" PURGER_SHA256="$PURGER_SHA256" \
@@ -215,7 +227,8 @@ const manifest = {
artifact: process.env.ARTIFACT, artifact: process.env.ARTIFACT,
sha256: process.env.SHA256, sha256: process.env.SHA256,
artifactSizeBytes: Number(process.env.ARTIFACT_SIZE_BYTES), artifactSizeBytes: Number(process.env.ARTIFACT_SIZE_BYTES),
publishedAt: new Date().toISOString(), publishedAt: process.env.PUBLISHED_AT,
releaseMetadata: 'release-metadata.json',
minimumJava: 17, minimumJava: 17,
source: { source: {
revision: process.env.SOURCE_REVISION, revision: process.env.SOURCE_REVISION,
+21
View File
@@ -166,6 +166,27 @@ if tar -tvzf "$ARTIFACT" | grep -Eq '^l'; then
printf 'Release archive contains a symbolic link\n' >&2 printf 'Release archive contains a symbolic link\n' >&2
exit 1 exit 1
fi fi
RELEASE_METADATA_PATH=$(jq -r '.releaseMetadata // empty' release-manifest.json)
if [ -n "$RELEASE_METADATA_PATH" ]; then
[ "$RELEASE_METADATA_PATH" = "release-metadata.json" ] \
|| { printf 'Release metadata path is invalid\n' >&2; exit 1; }
tar -xOf "$ARTIFACT" "./$RELEASE_METADATA_PATH" > "$WORK/release-metadata.json" \
|| { printf 'Release archive is missing embedded release metadata\n' >&2; exit 1; }
METADATA_VERSION=$(jq -er '.version | strings | select(length > 0)' "$WORK/release-metadata.json") \
|| { printf 'Embedded release metadata version is invalid\n' >&2; exit 1; }
METADATA_NOTES=$(jq -er '.releaseNotes | strings | select(length > 0 and length <= 4000)' \
"$WORK/release-metadata.json") \
|| { printf 'Embedded release metadata notes are invalid\n' >&2; exit 1; }
METADATA_PUBLISHED_AT=$(jq -er '.publishedAt | strings | select(length > 0)' \
"$WORK/release-metadata.json") \
|| { printf 'Embedded release metadata publish time is invalid\n' >&2; exit 1; }
MANIFEST_PUBLISHED_AT=$(jq -er '.publishedAt | strings | select(length > 0)' release-manifest.json) \
|| { printf 'Release manifest publish time is invalid\n' >&2; exit 1; }
[ "$METADATA_VERSION" = "$VERSION" ] \
&& [ "$METADATA_NOTES" = "$RELEASE_NOTES" ] \
&& [ "$METADATA_PUBLISHED_AT" = "$MANIFEST_PUBLISHED_AT" ] \
|| { printf 'Embedded release metadata does not match the signed manifest\n' >&2; exit 1; }
fi
[ "$(tar -xOf "$ARTIFACT" ./VERSION)" = "$VERSION" ] \ [ "$(tar -xOf "$ARTIFACT" ./VERSION)" = "$VERSION" ] \
|| { printf 'Release archive version does not match the manifest\n' >&2; exit 1; } || { printf 'Release archive version does not match the manifest\n' >&2; exit 1; }
tar -xOf "$ARTIFACT" ./app.jar > "$WORK/app.jar" tar -xOf "$ARTIFACT" ./app.jar > "$WORK/app.jar"