feat(release): 统一 2.x 镜像标签通道 (#512)

This commit is contained in:
tbphp authored and GitHub committed 2026-08-31 17:35:12 +08:00
1 parent d6a66e9877
commit 3d14c18dcf
10 files changed
+832 -181

No files matched your search

+85
View File
@@ -0,0 +1,85 @@
#!/usr/bin/env python3
import re
import sys
VERSION_PATTERN = re.compile(
r"^v?(0|[1-9][0-9]*)\."
r"(0|[1-9][0-9]*)\."
r"(0|[1-9][0-9]*)"
r"(?:-([0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*))?$"
)
def parse_version(raw: str) -> tuple[int, int, int, list[tuple[bool, int | str]]]:
match = VERSION_PATTERN.fullmatch(raw)
if match is None:
raise ValueError(f"invalid semantic version: {raw!r}")
prerelease: list[tuple[bool, int | str]] = []
raw_prerelease = match.group(4)
if raw_prerelease is not None:
for identifier in raw_prerelease.split("."):
if identifier.isascii() and identifier.isdigit():
if len(identifier) > 1 and identifier.startswith("0"):
raise ValueError(
f"numeric prerelease identifier has a leading zero: {raw!r}"
)
prerelease.append((True, int(identifier)))
else:
prerelease.append((False, identifier))
return int(match.group(1)), int(match.group(2)), int(match.group(3)), prerelease
def compare_identifiers(
left: tuple[bool, int | str], right: tuple[bool, int | str]
) -> int:
left_numeric, left_value = left
right_numeric, right_value = right
if left_numeric != right_numeric:
return -1 if left_numeric else 1
return (left_value > right_value) - (left_value < right_value)
def compare_versions(left_raw: str, right_raw: str) -> int:
left_major, left_minor, left_patch, left_prerelease = parse_version(left_raw)
right_major, right_minor, right_patch, right_prerelease = parse_version(right_raw)
left_core = (left_major, left_minor, left_patch)
right_core = (right_major, right_minor, right_patch)
if left_core != right_core:
return (left_core > right_core) - (left_core < right_core)
if not left_prerelease and not right_prerelease:
return 0
if not left_prerelease:
return 1
if not right_prerelease:
return -1
for left_identifier, right_identifier in zip(left_prerelease, right_prerelease):
comparison = compare_identifiers(left_identifier, right_identifier)
if comparison != 0:
return comparison
return (len(left_prerelease) > len(right_prerelease)) - (
len(left_prerelease) < len(right_prerelease)
)
def main() -> int:
if len(sys.argv) != 3:
print("usage: release-compare-semver.py LEFT RIGHT", file=sys.stderr)
return 2
try:
comparison = compare_versions(sys.argv[1], sys.argv[2])
except ValueError as error:
print(error, file=sys.stderr)
return 2
print(comparison)
return 0
if __name__ == "__main__":
raise SystemExit(main())
+43
View File
@@ -0,0 +1,43 @@
#!/usr/bin/env bash
set -euo pipefail
# 读取多架构镜像内的软件版本标签。registry 返回值属于不可信输入,只有两个
# 目标架构完全一致且满足严格 SemVer(允许 Git 的 v 前缀)时才输出。
image="${1:?image is required}"
if ! inspection="$(
docker buildx imagetools inspect "${image}" --format '{{json .}}' 2>&1
)"; then
printf '%s\n' "${inspection}" >&2
exit 2
fi
semver_regex='^v?(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)(-[0-9A-Za-z-]+(\.[0-9A-Za-z-]+)*)?$'
if ! version="$(
jq -er \
--arg semver_regex "${semver_regex}" \
'[
.image["linux/amd64"].config.Labels[
"org.opencontainers.image.version"
],
.image["linux/arm64"].config.Labels[
"org.opencontainers.image.version"
]
]
| if
length == 2 and
all(type == "string") and
.[0] == .[1] and
(.[0] | test($semver_regex))
then .[0]
else error("image version labels are missing, invalid, or inconsistent")
end' \
<<<"${inspection}" 2>/dev/null
)"; then
printf 'image version labels are missing, invalid, or inconsistent for %s\n' \
"${image}" >&2
exit 1
fi
printf '%s' "${version}"
+129
View File
@@ -0,0 +1,129 @@
#!/usr/bin/env bash
set -euo pipefail
release_version="${RELEASE_VERSION:?RELEASE_VERSION is required}"
image_exact="${IMAGE_EXACT:?IMAGE_EXACT is required}"
image_beta="${IMAGE_BETA:-}"
image_major="${IMAGE_MAJOR:?IMAGE_MAJOR is required}"
promote_beta="${PROMOTE_BETA:?PROMOTE_BETA is required}"
promote_major="${PROMOTE_MAJOR:?PROMOTE_MAJOR is required}"
expected_revision="${EXPECTED_REVISION:?EXPECTED_REVISION is required}"
expected_ghcr_latest="${EXPECTED_GHCR_LATEST:?EXPECTED_GHCR_LATEST is required}"
expected_dockerhub_latest="${EXPECTED_DOCKERHUB_LATEST:?EXPECTED_DOCKERHUB_LATEST is required}"
output_file="${GITHUB_OUTPUT:?GITHUB_OUTPUT is required}"
semver_regex='^v?(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)(-[0-9A-Za-z-]+(\.[0-9A-Za-z-]+)*)?$'
digest_regex='^sha256:[0-9a-f]{64}$'
alias_regex='^[0-9]+([.][0-9]+)?(-beta)?$'
[[ "${release_version}" =~ ${semver_regex} ]]
[[ "${image_exact}" == "${release_version#v}" ]]
[[ "${image_major}" =~ ^[0-9]+$ ]]
[[ -z "${image_beta}" || "${image_beta}" =~ ${alias_regex} ]]
[[ "${promote_beta}" == "true" || "${promote_beta}" == "false" ]]
[[ "${promote_major}" == "true" || "${promote_major}" == "false" ]]
[[ "${expected_revision}" =~ ^[0-9a-f]{40}$ ]]
[[ "${expected_ghcr_latest}" == "absent" || "${expected_ghcr_latest}" =~ ${digest_regex} ]]
[[ "${expected_dockerhub_latest}" == "absent" || "${expected_dockerhub_latest}" =~ ${digest_regex} ]]
if [[ "${promote_beta}" == "true" ]]; then
test -n "${image_beta}"
fi
repositories=(ghcr.io/tbphp/gpt-load tbphp/gpt-load)
exact_digest=
for repository in "${repositories[@]}"; do
exact="${repository}:${image_exact}"
.github/scripts/release-verify-image-revision.sh \
"${exact}" "${expected_revision}" >/dev/null
digest="$(.github/scripts/release-image-digest.sh "${exact}")"
[[ "${digest}" =~ ${digest_regex} ]]
if [[ -z "${exact_digest}" ]]; then
exact_digest="${digest}"
else
test "${digest}" = "${exact_digest}"
fi
done
channel_is_current=false
promote_channel() {
local alias="$1"
local enabled="$2"
local repository=
local channel=
local current_digest=
local current_version=
local comparison=
local newer_exists=false
local expected_digest=
local source=
local promoted_digest=
local promoted_version=
channel_is_current=false
if [[ "${enabled}" != "true" ]]; then
return
fi
for repository in "${repositories[@]}"; do
channel="${repository}:${alias}"
current_digest="$(.github/scripts/release-image-digest.sh "${channel}")"
if [[ "${current_digest}" == "absent" ]]; then
continue
fi
[[ "${current_digest}" =~ ${digest_regex} ]]
current_version="$(.github/scripts/release-image-version.sh "${channel}")"
comparison="$(
python3 .github/scripts/release-compare-semver.py \
"${current_version}" "${release_version}"
)"
[[ "${comparison}" == "-1" || "${comparison}" == "0" || "${comparison}" == "1" ]]
if [[ "${comparison}" == "1" ]]; then
newer_exists=true
elif [[ "${comparison}" == "0" ]]; then
expected_digest="${exact_digest}"
if [[ "${current_digest}" != "${expected_digest}" ]]; then
printf 'channel %s already has version %s with unexpected digest\n' \
"${channel}" "${release_version}" >&2
return 1
fi
fi
done
if [[ "${newer_exists}" == "true" ]]; then
printf 'skip %s: a registry already points to a newer version than %s\n' \
"${alias}" "${release_version}"
return
fi
for repository in "${repositories[@]}"; do
channel="${repository}:${alias}"
expected_digest="${exact_digest}"
current_digest="$(.github/scripts/release-image-digest.sh "${channel}")"
if [[ "${current_digest}" != "${expected_digest}" ]]; then
source="${repository}@${expected_digest}"
docker buildx imagetools create \
--tag "${channel}" \
"${source}"
fi
promoted_digest="$(.github/scripts/release-image-digest.sh "${channel}")"
test "${promoted_digest}" = "${expected_digest}"
promoted_version="$(.github/scripts/release-image-version.sh "${channel}")"
test "${promoted_version}" = "${release_version}"
done
channel_is_current=true
}
promote_channel "${image_beta}" "${promote_beta}"
beta_current="${channel_is_current}"
promote_channel "${image_major}" "${promote_major}"
major_current="${channel_is_current}"
test "$(.github/scripts/release-image-digest.sh ghcr.io/tbphp/gpt-load:latest)" = \
"${expected_ghcr_latest}"
test "$(.github/scripts/release-image-digest.sh tbphp/gpt-load:latest)" = \
"${expected_dockerhub_latest}"
{
printf 'beta_current=%s\n' "${beta_current}"
printf 'major_current=%s\n' "${major_current}"
} >>"${output_file}"