This commit is contained in:
@@ -70,7 +70,8 @@ PostgreSQL 18 兼容工作继续冻结。
|
||||
- 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`,更新器只调用已有工具,不会安装数据库。
|
||||
- `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 支持边界
|
||||
|
||||
@@ -83,7 +84,7 @@ PostgreSQL 18 兼容工作继续冻结。
|
||||
先在宝塔面板停止并删除当前错误的 Java 项目,再执行:
|
||||
|
||||
```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(包括
|
||||
@@ -96,7 +97,7 @@ curl -fsSL https://git.awaioi.com/ERP-Team/kaidi/releases/download/v1.0.0-previe
|
||||
随后执行一键清理:
|
||||
|
||||
```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` 参数丢失。
|
||||
@@ -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 一键安装**还需要
|
||||
systemd/systemd-analyze,**宝塔手动部署**不要求 systemd。32 位 Linux 需要宿主机
|
||||
@@ -136,7 +137,7 @@ systemd/systemd-analyze,**宝塔手动部署**不要求 systemd。32 位 Linux
|
||||
必须直接位于 `RELEASE_ROOT`,不能再嵌套一层目录。
|
||||
|
||||
```bash
|
||||
VERSION=1.0.0-preview.47
|
||||
VERSION=1.0.0-preview.48
|
||||
APP_ROOT=/www/wwwroot/kaidi
|
||||
RELEASE_ROOT="$APP_ROOT/releases/$VERSION"
|
||||
sudo install -d -m 0755 "$RELEASE_ROOT"
|
||||
@@ -245,7 +246,7 @@ MySQL 8.4;提前准备外部 MySQL,完成上述向导即可。systemd 在线
|
||||
`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
|
||||
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 项目;宝塔面板元数据属于外部资源,必须由面板先停用,
|
||||
@@ -269,12 +270,14 @@ Actions 页面显示 “No matching online runner”,先启动并注册该标
|
||||
工作流先建立不可见草稿,再显式上传并核对 10 个资产的名称和大小,最后才发布为 `/releases/latest`。Gitea 的
|
||||
`latest` 会排除 `prerelease=true`,因此即使 tag 名含 `preview`,发布记录的 `prerelease` 也固定为 `false`;
|
||||
Preview 属性由 SemVer 版本名表达。每个版本必须先在 `release-notes/<version>.md` 写好面向用户的更新日志。
|
||||
打包器会把同一份内容写入签名 `release-manifest.json`,后台历史从该清单读取;发布脚本也会用它生成
|
||||
Gitea Release 正文,避免三处内容不一致。之后推送 tag 即会构建、测试、签名并发布:
|
||||
打包器会把同一份内容写入签名 `release-manifest.json`,并把同一份签名元数据随应用制品写入
|
||||
`release-metadata.json`。后台在线检查优先读取 Release 清单;应用重启后若旧版更新器没有把说明写入状态文件,
|
||||
则从当前已验签制品中的元数据恢复,再写入终态审计,因此不依赖重启后的 Gitea 网络请求。发布脚本也会用
|
||||
清单生成 Gitea Release 正文,避免页面、清单和制品三处内容不一致。之后推送 tag 即会构建、测试、签名并发布:
|
||||
|
||||
```bash
|
||||
git tag v1.0.0-preview.47
|
||||
git push origin v1.0.0-preview.47
|
||||
git tag v1.0.0-preview.48
|
||||
git push origin v1.0.0-preview.48
|
||||
```
|
||||
|
||||
在线更新仍使用独立的 TDesign 页面:系统管理员进入“系统治理 → 系统更新”。更新源由 root 在
|
||||
@@ -326,7 +329,7 @@ cat /var/lib/kaidi-update/status.json
|
||||
```bash
|
||||
KAIDI_RELEASE_SIGNING_KEY=/secure/release-signing-private.pem \
|
||||
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 \
|
||||
./scripts/verify-release.sh dist/release
|
||||
```
|
||||
|
||||
+68
-7
@@ -34,6 +34,7 @@ import java.time.Instant;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Objects;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
import org.slf4j.Logger;
|
||||
@@ -47,6 +48,7 @@ public class SystemUpdateApplicationService {
|
||||
private static final Logger log = LoggerFactory.getLogger(SystemUpdateApplicationService.class);
|
||||
|
||||
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_STATUS_BYTES = 64 * 1024;
|
||||
private static final int MAX_EVENT_LOG_BYTES = 256 * 1024;
|
||||
@@ -418,15 +420,71 @@ public class SystemUpdateApplicationService {
|
||||
}
|
||||
|
||||
private UpdateStatus readStatus() {
|
||||
UpdateStatus resolved;
|
||||
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() {
|
||||
@@ -702,6 +760,9 @@ public class SystemUpdateApplicationService {
|
||||
String releaseNotes) {
|
||||
}
|
||||
|
||||
private record ReleaseMetadata(String version, String releaseNotes, Instant publishedAt) {
|
||||
}
|
||||
|
||||
private record UpdateStatus(String state, String message, String targetVersion, Instant updatedAt,
|
||||
Long downloadedBytes, Long totalBytes, Long bytesPerSecond,
|
||||
Integer downloadPercent, Integer restartExpectedSeconds,
|
||||
|
||||
+37
-1
@@ -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);
|
||||
}
|
||||
|
||||
@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) {
|
||||
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,
|
||||
Duration.ofSeconds(2), Duration.ofSeconds(2), true);
|
||||
AuthorizationService authorization = mock(AuthorizationService.class);
|
||||
|
||||
@@ -15,7 +15,7 @@ FILE_SCANNER_ENABLED=true
|
||||
FINANCE_BOOTSTRAP_ENABLED=false
|
||||
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
|
||||
FINANCE_UPDATE_ENABLED=true
|
||||
# Use either a stable direct asset base URL or the public Gitea latest-release API.
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
更新日志(Preview 48)
|
||||
|
||||
本版本修复在线更新完成后后台看不到 Release 更新日志的问题。
|
||||
|
||||
• 更新日志继续以签名 release-manifest.json 为唯一发布来源,Gitea Release 正文与清单使用同一份内容。
|
||||
• 发布制品新增 release-metadata.json,并由签名制品携带版本、发布时间和完整更新说明。
|
||||
• 应用重启后会从当前已验签制品恢复更新说明;即使由旧版更新器完成版本切换,也不会再丢失日志。
|
||||
• 更新完成后的终态审计会保存 Release 更新说明,系统更新页按版本显示完整日志,而不是只显示“更新成功”。
|
||||
• 增加发布清单与制品内元数据的一致性校验,避免版本、发布时间和更新说明发生漂移。
|
||||
|
||||
升级说明:本版本不要求安装 MySQL,也不会自动修改数据库服务;更新器默认跳过数据库备份,仅使用已验签的 Release 制品完成应用切换。
|
||||
+28
-15
@@ -94,6 +94,22 @@ if grep -Eq '127\.0\.0\.1:3307|kaidi_local_2026' \
|
||||
exit 1
|
||||
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"
|
||||
mkdir -p "$OUTPUT_DIR"
|
||||
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.path" "$STAGE/ops/kaidi-update.path"
|
||||
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
|
||||
printf 'Release stage contains a symbolic link\n' >&2
|
||||
@@ -187,22 +212,9 @@ KAIDI_PURGER_SHA256=$PURGER_SHA256
|
||||
EOF
|
||||
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" \
|
||||
RELEASE_NOTES="$RELEASE_NOTES_VALUE" \
|
||||
PUBLISHED_AT="$PUBLISHED_AT" \
|
||||
BACKEND_SBOM_SHA256="$BACKEND_SBOM_SHA256" FRONTEND_SBOM_SHA256="$FRONTEND_SBOM_SHA256" \
|
||||
BACKEND_BUILD_VERSION="$BACKEND_BUILD_VERSION" FRONTEND_BUILD_VERSION="$FRONTEND_BUILD_VERSION" \
|
||||
INSTALLER_SHA256="$INSTALLER_SHA256" PURGER_SHA256="$PURGER_SHA256" \
|
||||
@@ -215,7 +227,8 @@ const manifest = {
|
||||
artifact: process.env.ARTIFACT,
|
||||
sha256: process.env.SHA256,
|
||||
artifactSizeBytes: Number(process.env.ARTIFACT_SIZE_BYTES),
|
||||
publishedAt: new Date().toISOString(),
|
||||
publishedAt: process.env.PUBLISHED_AT,
|
||||
releaseMetadata: 'release-metadata.json',
|
||||
minimumJava: 17,
|
||||
source: {
|
||||
revision: process.env.SOURCE_REVISION,
|
||||
|
||||
@@ -166,6 +166,27 @@ if tar -tvzf "$ARTIFACT" | grep -Eq '^l'; then
|
||||
printf 'Release archive contains a symbolic link\n' >&2
|
||||
exit 1
|
||||
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" ] \
|
||||
|| { printf 'Release archive version does not match the manifest\n' >&2; exit 1; }
|
||||
tar -xOf "$ARTIFACT" ./app.jar > "$WORK/app.jar"
|
||||
|
||||
Reference in New Issue
Block a user