fix: keep release downloads on configured origin
Signed Release / release (push) Successful in 7m18s

This commit is contained in:
Qiufeng
2026-08-05 06:23:39 +08:00
parent 5d7112ce31
commit c172350200
9 changed files with 76 additions and 47 deletions
+1 -1
View File
@@ -211,7 +211,7 @@ curl -fsSL https://git.awaioi.com/awaioi/ERP/raw/branch/main/install.sh | bash
tmp="$(mktemp)" tmp="$(mktemp)"
trap 'rm -f -- "$tmp"' EXIT trap 'rm -f -- "$tmp"' EXIT
curl -fsSL https://git.awaioi.com/awaioi/ERP/raw/tag/v0.3.8/install.sh -o "$tmp" curl -fsSL https://git.awaioi.com/awaioi/ERP/raw/tag/v0.3.8/install.sh -o "$tmp"
printf '%s %s\n' '716f6a9ea75659be3c7162d031f1b1fe5e8aa0e82e0101296153842dff9b35d6' "$tmp" | sha256sum -c - printf '%s %s\n' 'ae6a6613d4abe37ba24b41e1901eaa3b29cc32939583302c7f62277922fbbe9b' "$tmp" | sha256sum -c -
sudo -E bash "$tmp" --version 0.3.8 sudo -E bash "$tmp" --version 0.3.8
) )
``` ```
+15 -7
View File
@@ -220,12 +220,15 @@ if channel.lower() == "stable" and release.get("prerelease"):
raise SystemExit("prerelease rejected on stable channel") raise SystemExit("prerelease rejected on stable channel")
if channel.lower() == "stable" and "-" in version.split("+", 1)[0]: if channel.lower() == "stable" and "-" in version.split("+", 1)[0]:
raise SystemExit("prerelease tag rejected on stable channel") raise SystemExit("prerelease tag rejected on stable channel")
assets = {str(item.get("name")): str(item.get("browser_download_url") or "") for item in release.get("assets", [])} assets = {
str(item.get("name") or "")
for item in release.get("assets", [])
if isinstance(item, dict)
}
names = [f"kaidi-erp-{version}.tar.gz", "SHA256SUMS", "SHA256SUMS.sig"] names = [f"kaidi-erp-{version}.tar.gz", "SHA256SUMS", "SHA256SUMS.sig"]
urls = [assets.get(name, "") for name in names] if any(name not in assets for name in names):
if any(not value or any(ch.isspace() for ch in value) for value in urls):
raise SystemExit("release assets are incomplete") raise SystemExit("release assets are incomplete")
print("\t".join([version, *urls])) print("\t".join([version, tag]))
PY PY
} }
@@ -371,7 +374,8 @@ install_release() {
|| fail "健康检查超时时间配置无效" || fail "健康检查超时时间配置无效"
[[ "$HEALTH_POLL_SECONDS" =~ ^[1-9][0-9]*$ ]] \ [[ "$HEALTH_POLL_SECONDS" =~ ^[1-9][0-9]*$ ]] \
|| fail "健康检查间隔配置无效" || fail "健康检查间隔配置无效"
[[ "$REPOSITORY" =~ ^[^/[:space:]]+/[^/[:space:]]+$ ]] || fail "更新仓库配置无效" [[ "$REPOSITORY" =~ ^[A-Za-z0-9][A-Za-z0-9._-]{0,99}/[A-Za-z0-9][A-Za-z0-9._-]{0,99}$ ]] \
|| fail "更新仓库配置无效"
require_command curl require_command curl
require_command python3 require_command python3
require_command tar require_command tar
@@ -386,9 +390,13 @@ install_release() {
local selection="$TMP_DIR/selection" local selection="$TMP_DIR/selection"
select_release "$release_json" "$requested" "$selection" || fail "Release 元数据验证失败" 10 select_release "$release_json" "$requested" "$selection" || fail "Release 元数据验证失败" 10
local archive_url sums_url signature_url local release_tag release_base archive_url sums_url signature_url
IFS=$'\t' read -r TARGET_VERSION archive_url sums_url signature_url < "$selection" IFS=$'\t' read -r TARGET_VERSION release_tag < "$selection"
local archive_name="kaidi-erp-${TARGET_VERSION}.tar.gz" local archive_name="kaidi-erp-${TARGET_VERSION}.tar.gz"
release_base="${GITEA_BASE_URL%/}/$owner/$repo/releases/download/$release_tag"
archive_url="$release_base/$archive_name"
sums_url="$release_base/SHA256SUMS"
signature_url="$release_base/SHA256SUMS.sig"
local archive="$TMP_DIR/$archive_name" sums="$TMP_DIR/SHA256SUMS" signature="$TMP_DIR/SHA256SUMS.sig" local archive="$TMP_DIR/$archive_name" sums="$TMP_DIR/SHA256SUMS" signature="$TMP_DIR/SHA256SUMS.sig"
write_state DOWNLOADING 20 "正在下载版本 ${TARGET_VERSION}" "$TARGET_VERSION" write_state DOWNLOADING 20 "正在下载版本 ${TARGET_VERSION}" "$TARGET_VERSION"
+1 -1
View File
@@ -59,7 +59,7 @@ Linux 生产服务要求主机使用 systemd;没有 systemd 的容器、WSL
tmp="$(mktemp)" tmp="$(mktemp)"
trap 'rm -f -- "$tmp"' EXIT trap 'rm -f -- "$tmp"' EXIT
curl -fsSL https://git.awaioi.com/awaioi/ERP/raw/tag/v0.3.8/install.sh -o "$tmp" curl -fsSL https://git.awaioi.com/awaioi/ERP/raw/tag/v0.3.8/install.sh -o "$tmp"
printf '%s %s\n' '716f6a9ea75659be3c7162d031f1b1fe5e8aa0e82e0101296153842dff9b35d6' "$tmp" | sha256sum -c - printf '%s %s\n' 'ae6a6613d4abe37ba24b41e1901eaa3b29cc32939583302c7f62277922fbbe9b' "$tmp" | sha256sum -c -
sudo -E bash "$tmp" --version 0.3.8 sudo -E bash "$tmp" --version 0.3.8
) )
``` ```
+16 -7
View File
@@ -200,7 +200,8 @@ download() {
} }
download_release() { download_release() {
[[ "$REPOSITORY" =~ ^[^/[:space:]]+/[^/[:space:]]+$ ]] || fail 'invalid Gitea repository' [[ "$REPOSITORY" =~ ^[A-Za-z0-9][A-Za-z0-9._-]{0,99}/[A-Za-z0-9][A-Za-z0-9._-]{0,99}$ ]] \
|| fail 'invalid Gitea repository'
local owner="${REPOSITORY%%/*}" repo="${REPOSITORY#*/}" local owner="${REPOSITORY%%/*}" repo="${REPOSITORY#*/}"
local release_json="$TMP_DIR/release.json" local release_json="$TMP_DIR/release.json"
download "${GITEA_BASE_URL%/}/api/v1/repos/$owner/$repo/releases/latest" "$release_json" \ download "${GITEA_BASE_URL%/}/api/v1/repos/$owner/$repo/releases/latest" "$release_json" \
@@ -223,17 +224,20 @@ if "-" in version.split("+", 1)[0]:
requested = sys.argv[2].removeprefix("v") requested = sys.argv[2].removeprefix("v")
if requested and requested != version: if requested and requested != version:
raise SystemExit("requested version does not match latest release") raise SystemExit("requested version does not match latest release")
assets = {str(a.get("name")): str(a.get("browser_download_url") or "") for a in release.get("assets", [])} assets = {
str(a.get("name") or "")
for a in release.get("assets", [])
if isinstance(a, dict)
}
names = [ names = [
f"kaidi-erp-{version}.tar.gz", f"kaidi-erp-{version}.tar.gz",
f"kaidi-erp-installer-{version}.jar", f"kaidi-erp-installer-{version}.jar",
"SHA256SUMS", "SHA256SUMS",
"SHA256SUMS.sig", "SHA256SUMS.sig",
] ]
urls = [assets.get(name, "") for name in names] if any(name not in assets for name in names):
if any(not url or any(ch.isspace() for ch in url) for url in urls):
raise SystemExit("release assets missing") raise SystemExit("release assets missing")
print("\t".join([version, *urls])) print("\t".join([version, tag]))
PY PY
then then
local reason="invalid Release metadata" local reason="invalid Release metadata"
@@ -241,10 +245,15 @@ PY
fail "$reason" fail "$reason"
fi fi
local archive_url installer_url sums_url signature_url local release_tag release_base archive_url installer_url sums_url signature_url
IFS=$'\t' read -r VERSION archive_url installer_url sums_url signature_url < "$selection" IFS=$'\t' read -r VERSION release_tag < "$selection"
ARCHIVE_NAME="kaidi-erp-${VERSION}.tar.gz" ARCHIVE_NAME="kaidi-erp-${VERSION}.tar.gz"
INSTALLER_NAME="kaidi-erp-installer-${VERSION}.jar" INSTALLER_NAME="kaidi-erp-installer-${VERSION}.jar"
release_base="${GITEA_BASE_URL%/}/$owner/$repo/releases/download/$release_tag"
archive_url="$release_base/$ARCHIVE_NAME"
installer_url="$release_base/$INSTALLER_NAME"
sums_url="$release_base/SHA256SUMS"
signature_url="$release_base/SHA256SUMS.sig"
ARCHIVE_PATH="$TMP_DIR/$ARCHIVE_NAME" ARCHIVE_PATH="$TMP_DIR/$ARCHIVE_NAME"
INSTALLER_PATH="$TMP_DIR/$INSTALLER_NAME" INSTALLER_PATH="$TMP_DIR/$INSTALLER_NAME"
download "$archive_url" "$ARCHIVE_PATH" download "$archive_url" "$ARCHIVE_PATH"
@@ -50,7 +50,6 @@ public class SystemUpdateService {
private static final int MAX_RELEASE_HISTORY = 20; private static final int MAX_RELEASE_HISTORY = 20;
private static final int MAX_RELEASE_ASSETS = 32; private static final int MAX_RELEASE_ASSETS = 32;
private static final int MAX_ASSET_NAME_CHARS = 255; private static final int MAX_ASSET_NAME_CHARS = 255;
private static final int MAX_ASSET_URL_CHARS = 4_096;
private final UpdateProperties properties; private final UpdateProperties properties;
private final ObjectMapper objectMapper; private final ObjectMapper objectMapper;
@@ -266,6 +265,16 @@ public class SystemUpdateService {
+ encode(repository[1]) + "/releases" + suffix); + encode(repository[1]) + "/releases" + suffix);
} }
private URI releaseAssetUri(String tag, String name) {
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("/+$", "");
return URI.create(base + "/" + encode(repository[0]) + "/" + encode(repository[1])
+ "/releases/download/" + encode(tag) + "/" + encode(name));
}
private JsonNode fetchReleaseJson(URI uri) { private JsonNode fetchReleaseJson(URI uri) {
HttpRequest.Builder builder = HttpRequest.newBuilder(uri) HttpRequest.Builder builder = HttpRequest.newBuilder(uri)
.timeout(Duration.ofSeconds(Math.max(1, properties.getRequestTimeoutSeconds()))) .timeout(Duration.ofSeconds(Math.max(1, properties.getRequestTimeoutSeconds())))
@@ -327,15 +336,12 @@ public class SystemUpdateService {
List<ReleaseAsset> assets = new ArrayList<>(); List<ReleaseAsset> assets = new ArrayList<>();
for (JsonNode node : root.path("assets")) { for (JsonNode node : root.path("assets")) {
String name = text(node, "name"); String name = text(node, "name");
String downloadUrl = text(node, "browser_download_url"); if (name.isBlank() || name.length() > MAX_ASSET_NAME_CHARS) {
if (name.isBlank() || name.length() > MAX_ASSET_NAME_CHARS
|| downloadUrl.isBlank() || downloadUrl.length() > MAX_ASSET_URL_CHARS) {
throw new ApiException(502, "Release 文件信息无效"); throw new ApiException(502, "Release 文件信息无效");
} }
validateAssetUrl(downloadUrl);
assets.add(new ReleaseAsset( assets.add(new ReleaseAsset(
name, name,
downloadUrl, releaseAssetUri(tag, name).toString(),
node.path("size").asLong(0) node.path("size").asLong(0)
)); ));
} }
@@ -43,8 +43,7 @@ class SystemUpdateServiceTest {
void checksLatestGiteaReleaseAndRequiresSignedAssetSet() throws Exception { void checksLatestGiteaReleaseAndRequiresSignedAssetSet() throws Exception {
HttpServer server = HttpServer.create(new InetSocketAddress("127.0.0.1", 0), 0); HttpServer server = HttpServer.create(new InetSocketAddress("127.0.0.1", 0), 0);
server.createContext("/api/v1/repos/awaioi/ERP/releases/latest", exchange -> { server.createContext("/api/v1/repos/awaioi/ERP/releases/latest", exchange -> {
String origin = "http://127.0.0.1:" + exchange.getLocalAddress().getPort(); byte[] body = """
byte[] body = ("""
{ {
"tag_name": "v0.2.0", "tag_name": "v0.2.0",
"body": "PostgreSQL production release", "body": "PostgreSQL production release",
@@ -52,12 +51,12 @@ class SystemUpdateServiceTest {
"prerelease": false, "prerelease": false,
"published_at": "2026-08-03T10:00:00Z", "published_at": "2026-08-03T10:00:00Z",
"assets": [ "assets": [
{"name":"kaidi-erp-0.2.0.tar.gz","browser_download_url":"%s/app","size":123}, {"name":"kaidi-erp-0.2.0.tar.gz","browser_download_url":"http://legacy.invalid/app","size":123},
{"name":"SHA256SUMS","browser_download_url":"%s/sums","size":64}, {"name":"SHA256SUMS","browser_download_url":"http://legacy.invalid/sums","size":64},
{"name":"SHA256SUMS.sig","browser_download_url":"%s/sig","size":64} {"name":"SHA256SUMS.sig","browser_download_url":"http://legacy.invalid/sig","size":64}
] ]
} }
""").formatted(origin, origin, origin).getBytes(StandardCharsets.UTF_8); """.getBytes(StandardCharsets.UTF_8);
exchange.getResponseHeaders().add("Content-Type", "application/json"); exchange.getResponseHeaders().add("Content-Type", "application/json");
exchange.sendResponseHeaders(200, body.length); exchange.sendResponseHeaders(200, body.length);
exchange.getResponseBody().write(body); exchange.getResponseBody().write(body);
@@ -75,6 +74,9 @@ class SystemUpdateServiceTest {
assertThat(status.latestVersion()).isEqualTo("0.2.0"); assertThat(status.latestVersion()).isEqualTo("0.2.0");
assertThat(status.assets()).extracting(SystemUpdateService.ReleaseAsset::name) assertThat(status.assets()).extracting(SystemUpdateService.ReleaseAsset::name)
.containsExactly("kaidi-erp-0.2.0.tar.gz", "SHA256SUMS", "SHA256SUMS.sig"); .containsExactly("kaidi-erp-0.2.0.tar.gz", "SHA256SUMS", "SHA256SUMS.sig");
assertThat(status.assets()).extracting(SystemUpdateService.ReleaseAsset::downloadUrl)
.allMatch(url -> url.startsWith("http://127.0.0.1:" + server.getAddress().getPort()
+ "/awaioi/ERP/releases/download/v0.2.0/"));
} finally { } finally {
server.stop(0); server.stop(0);
} }
@@ -198,7 +200,7 @@ class SystemUpdateServiceTest {
} }
@Test @Test
void rejectsReleaseAssetsFromAnotherOrigin() throws Exception { void ignoresReleaseAssetOriginsAndBuildsTrustedDownloadUrls() throws Exception {
HttpServer server = HttpServer.create(new InetSocketAddress("127.0.0.1", 0), 0); HttpServer server = HttpServer.create(new InetSocketAddress("127.0.0.1", 0), 0);
server.createContext("/api/v1/repos/awaioi/ERP/releases/latest", exchange -> { server.createContext("/api/v1/repos/awaioi/ERP/releases/latest", exchange -> {
byte[] body = """ byte[] body = """
@@ -214,9 +216,11 @@ class SystemUpdateServiceTest {
server.start(); server.start();
try { try {
SystemUpdateService service = service(configuredProperties(server.getAddress().getPort()), "0.3.0"); SystemUpdateService service = service(configuredProperties(server.getAddress().getPort()), "0.3.0");
assertThatThrownBy(service::check) SystemUpdateService.UpdateStatus status = service.check();
.isInstanceOf(ApiException.class)
.hasMessageContaining("未受信任"); assertThat(status.assets()).extracting(SystemUpdateService.ReleaseAsset::downloadUrl)
.allMatch(url -> url.startsWith("http://127.0.0.1:" + server.getAddress().getPort()
+ "/awaioi/ERP/releases/download/v0.4.0/"));
} finally { } finally {
server.stop(0); server.stop(0);
} }
+1 -1
View File
@@ -3,7 +3,7 @@ set -Eeuo pipefail
TARGET=/root/kaidi-erp-reinstall-v038.sh TARGET=/root/kaidi-erp-reinstall-v038.sh
URL=https://git.awaioi.com/awaioi/ERP/raw/tag/v0.3.8/reinstall-centos9.sh URL=https://git.awaioi.com/awaioi/ERP/raw/tag/v0.3.8/reinstall-centos9.sh
SHA256=77c21df88b23596ce0b0ffd40c1eb341c7a0653871cdd359ef13af22c6ffe8c4 SHA256=a8496345fa5aaf6fb55180805098fb0e05bdc68b21c455b1c0b1901bba57fedd
curl -fsSL --connect-timeout 15 --max-time 300 "$URL" -o "$TARGET" curl -fsSL --connect-timeout 15 --max-time 300 "$URL" -o "$TARGET"
printf '%s %s\n' "$SHA256" "$TARGET" | sha256sum -c - printf '%s %s\n' "$SHA256" "$TARGET" | sha256sum -c -
+1 -1
View File
@@ -5,7 +5,7 @@ GITEA_BASE_URL="https://git.awaioi.com"
REPOSITORY="awaioi/ERP" REPOSITORY="awaioi/ERP"
VERSION="0.3.8" VERSION="0.3.8"
TAG="v${VERSION}" TAG="v${VERSION}"
INSTALL_SHA256="716f6a9ea75659be3c7162d031f1b1fe5e8aa0e82e0101296153842dff9b35d6" INSTALL_SHA256="ae6a6613d4abe37ba24b41e1901eaa3b29cc32939583302c7f62277922fbbe9b"
UNINSTALL_SHA256="98c56fed2fd4d01874e4ab5a1a4f3ec42ec3e29b315ffd87385a95488d587546" UNINSTALL_SHA256="98c56fed2fd4d01874e4ab5a1a4f3ec42ec3e29b315ffd87385a95488d587546"
PG_ROOT="${ERP_RECOVERY_PG_ROOT:-/www/server/pgsql}" PG_ROOT="${ERP_RECOVERY_PG_ROOT:-/www/server/pgsql}"
PG_DATA="${ERP_RECOVERY_PG_DATA:-/www/server/pgsql/data}" PG_DATA="${ERP_RECOVERY_PG_DATA:-/www/server/pgsql/data}"
+15 -13
View File
@@ -323,22 +323,23 @@ class Handler(BaseHTTPRequestHandler):
def do_GET(self): def do_GET(self):
if self.path == "/api/v1/repos/awaioi/ERP/releases/latest": if self.path == "/api/v1/repos/awaioi/ERP/releases/latest":
base = f"http://127.0.0.1:{self.server.server_port}/assets" legacy = "http://legacy.invalid/assets"
payload = { payload = {
"tag_name": "v" + version, "tag_name": "v" + version,
"draft": False, "draft": False,
"prerelease": False, "prerelease": False,
"assets": [ "assets": [
{"name": archive, "browser_download_url": base + "/" + archive}, {"name": archive, "browser_download_url": legacy + "/" + archive},
{"name": f"kaidi-erp-installer-{version}.jar", "browser_download_url": base + f"/kaidi-erp-installer-{version}.jar"}, {"name": f"kaidi-erp-installer-{version}.jar", "browser_download_url": legacy + f"/kaidi-erp-installer-{version}.jar"},
{"name": "SHA256SUMS", "browser_download_url": base + "/SHA256SUMS"}, {"name": "SHA256SUMS", "browser_download_url": legacy + "/SHA256SUMS"},
{"name": "SHA256SUMS.sig", "browser_download_url": base + "/SHA256SUMS.sig"}, {"name": "SHA256SUMS.sig", "browser_download_url": legacy + "/SHA256SUMS.sig"},
], ],
} }
self.send_bytes(200, json.dumps(payload).encode(), "application/json") self.send_bytes(200, json.dumps(payload).encode(), "application/json")
return return
if self.path.startswith("/assets/"): release_prefix = f"/awaioi/ERP/releases/download/v{version}/"
name = self.path.removeprefix("/assets/") if self.path.startswith(release_prefix):
name = self.path.removeprefix(release_prefix)
if name not in {archive, f"kaidi-erp-installer-{version}.jar", "SHA256SUMS", "SHA256SUMS.sig"}: if name not in {archive, f"kaidi-erp-installer-{version}.jar", "SHA256SUMS", "SHA256SUMS.sig"}:
self.send_bytes(404, b"not found", "text/plain") self.send_bytes(404, b"not found", "text/plain")
return return
@@ -437,15 +438,15 @@ class Handler(BaseHTTPRequestHandler):
def do_GET(self): def do_GET(self):
if self.path == "/api/v1/repos/awaioi/ERP/releases/latest": if self.path == "/api/v1/repos/awaioi/ERP/releases/latest":
base = f"http://127.0.0.1:{self.server.server_port}" legacy = "http://legacy.invalid"
payload = { payload = {
"tag_name": "v2.0.0", "tag_name": "v2.0.0",
"draft": False, "draft": False,
"prerelease": False, "prerelease": False,
"assets": [ "assets": [
{"name": "kaidi-erp-2.0.0.tar.gz", "browser_download_url": base + "/assets/kaidi-erp-2.0.0.tar.gz"}, {"name": "kaidi-erp-2.0.0.tar.gz", "browser_download_url": legacy + "/assets/kaidi-erp-2.0.0.tar.gz"},
{"name": "SHA256SUMS", "browser_download_url": base + "/assets/SHA256SUMS"}, {"name": "SHA256SUMS", "browser_download_url": legacy + "/assets/SHA256SUMS"},
{"name": "SHA256SUMS.sig", "browser_download_url": base + "/assets/SHA256SUMS.sig"}, {"name": "SHA256SUMS.sig", "browser_download_url": legacy + "/assets/SHA256SUMS.sig"},
], ],
} }
self.send_bytes(200, json.dumps(payload).encode(), "application/json") self.send_bytes(200, json.dumps(payload).encode(), "application/json")
@@ -455,8 +456,9 @@ class Handler(BaseHTTPRequestHandler):
healthy = current == "2.0.0" if mode == "success" else current == "1.0.0" healthy = current == "2.0.0" if mode == "success" else current == "1.0.0"
self.send_bytes(200 if healthy else 503, b'{"status":"UP"}' if healthy else b'{"status":"DOWN"}', "application/json") self.send_bytes(200 if healthy else 503, b'{"status":"UP"}' if healthy else b'{"status":"DOWN"}', "application/json")
return return
if self.path.startswith("/assets/"): release_prefix = "/awaioi/ERP/releases/download/v2.0.0/"
name = self.path.removeprefix("/assets/") if self.path.startswith(release_prefix):
name = self.path.removeprefix(release_prefix)
if name not in {"kaidi-erp-2.0.0.tar.gz", "SHA256SUMS", "SHA256SUMS.sig"}: if name not in {"kaidi-erp-2.0.0.tar.gz", "SHA256SUMS", "SHA256SUMS.sig"}:
self.send_bytes(404, b"not found", "text/plain") self.send_bytes(404, b"not found", "text/plain")
return return