diff --git a/README.md b/README.md index 5154c83..ae37d82 100644 --- a/README.md +++ b/README.md @@ -111,7 +111,7 @@ PostgreSQL-only 正式构建: ```bash cd oa-backend -./gradlew clean bootJar -PreleaseVersion=0.3.5 -PproductionBuild=true +./gradlew clean bootJar -PreleaseVersion=0.3.6 -PproductionBuild=true ``` 正式 JAR 必须包含 PostgreSQL 驱动,并且不得包含 `sqlite-jdbc` 或 `hibernate-community-dialects`。 @@ -214,18 +214,18 @@ curl -fsSL https://git.example.com/awaioi/ERP/raw/branch/main/install.sh \ set -e tmp="$(mktemp)" trap 'rm -f -- "$tmp"' EXIT - curl -fsSL http://38.76.196.225:10099/awaioi/ERP/raw/tag/v0.3.5/install.sh -o "$tmp" + curl -fsSL http://38.76.196.225:10099/awaioi/ERP/raw/tag/v0.3.6/install.sh -o "$tmp" printf '%s %s\n' '76917da519895bec815bb5492b12f0894e8cff26d6754cb9b2569b45e52ed83c' "$tmp" | sha256sum -c - sudo -E bash "$tmp" \ --gitea-url http://38.76.196.225:10099 \ --repository awaioi/ERP \ - --version 0.3.5 \ + --version 0.3.6 \ --public-url http://38.76.196.225:8091 \ --allow-insecure ) ``` -只有 `v0.3.5` Release 发布后这条命令才可下载安装包。固定 tag 和 SHA-256 只用于保护当前 HTTP 引导脚本不被传输途中篡改;Release 资产仍会继续执行 Ed25519 和 SHA-256 双重校验。HTTP 会暴露请求、Release 元数据和可能使用的访问令牌,不得作为长期生产方案。 +只有 `v0.3.6` Release 发布后这条命令才可下载安装包。固定 tag 和 SHA-256 只用于保护当前 HTTP 引导脚本不被传输途中篡改;Release 资产仍会继续执行 Ed25519 和 SHA-256 双重校验。HTTP 会暴露请求、Release 元数据和可能使用的访问令牌,不得作为长期生产方案。 ### 完整卸载后重装 @@ -236,7 +236,7 @@ curl -fsSL https://git.example.com/awaioi/ERP/raw/branch/main/install.sh \ set -e tmp="$(mktemp)" trap 'rm -f -- "$tmp"' EXIT - curl -fsSL http://38.76.196.225:10099/awaioi/ERP/raw/tag/v0.3.5/uninstall.sh -o "$tmp" + curl -fsSL http://38.76.196.225:10099/awaioi/ERP/raw/tag/v0.3.6/uninstall.sh -o "$tmp" printf '%s %s\n' '98c56fed2fd4d01874e4ab5a1a4f3ec42ec3e29b315ffd87385a95488d587546' "$tmp" | sha256sum -c - sudo -E bash "$tmp" --purge-database --yes ) @@ -289,9 +289,9 @@ location / { 应用定制平台 -> 系统更新 ``` -入口只对 `ADMIN` 角色显示,对应前端路由为 `/appdev/update`,后端 API 为 `/api/oa/system-update/*`。页面会显示当前版本、最新版本、检查时间、Release 更新日志、发布日期、发布文件、Ed25519/SHA-256 校验方式,以及下载、验签、安装、重启和自动回滚阶段。顶部和手机入口发现新版本时会显示版本提示。 +入口只对 `ADMIN` 角色显示,对应前端路由为 `/appdev/update`,后端 API 为 `/api/oa/system-update/*`。页面只显示当前版本、在线最新版本、检查时间、最新版本更新日志、历史正式版本,以及下载、验签、安装、重启和自动回滚进度。顶部和手机入口发现新版本时会显示版本提示。 -“更新源设置”可在线修改启用状态、Gitea 地址、`owner/repository`、正式版/预览版通道、私有仓库 Token 和 HTTP 测试开关。设置原子写回 `/etc/kaidi-erp/erp.env`,只允许修改 `OA_UPDATE_*` 白名单,不会覆盖 PostgreSQL 密码等其他配置;Token 永不通过 API 回显,页面只显示“Token 已配置”。公开仓库无需填写 Token,正式环境应使用 HTTPS。 +更新源由安装器写入服务器的 `/etc/kaidi-erp/erp.env`,后台页面不会要求管理员重复填写 Gitea 地址、仓库、Token、通道或 HTTP 开关。需要变更基础设施配置时由服务器运维人员修改 `OA_UPDATE_*` 环境变量并重启服务;公开仓库无需 Token,正式环境应使用 HTTPS。 更新过程如下: @@ -307,7 +307,7 @@ location / { 同一安装目录使用操作系统文件锁,不能并发执行两个更新任务。也可以手工触发: ```bash -/opt/kaidi-erp/current/bin/erp-update install 0.3.5 +/opt/kaidi-erp/current/bin/erp-update install 0.3.6 ``` 应用回滚不等于数据库回滚。包含不可逆 Flyway 迁移的版本必须先保证旧应用仍兼容新结构,并建议在安装配置中启用: @@ -348,8 +348,8 @@ base64 < ~/.config/kaidi-erp/release-signing-key.pem | tr -d '\n' ```bash git switch main git pull --ff-only origin main -git tag -a v0.3.5 -m 'Kaidi ERP v0.3.5' -git push origin v0.3.5 +git tag -a v0.3.6 -m 'Kaidi ERP v0.3.6' +git push origin v0.3.6 ``` 发布完成后必须确认 Release 页面存在四个资产,并使用仓库中的 `distribution/release-public-key.pem` 验证签名。私钥与该公钥不匹配时打包脚本会直接失败。 @@ -358,7 +358,7 @@ git push origin v0.3.5 ```bash ERP_RELEASE_PRIVATE_KEY_FILE="$HOME/.config/kaidi-erp/release-signing-key.pem" \ - bash scripts/package-release.sh 0.3.5 + bash scripts/package-release.sh 0.3.6 ``` ## 配置参考 diff --git a/docs/online-install-and-update.md b/docs/online-install-and-update.md index 1d4daf0..922efde 100644 --- a/docs/online-install-and-update.md +++ b/docs/online-install-and-update.md @@ -27,8 +27,8 @@ base64 < ~/.config/kaidi-erp/release-signing-key.pem | tr -d '\n' 发布稳定版本: ```bash -git tag v0.3.5 -git push origin v0.3.5 +git tag v0.3.6 +git push origin v0.3.6 ``` ## 首次安装 @@ -52,17 +52,17 @@ Linux 生产服务要求主机使用 systemd;没有 systemd 的容器、WSL 当前 `http://38.76.196.225:10099` 仅用于开发测试,安装器必须同时传入 `--allow-insecure`。在没有 HTTPS 的情况下,必须从固定 tag 下载引导脚本并验证本版本记录的 SHA-256,禁止把可变的 `main` 分支脚本直接管道给 root。HTTP 仍会暴露请求、Release 元数据和 Gitea token,不应作为生产部署方式。 -当前 `v0.3.5` 安装命令: +当前 `v0.3.6` 安装命令: ```bash ( set -e tmp="$(mktemp)" trap 'rm -f -- "$tmp"' EXIT - curl -fsSL http://38.76.196.225:10099/awaioi/ERP/raw/tag/v0.3.5/install.sh -o "$tmp" + curl -fsSL http://38.76.196.225:10099/awaioi/ERP/raw/tag/v0.3.6/install.sh -o "$tmp" printf '%s %s\n' '76917da519895bec815bb5492b12f0894e8cff26d6754cb9b2569b45e52ed83c' "$tmp" | sha256sum -c - sudo -E bash "$tmp" --gitea-url http://38.76.196.225:10099 \ - --repository awaioi/ERP --version 0.3.5 \ + --repository awaioi/ERP --version 0.3.6 \ --public-url http://38.76.196.225:8091 --allow-insecure ) ``` @@ -74,7 +74,7 @@ Linux 生产服务要求主机使用 systemd;没有 systemd 的容器、WSL set -e tmp="$(mktemp)" trap 'rm -f -- "$tmp"' EXIT - curl -fsSL http://38.76.196.225:10099/awaioi/ERP/raw/tag/v0.3.5/uninstall.sh -o "$tmp" + curl -fsSL http://38.76.196.225:10099/awaioi/ERP/raw/tag/v0.3.6/uninstall.sh -o "$tmp" printf '%s %s\n' '98c56fed2fd4d01874e4ab5a1a4f3ec42ec3e29b315ffd87385a95488d587546' "$tmp" | sha256sum -c - sudo -E bash "$tmp" --purge-database --yes ) @@ -86,9 +86,9 @@ Linux 生产服务要求主机使用 systemd;没有 systemd 的容器、WSL 管理员可从顶部工具栏、用户菜单、手机导航抽屉或“应用定制平台 -> 系统更新”进入 `/appdev/update`。入口只对 `ADMIN` 角色显示;发现新版本时顶部和手机入口会显示版本提示。 -页面的“更新源设置”可保存启用状态、Gitea 地址、`owner/repository`、正式版/预览版通道、可选 Token 和 HTTP 测试开关。配置原子写回 `ERP_CONFIG_FILE` 指向的 `erp.env`,只修改 `OA_UPDATE_*` 白名单;数据库密码等字段保持不变。Token 不会通过 API 回显,公开仓库可以留空。 +更新源由首次安装器写入 `ERP_CONFIG_FILE` 指向的 `erp.env`。更新页面不会要求管理员重复填写 Gitea 地址、仓库、Token、通道或 HTTP 开关;需要变更这些基础设施参数时,由服务器运维人员修改 `OA_UPDATE_*` 环境变量并重启服务。 -点击“保存并检查”或“检查更新”后,页面会展示当前/最新版本、发布日期、Release 更新日志、发布资产和签名校验方式。安装时持续显示下载、验签、安装、重启和回滚进度;服务重启短暂断开期间页面会自动重连。后端启动独立更新助手,更新助手会: +点击“检查更新”后,页面会展示当前版本、在线最新版本、发布日期、最新版本更新日志和历史正式版本记录。点击“安装并重启”后持续显示下载、验签、安装、重启和回滚进度;服务重启短暂断开期间页面会自动重连。后端启动独立更新助手,更新助手会: 1. 下载正式归档、`SHA256SUMS` 和签名并验证 Ed25519/SHA-256;独立安装器资产只在首次安装使用。 2. 拒绝路径穿越、符号链接和结构不完整的安装包。 @@ -99,7 +99,7 @@ Linux 生产服务要求主机使用 systemd;没有 systemd 的容器、WSL 更新过程使用操作系统文件锁,同一安装目录同时只允许一个更新任务。手动触发可执行: ```bash -/opt/kaidi-erp/current/bin/erp-update install 0.3.5 +/opt/kaidi-erp/current/bin/erp-update install 0.3.6 ``` 在线更新依赖安装器注册的 systemd 或 launchd 服务来拉起新旧版本。使用 `--no-service` 时后台更新默认关闭;如由其他进程管理器接管,须先确认它会在 ERP 进程退出后自动重启,再手工启用 `OA_UPDATE_ENABLED=true`。健康检查默认最多等待 120 秒、每 2 秒轮询一次,可分别通过 `ERP_UPDATE_HEALTH_TIMEOUT_SECONDS` 和 `ERP_UPDATE_HEALTH_POLL_SECONDS` 调整。 diff --git a/oa-backend/src/main/java/com/kaidi/oa/service/SystemUpdateService.java b/oa-backend/src/main/java/com/kaidi/oa/service/SystemUpdateService.java index 3108a47..e7c9221 100644 --- a/oa-backend/src/main/java/com/kaidi/oa/service/SystemUpdateService.java +++ b/oa-backend/src/main/java/com/kaidi/oa/service/SystemUpdateService.java @@ -47,6 +47,7 @@ public class SystemUpdateService { + "(?:\\+([0-9A-Za-z-]+(?:\\.[0-9A-Za-z-]+)*))?$"); private static final long MAX_STATE_BYTES = 64 * 1024; private static final int MAX_RELEASE_NOTES_CHARS = 32_000; + private static final int MAX_RELEASE_HISTORY = 20; private static final int MAX_RELEASE_ASSETS = 32; private static final int MAX_ASSET_NAME_CHARS = 255; private static final int MAX_ASSET_URL_CHARS = 4_096; @@ -138,6 +139,24 @@ public class SystemUpdateService { } } + public List releases() { + requireConfigured(); + List releases = new ArrayList<>(fetchReleaseHistory()); + releases.sort((left, right) -> compareVersions(right.version(), left.version())); + String latestVersion = releases.isEmpty() ? null : releases.get(0).version(); + List history = new ArrayList<>(releases.size()); + for (ReleaseInfo release : releases) { + history.add(new ReleaseHistoryItem( + release.version(), + release.publishedAt(), + release.notes(), + sameVersion(release.version(), currentVersion), + latestVersion != null && sameVersion(release.version(), latestVersion) + )); + } + return List.copyOf(history); + } + public UpdateStatus install(String requestedVersion) { requireConfigured(); if (!isVersion(requestedVersion)) { @@ -197,13 +216,57 @@ public class SystemUpdateService { } private ReleaseInfo fetchLatestRelease() { + JsonNode latest = fetchReleaseJson(releaseApiUri("/latest")); + if (!isStableChannel() || !isPrerelease(latest)) { + return parseRelease(latest); + } + + ReleaseInfo selected = null; + JsonNode selectedNode = null; + for (JsonNode node : fetchReleaseList()) { + ReleaseInfo candidate = parseReleaseHistoryItem(node); + if (candidate != null && (selected == null + || compareVersions(candidate.version(), selected.version()) > 0)) { + selected = candidate; + selectedNode = node; + } + } + if (selectedNode == null) { + throw new ApiException(404, "Gitea 尚未发布正式版本"); + } + return parseRelease(selectedNode); + } + + private List fetchReleaseHistory() { + List releases = new ArrayList<>(); + for (JsonNode node : fetchReleaseList()) { + ReleaseInfo release = parseReleaseHistoryItem(node); + if (release != null) { + releases.add(release); + } + } + return List.copyOf(releases); + } + + private JsonNode fetchReleaseList() { + JsonNode root = fetchReleaseJson(releaseApiUri("?limit=" + MAX_RELEASE_HISTORY + "&page=1")); + if (!root.isArray() || root.size() > MAX_RELEASE_HISTORY) { + throw new ApiException(502, "Gitea Release 历史数据无效"); + } + return root; + } + + private URI releaseApiUri(String suffix) { String[] repository = properties.getRepository().split("/", 2); if (repository.length != 2 || repository[0].isBlank() || repository[1].isBlank()) { throw new ApiException(500, "更新仓库配置无效"); } String base = properties.getGiteaBaseUrl().replaceAll("/+$", ""); - URI uri = URI.create(base + "/api/v1/repos/" + encode(repository[0]) + "/" - + encode(repository[1]) + "/releases/latest"); + return URI.create(base + "/api/v1/repos/" + encode(repository[0]) + "/" + + encode(repository[1]) + "/releases" + suffix); + } + + private JsonNode fetchReleaseJson(URI uri) { HttpRequest.Builder builder = HttpRequest.newBuilder(uri) .timeout(Duration.ofSeconds(Math.max(1, properties.getRequestTimeoutSeconds()))) .header("Accept", "application/json") @@ -221,7 +284,7 @@ public class SystemUpdateService { if (response.statusCode() < 200 || response.statusCode() >= 300) { throw new ApiException(502, "Gitea Release API 返回 HTTP " + response.statusCode()); } - return parseRelease(objectMapper.readTree(response.body())); + return objectMapper.readTree(response.body()); } catch (InterruptedException e) { Thread.currentThread().interrupt(); throw new ApiException(503, "检查更新被中断"); @@ -231,6 +294,22 @@ public class SystemUpdateService { } } + private ReleaseInfo parseReleaseHistoryItem(JsonNode root) { + String tag = text(root, "tag_name"); + if (!isVersion(tag) || root.path("draft").asBoolean(false)) { + return null; + } + if (isPrerelease(root) && isStableChannel()) { + return null; + } + return new ReleaseInfo( + normalizeVersion(tag), + releaseNotes(root), + releasePublishedAt(root), + List.of() + ); + } + private ReleaseInfo parseRelease(JsonNode root) { String tag = text(root, "tag_name"); if (!isVersion(tag)) { @@ -239,11 +318,7 @@ public class SystemUpdateService { if (root.path("draft").asBoolean(false)) { throw new ApiException(502, "最新 Release 仍是草稿"); } - Matcher versionMatcher = VERSION_PATTERN.matcher(tag); - versionMatcher.matches(); - boolean taggedPrerelease = versionMatcher.group(4) != null; - if ((root.path("prerelease").asBoolean(false) || taggedPrerelease) - && "stable".equalsIgnoreCase(properties.getChannel())) { + if (isPrerelease(root) && isStableChannel()) { throw new ApiException(502, "稳定频道拒绝预发布版本"); } if (!root.path("assets").isArray() || root.path("assets").size() > MAX_RELEASE_ASSETS) { @@ -268,20 +343,36 @@ public class SystemUpdateService { requireAsset(assets, "kaidi-erp-" + version + ".tar.gz"); requireAsset(assets, "SHA256SUMS"); requireAsset(assets, "SHA256SUMS.sig"); - Instant publishedAt = null; + return new ReleaseInfo(version, releaseNotes(root), releasePublishedAt(root), List.copyOf(assets)); + } + + private boolean isStableChannel() { + return "stable".equalsIgnoreCase(properties.getChannel()); + } + + private static boolean isPrerelease(JsonNode root) { + Matcher matcher = VERSION_PATTERN.matcher(text(root, "tag_name")); + return root.path("prerelease").asBoolean(false) || (matcher.matches() && matcher.group(4) != null); + } + + private static Instant releasePublishedAt(JsonNode root) { String published = text(root, "published_at"); - if (!published.isBlank()) { - try { - publishedAt = Instant.parse(published); - } catch (RuntimeException ignore) { - // An invalid optional timestamp must not hide an otherwise valid release. - } + if (published.isBlank()) { + return null; } + try { + return Instant.parse(published); + } catch (RuntimeException ignore) { + return null; + } + } + + private static String releaseNotes(JsonNode root) { String notes = text(root, "body"); if (notes.length() > MAX_RELEASE_NOTES_CHARS) { - notes = notes.substring(0, MAX_RELEASE_NOTES_CHARS) + "\n\n[发布说明过长,已截断]"; + return notes.substring(0, MAX_RELEASE_NOTES_CHARS) + "\n\n[发布说明过长,已截断]"; } - return new ReleaseInfo(version, notes, publishedAt, List.copyOf(assets)); + return notes; } private void requireConfigured() { @@ -604,6 +695,15 @@ public class SystemUpdateService { public record ReleaseAsset(String name, String downloadUrl, long size) { } + public record ReleaseHistoryItem( + String version, + Instant publishedAt, + String releaseNotes, + boolean current, + boolean latest + ) { + } + public enum UpdatePhase { IDLE, CHECKING, diff --git a/oa-backend/src/main/java/com/kaidi/oa/web/SystemUpdateController.java b/oa-backend/src/main/java/com/kaidi/oa/web/SystemUpdateController.java index d41c18a..4a5a7e2 100644 --- a/oa-backend/src/main/java/com/kaidi/oa/web/SystemUpdateController.java +++ b/oa-backend/src/main/java/com/kaidi/oa/web/SystemUpdateController.java @@ -5,6 +5,7 @@ import com.kaidi.oa.service.SystemUpdateConfigService; import com.kaidi.oa.service.SystemUpdateConfigService.UpdateConfig; import com.kaidi.oa.service.SystemUpdateConfigService.UpdateConfigRequest; import com.kaidi.oa.service.SystemUpdateService; +import com.kaidi.oa.service.SystemUpdateService.ReleaseHistoryItem; import com.kaidi.oa.service.SystemUpdateService.UpdateStatus; import jakarta.validation.Valid; import jakarta.validation.constraints.NotBlank; @@ -15,6 +16,8 @@ import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; +import java.util.List; + /** Administrator-only API for checking and installing signed Gitea releases. */ @RestController @RequestMapping("/api/oa/system-update") @@ -46,6 +49,11 @@ public class SystemUpdateController { return ApiResp.ok(updateService.status()); } + @GetMapping("/releases") + public ApiResp> releases() { + return ApiResp.ok(updateService.releases()); + } + @PostMapping("/check") public ApiResp check() { return ApiResp.ok(updateService.check()); diff --git a/oa-backend/src/test/java/com/kaidi/oa/service/SystemUpdateServiceTest.java b/oa-backend/src/test/java/com/kaidi/oa/service/SystemUpdateServiceTest.java index ba28c70..1399159 100644 --- a/oa-backend/src/test/java/com/kaidi/oa/service/SystemUpdateServiceTest.java +++ b/oa-backend/src/test/java/com/kaidi/oa/service/SystemUpdateServiceTest.java @@ -14,6 +14,7 @@ import java.nio.charset.StandardCharsets; import java.nio.file.Files; import java.nio.file.Path; import java.time.Instant; +import java.util.List; import java.util.Properties; import static org.assertj.core.api.Assertions.assertThat; @@ -79,6 +80,86 @@ class SystemUpdateServiceTest { } } + @Test + void stableChannelFallsBackToNewestStableReleaseWhenLatestIsPrerelease() throws Exception { + HttpServer server = HttpServer.create(new InetSocketAddress("127.0.0.1", 0), 0); + server.createContext("/api/v1/repos/awaioi/ERP/releases/latest", exchange -> { + byte[] body = """ + {"tag_name":"v0.3.7-rc.1","draft":false,"prerelease":true,"assets":[]} + """.getBytes(StandardCharsets.UTF_8); + exchange.sendResponseHeaders(200, body.length); + exchange.getResponseBody().write(body); + exchange.close(); + }); + server.createContext("/api/v1/repos/awaioi/ERP/releases", exchange -> { + String origin = "http://127.0.0.1:" + exchange.getLocalAddress().getPort(); + byte[] body = (""" + [ + {"tag_name":"v0.3.7-rc.1","draft":false,"prerelease":true,"assets":[]}, + {"tag_name":"v0.3.5","draft":false,"prerelease":false,"assets":[]}, + {"tag_name":"v0.3.6","body":"Latest stable","draft":false,"prerelease":false, + "published_at":"2026-08-04T10:00:00Z","assets":[ + {"name":"kaidi-erp-0.3.6.tar.gz","browser_download_url":"%s/app","size":123}, + {"name":"SHA256SUMS","browser_download_url":"%s/sums","size":64}, + {"name":"SHA256SUMS.sig","browser_download_url":"%s/sig","size":64} + ]} + ] + """).formatted(origin, origin, origin).getBytes(StandardCharsets.UTF_8); + exchange.sendResponseHeaders(200, body.length); + exchange.getResponseBody().write(body); + exchange.close(); + }); + server.start(); + try { + SystemUpdateService service = service(configuredProperties(server.getAddress().getPort()), "0.3.5"); + + SystemUpdateService.UpdateStatus status = service.check(); + + assertThat(status.latestVersion()).isEqualTo("0.3.6"); + assertThat(status.updateAvailable()).isTrue(); + assertThat(status.releaseNotes()).isEqualTo("Latest stable"); + } finally { + server.stop(0); + } + } + + @Test + void listsStableReleaseHistoryNewestFirstAndMarksCurrentVersion() throws Exception { + HttpServer server = HttpServer.create(new InetSocketAddress("127.0.0.1", 0), 0); + server.createContext("/api/v1/repos/awaioi/ERP/releases", exchange -> { + byte[] body = """ + [ + {"tag_name":"v0.3.4","body":"Older fixes","draft":false,"prerelease":false, + "published_at":"2026-08-03T10:00:00Z"}, + {"tag_name":"v0.3.6","body":"Simplified updater","draft":false,"prerelease":false, + "published_at":"2026-08-04T10:00:00Z"}, + {"tag_name":"v0.3.7-rc.1","body":"Preview","draft":false,"prerelease":true}, + {"tag_name":"v0.3.8","body":"Draft","draft":true,"prerelease":false}, + {"tag_name":"not-a-version","body":"Invalid","draft":false,"prerelease":false} + ] + """.getBytes(StandardCharsets.UTF_8); + exchange.getResponseHeaders().add("Content-Type", "application/json"); + exchange.sendResponseHeaders(200, body.length); + exchange.getResponseBody().write(body); + exchange.close(); + }); + server.start(); + try { + SystemUpdateService service = service(configuredProperties(server.getAddress().getPort()), "0.3.4"); + + List releases = service.releases(); + + assertThat(releases).extracting(SystemUpdateService.ReleaseHistoryItem::version) + .containsExactly("0.3.6", "0.3.4"); + assertThat(releases.get(0).latest()).isTrue(); + assertThat(releases.get(0).current()).isFalse(); + assertThat(releases.get(1).current()).isTrue(); + assertThat(releases.get(1).releaseNotes()).isEqualTo("Older fixes"); + } finally { + server.stop(0); + } + } + @Test void persistsReleaseMetadataAcrossServiceRestart() throws Exception { Path stateFile = Files.createTempFile("system-update-metadata-", ".json"); diff --git a/ofbiz-framework/plugins/modern-ui/app/src/oa/api/index.ts b/ofbiz-framework/plugins/modern-ui/app/src/oa/api/index.ts index ed50009..8032794 100644 --- a/ofbiz-framework/plugins/modern-ui/app/src/oa/api/index.ts +++ b/ofbiz-framework/plugins/modern-ui/app/src/oa/api/index.ts @@ -189,9 +189,8 @@ export const oaApi = { // 统一预警(全平台期限/异常聚合) listAlerts: alertsApi.listAlerts, // 系统更新 - getSystemUpdateConfig: updateApi.getSystemUpdateConfig, - saveSystemUpdateConfig: updateApi.saveSystemUpdateConfig, getSystemUpdateStatus: updateApi.getSystemUpdateStatus, + getSystemUpdateReleases: updateApi.getSystemUpdateReleases, checkSystemUpdate: updateApi.checkSystemUpdate, installSystemUpdate: updateApi.installSystemUpdate } @@ -219,7 +218,7 @@ export type { } from './reportdefs' export type { Alert } from './alerts' export type { - SystemUpdateStatus, SystemUpdateConfig, SystemUpdateConfigInput, UpdatePhase, ReleaseAsset + SystemUpdateStatus, ReleaseHistoryItem, UpdatePhase, ReleaseAsset } from './update' export type { CompanySubject, Contract, Supplier, Customer, BankAccount, Seal, Invoice, ContractMilestone diff --git a/ofbiz-framework/plugins/modern-ui/app/src/oa/api/update.ts b/ofbiz-framework/plugins/modern-ui/app/src/oa/api/update.ts index c0f8982..9a357eb 100644 --- a/ofbiz-framework/plugins/modern-ui/app/src/oa/api/update.ts +++ b/ofbiz-framework/plugins/modern-ui/app/src/oa/api/update.ts @@ -36,37 +36,22 @@ export interface SystemUpdateStatus { error: string | null } -export interface SystemUpdateConfig { - enabled: boolean - giteaBaseUrl: string - repository: string - channel: 'stable' | 'preview' - tokenConfigured: boolean - allowInsecureHttp: boolean -} - -export interface SystemUpdateConfigInput { - enabled: boolean - giteaBaseUrl: string - repository: string - channel: 'stable' | 'preview' - token: string - clearToken: boolean - allowInsecureHttp: boolean -} - -export function getSystemUpdateConfig() { - return http.get('/system-update/config') -} - -export function saveSystemUpdateConfig(input: SystemUpdateConfigInput) { - return http.put('/system-update/config', input) +export interface ReleaseHistoryItem { + version: string + publishedAt: string | null + releaseNotes: string + current: boolean + latest: boolean } export function getSystemUpdateStatus() { return http.get('/system-update/status') } +export function getSystemUpdateReleases() { + return http.get('/system-update/releases', undefined, { timeoutMs: 30000 }) +} + export function checkSystemUpdate() { return http.post('/system-update/check', undefined, { timeoutMs: 30000 }) } diff --git a/ofbiz-framework/plugins/modern-ui/app/src/oa/pages/appdev/update.vue b/ofbiz-framework/plugins/modern-ui/app/src/oa/pages/appdev/update.vue index 5ec9511..f3d5056 100644 --- a/ofbiz-framework/plugins/modern-ui/app/src/oa/pages/appdev/update.vue +++ b/ofbiz-framework/plugins/modern-ui/app/src/oa/pages/appdev/update.vue @@ -1,21 +1,13 @@