Files
gpt-load/.github/workflows/release.yml
T
tbphp d4bbb9193a feat: 升级 Go 与 SDK 版本 (#485)
* feat(gateway): 升级 SDK 并封口兼容合同

* fix: 修正升级后的格式与模型过滤

* fix(ci): 更新 Go 漏洞扫描器

* fix(gateway): 拒绝非标准服务层级字段
2026-08-27 22:48:16 +08:00

1367 lines
52 KiB
YAML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
name: Release 2.x
on:
push:
tags:
- "v2.*"
permissions:
contents: read
# 竖线分隔在任何错误(含网络错误)时回退到下一个源;默认的逗号只在
# 404/410 时回退,proxy.golang.org 瞬时故障会直接让整个 job 失败。
env:
GOPROXY: https://proxy.golang.org|direct
concurrency:
group: release-${{ github.ref }}
cancel-in-progress: false
jobs:
validate-tag:
runs-on: ubuntu-24.04
timeout-minutes: 5
permissions:
contents: read
# 读取同一 commit 上的 CI 结论需要 actions:read。
actions: read
outputs:
version: ${{ steps.tag.outputs.version }}
prerelease: ${{ steps.tag.outputs.prerelease }}
image_exact: ${{ steps.tag.outputs.image_exact }}
image_minor: ${{ steps.tag.outputs.image_minor }}
image_major: ${{ steps.tag.outputs.image_major }}
beta: ${{ steps.tag.outputs.beta }}
ci_verified: ${{ steps.ci.outputs.ci_verified }}
steps:
- name: Checkout tag commit
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
fetch-depth: 0
- name: Validate strict 2.x SemVer tag
id: tag
shell: bash
run: |
# release-tag-validation:start
tag="${GITHUB_REF_NAME}"
semver_regex='^v2\.(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)(-[0-9A-Za-z-]+(\.[0-9A-Za-z-]+)*)?$'
if [[ ! "${tag}" =~ ${semver_regex} ]]; then
printf 'invalid 2.x release tag\n' >&2
exit 1
fi
version="${tag#v}"
core="${version%%-*}"
prerelease=false
beta=false
if [[ "${version}" == *-* ]]; then
prerelease=true
prerelease_value="${version#*-}"
if [[ "${prerelease_value}" == "beta" ||
"${prerelease_value}" == beta.* ]]; then
beta=true
fi
IFS='.' read -r -a identifiers <<<"${prerelease_value}"
for identifier in "${identifiers[@]}"; do
if [[ "${identifier}" =~ ^[0-9]+$ ]] &&
[[ "${identifier}" != "0" ]] &&
[[ "${identifier}" == 0* ]]; then
printf 'numeric prerelease identifiers cannot contain leading zeroes\n' >&2
exit 1
fi
done
fi
IFS='.' read -r major minor _ <<<"${core}"
{
printf 'version=%s\n' "${tag}"
printf 'prerelease=%s\n' "${prerelease}"
printf 'image_exact=%s\n' "${tag}"
printf 'image_minor=%s.%s\n' "${major}" "${minor}"
printf 'image_major=%s\n' "${major}"
printf 'beta=%s\n' "${beta}"
} >>"${GITHUB_OUTPUT}"
# release-tag-validation:end
- name: Require tag commit in v2 history
shell: bash
run: |
git fetch --no-tags origin v2
git merge-base --is-ancestor "${GITHUB_SHA}" origin/v2
- name: Reuse CI verdict for this commit
id: ci
shell: bash
env:
GH_TOKEN: ${{ github.token }}
run: |
set -euo pipefail
# race 测试、CPA 桥接测试与数据库合同都是同一 commit 上的确定性验证,
# ci.yml 已经整体成功过时重跑不会得出不同结论,只会让关键路径多等约 8 分钟。
# 查不到成功记录、查询失败或结论不是 success 时一律回落为全量重跑。
# release-ci-verdict-reuse:start
runs=query-failed
ci_verified=false
if runs="$(
gh api \
"repos/${GITHUB_REPOSITORY}/actions/workflows/ci.yml/runs?head_sha=${GITHUB_SHA}&per_page=100" \
--jq '[
.workflow_runs[]
| select(.status == "completed" and .conclusion == "success")
] | length'
)" && [[ "${runs}" =~ ^[0-9]+$ ]] && ((runs > 0)); then
ci_verified=true
fi
# release-ci-verdict-reuse:end
printf 'ci_verified=%s\n' "${ci_verified}" >>"${GITHUB_OUTPUT}"
printf 'successful ci.yml runs for %s: %s -> ci_verified=%s\n' \
"${GITHUB_SHA}" "${runs}" "${ci_verified}"
verify-and-build-web:
needs: validate-tag
runs-on: ubuntu-24.04
timeout-minutes: 10
steps:
- name: Checkout tag commit
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
- name: Validate and build web UI
uses: ./.github/actions/web-ci
- name: Upload verified web dist
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: verified-web-dist
path: internal/webui/dist
if-no-files-found: error
# 仅在本次 run 内传递给 build-binaries,不作为发布产物保留。
retention-days: 1
static-checks:
needs: validate-tag
runs-on: ubuntu-24.04
timeout-minutes: 20
steps:
- name: Checkout tag commit
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
- name: Set up Go
uses: actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e # v7.0.0
with:
go-version-file: go.mod
cache: true
- name: Check Go formatting
run: |
# go-format-check:start
formatted_files="$(gofmt -l .)"
test -z "${formatted_files}"
# go-format-check:end
- name: Check module graph
run: go mod tidy -diff
- name: Check embedded CPA bridge module
working-directory: third_party/cpaembedded
run: |
go mod tidy -diff
go vet ./...
- name: Audit Go dependencies
run: go run golang.org/x/vuln/cmd/govulncheck@v1.7.0 ./...
- name: Run Go vet
run: go vet ./...
- name: Build backend
run: go build -trimpath -o gpt-load .
- name: Check repository invariants
run: git diff --check
race-tests:
needs: validate-tag
# 同一 commit 的 CI 已全绿时跳过;publication-preflight 接受 skipped 结果。
if: ${{ needs.validate-tag.outputs.ci_verified != 'true' }}
runs-on: ubuntu-24.04
timeout-minutes: 20
steps:
- name: Checkout tag commit
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
- name: Mark race-instrumented Go cache scope
# race-tests、test、database-contract(*) 默认共用同一个基于 go.sum 的
# setup-go 缓存 key;命中即不保存,race-instrumented GOCACHE 永远无法
# 写回,每次都要重新编译。加入这个 marker 文件参与 cache-dependency-path
# 哈希,让 race-tests 拥有独立 key 命名空间。
run: echo race-tests > .go-cache-scope
- name: Set up Go
uses: actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e # v7.0.0
with:
go-version-file: go.mod
cache: true
cache-dependency-path: |
go.sum
.go-cache-scope
- name: Run race-enabled tests
run: go test -race -count=1 -timeout=15m . ./internal/...
race-cpa:
needs: validate-tag
if: ${{ needs.validate-tag.outputs.ci_verified != 'true' }}
runs-on: ubuntu-24.04
timeout-minutes: 10
steps:
- name: Checkout tag commit
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
- name: Set up Go
uses: actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e # v7.0.0
with:
go-version-file: go.mod
cache: true
cache-dependency-path: third_party/cpaembedded/go.sum
- name: Run embedded CPA bridge race-enabled tests
working-directory: third_party/cpaembedded
run: go test -race -count=1 -timeout=15m ./...
database-contract:
name: Database contract (${{ matrix.driver }})
needs: validate-tag
if: ${{ needs.validate-tag.outputs.ci_verified != 'true' }}
runs-on: ubuntu-24.04
timeout-minutes: 15
strategy:
fail-fast: false
max-parallel: 2
matrix:
include:
- driver: mysql
dsn: mysql://root:root@127.0.0.1:3306/gpt_load?charset=utf8mb4&transaction_isolation=%27READ-COMMITTED%27
- driver: postgres
dsn: postgres://postgres:postgres@127.0.0.1:5432/gpt_load?sslmode=disable
services:
mysql:
image: mysql:8.4
env:
MYSQL_ROOT_PASSWORD: root
MYSQL_DATABASE: gpt_load
ports:
- 3306:3306
options: >-
--health-cmd="mysqladmin ping -h 127.0.0.1 -proot"
--health-interval=5s
--health-timeout=10s
--health-retries=20
postgres:
image: postgres:18
env:
POSTGRES_PASSWORD: postgres
POSTGRES_DB: gpt_load
ports:
- 5432:5432
options: >-
--health-cmd="pg_isready -U postgres -d gpt_load"
--health-interval=5s
--health-timeout=10s
--health-retries=20
steps:
- name: Checkout tag commit
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
- name: Set up Go
uses: actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e # v7.0.0
with:
go-version-file: go.mod
cache: true
- name: Run external database contract
env:
GPT_LOAD_DATABASE_TEST_DSN: ${{ matrix.dsn }}
run: |
go test -v -count=1 ./internal/storage ./internal/control ./internal/requestlog -run '^TestExternal(PostgresMigrationLockTimesOut|Database(ConcurrentMigrations|IncrementalMigrations|Lifecycle|ModelPriceIdentityUsesExactComparison|ReadSnapshotUsesRepeatableRead|MySQLInterruptedBaselineRecovery|MySQLRecoversPartialErrorDecisionMigration|GroupPriceReconciliation|RequestLogLifecycle))$'
build-binaries:
needs: verify-and-build-web
runs-on: ubuntu-24.04
timeout-minutes: 15
strategy:
fail-fast: true
max-parallel: 5
matrix:
include:
- goos: linux
goarch: amd64
filename: gpt-load-linux-amd64
- goos: linux
goarch: arm64
filename: gpt-load-linux-arm64
- goos: darwin
goarch: amd64
filename: gpt-load-macos-amd64
- goos: darwin
goarch: arm64
filename: gpt-load-macos-arm64
- goos: windows
goarch: amd64
filename: gpt-load-windows-amd64.exe
steps:
- name: Checkout tag commit
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
- name: Download verified web dist
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
with:
name: verified-web-dist
path: internal/webui/dist
- name: Set up Go
uses: actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e # v7.0.0
with:
go-version-file: go.mod
cache: true
- name: Build release binary
env:
CGO_ENABLED: 0
GOOS: ${{ matrix.goos }}
GOARCH: ${{ matrix.goarch }}
run: |
mkdir -p release
go build -trimpath \
-ldflags "-s -w -X gpt-load/internal/platform/version.Version=${{ github.ref_name }}" \
-o "release/${{ matrix.filename }}" .
- name: Upload release binary
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: binary-${{ matrix.filename }}
path: release/${{ matrix.filename }}
if-no-files-found: error
# native-artifact-smoke 按需下载此产物;保留期需与 release-assets
# 一致,否则失败的 run 在超过较短保留期后重试会因产物过期而无法下载。
retention-days: 14
package-metadata:
needs: validate-tag
runs-on: ubuntu-24.04
timeout-minutes: 10
steps:
- name: Checkout tag commit
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
- name: Set up Go
uses: actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e # v7.0.0
with:
go-version-file: go.mod
cache: true
- name: Generate release SBOM and license assets
run: |
mkdir -p release
cp LICENSE release/LICENSE
cp THIRD_PARTY_NOTICES.md release/THIRD_PARTY_NOTICES.md
cp LICENSES/Apache-2.0.txt release/Apache-2.0.txt
cp LICENSES/MIT.txt release/MIT.txt
cp LICENSES/MPL-2.0.txt release/MPL-2.0.txt
go run github.com/CycloneDX/cyclonedx-gomod/cmd/cyclonedx-gomod@v1.10.0 \
mod -json -noserial -notimestamp -output release/bom.cdx.json .
- name: Upload release metadata
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: release-metadata
path: release
if-no-files-found: error
# 仅在本次 run 内汇总进 release-assets,不单独保留。
retention-days: 1
package-checksums:
needs:
- build-binaries
- package-metadata
runs-on: ubuntu-24.04
timeout-minutes: 10
steps:
- name: Checkout tag commit
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
- name: Download all release binaries
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
with:
pattern: binary-gpt-load-*
path: release
merge-multiple: true
- name: Download release metadata
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
with:
name: release-metadata
path: release
- name: Generate SHA256SUMS
shell: bash
run: |
set -euo pipefail
# 校验和覆盖 .github/release-assets.txt 中除 SHA256SUMS 自身外的全部资产。
grep -vFx 'SHA256SUMS' .github/release-assets.txt >/tmp/checksum-targets
cd release
xargs -a /tmp/checksum-targets -d '\n' sha256sum >SHA256SUMS
- name: Verify complete release asset set
run: .github/scripts/release-verify-assets.sh release
- name: Upload complete release assets
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: release-assets
path: release
if-no-files-found: error
retention-days: 14
- name: Upload release checksums
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: release-checksums
path: release/SHA256SUMS
if-no-files-found: error
# 供 native-artifact-smoke 按需下载单个校验和文件;保留期需与
# release-assets 一致,否则失败的 run 延迟重试时可能已过期。
retention-days: 14
native-artifact-smoke:
# package-checksums 已经依赖 build-binaries,无需重复声明。
needs: package-checksums
timeout-minutes: 10
strategy:
fail-fast: true
max-parallel: 5
matrix:
include:
- runner: ubuntu-24.04
filename: gpt-load-linux-amd64
- runner: ubuntu-24.04-arm
filename: gpt-load-linux-arm64
- runner: macos-15-intel
filename: gpt-load-macos-amd64
- runner: macos-15
filename: gpt-load-macos-arm64
- runner: windows-2025
filename: gpt-load-windows-amd64.exe
runs-on: ${{ matrix.runner }}
steps:
- name: Checkout tag commit
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
- name: Download release binary
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
with:
name: binary-${{ matrix.filename }}
path: native-smoke
- name: Download release checksums
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
with:
name: release-checksums
path: native-smoke
- name: Smoke POSIX native artifact
if: runner.os != 'Windows'
shell: bash
env:
RELEASE_VERSION: ${{ github.ref_name }}
RELEASE_SMOKE_BINARY: ${{ github.workspace }}/native-smoke/${{ matrix.filename }}
RELEASE_SMOKE_CHECKSUM_FILE: ${{ github.workspace }}/native-smoke/SHA256SUMS
run: .github/scripts/release-native-smoke.sh
- name: Smoke Windows native artifact
if: runner.os == 'Windows'
shell: pwsh
env:
RELEASE_VERSION: ${{ github.ref_name }}
RELEASE_SMOKE_BINARY: ${{ github.workspace }}/native-smoke/${{ matrix.filename }}
RELEASE_SMOKE_CHECKSUM_FILE: ${{ github.workspace }}/native-smoke/SHA256SUMS
run: |
& .github/scripts/release-native-smoke.ps1
exit $LASTEXITCODE
docker-smoke:
needs: validate-tag
runs-on: ubuntu-24.04
timeout-minutes: 30
steps:
- name: Checkout tag commit
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
- name: Build and smoke local image
shell: bash
env:
RELEASE_VERSION: ${{ github.ref_name }}
RELEASE_SMOKE_SUFFIX: ${{ github.run_id }}
RELEASE_SMOKE_TRIVY_IMAGE: aquasec/trivy:0.72.0@sha256:cffe3f5161a47a6823fbd23d985795b3ed72a4c806da4c4df16266c02accdd6f
run: .github/scripts/release-docker-smoke.sh
prebuilt-image-smoke:
name: Prebuilt image smoke (${{ matrix.arch }})
# publish-images 实际推送的是 prebuilt target,必须在发布前用完全相同的打包
# 路径做 smoke 与漏洞扫描;docker-smoke 覆盖的源码自包含构建是另一条路径,
# 两者互相不能替代,删除任一个都会让一条发布路径失去发布前验证。
needs: build-binaries
strategy:
fail-fast: false
matrix:
include:
# publish-images 推送 linux/amd64 与 linux/arm64 两个平台,两条 prebuilt
# 路径 COPY 的是不同的二进制,必须各自验证。使用原生 arm64 runner 而非
# QEMU:QEMU 模拟下的启动行为不能代表真实 arm64 运行时。
- arch: amd64
runner: ubuntu-24.04
- arch: arm64
runner: ubuntu-24.04-arm
runs-on: ${{ matrix.runner }}
timeout-minutes: 30
steps:
- name: Checkout tag commit
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
- name: Download prebuilt Linux binary
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
with:
name: binary-gpt-load-linux-${{ matrix.arch }}
path: release
- name: Build and smoke prebuilt image
shell: bash
env:
RELEASE_VERSION: ${{ github.ref_name }}
RELEASE_SMOKE_SUFFIX: prebuilt-${{ matrix.arch }}-${{ github.run_id }}
RELEASE_SMOKE_TRIVY_IMAGE: aquasec/trivy:0.72.0@sha256:cffe3f5161a47a6823fbd23d985795b3ed72a4c806da4c4df16266c02accdd6f
RELEASE_SMOKE_BUILD_TARGET: prebuilt
run: .github/scripts/release-docker-smoke.sh
publication-preflight:
# 这里必须逐个列出全部 gate,即使其中几个已被别的 needs 间接依赖:
# 下面的 if 只能看到直接 needs 的 result,上游失败会让中间 job 变成 skipped,
# 漏列任何一个都会让它的 failure 无法被察觉。清理 needs 前先读这条注释。
needs:
- validate-tag
- verify-and-build-web
- static-checks
- race-tests
- race-cpa
- build-binaries
- package-checksums
- native-artifact-smoke
- docker-smoke
- prebuilt-image-smoke
- database-contract
# race-tests、race-cpa 与 database-contract 在 CI 已验证同一 commit 时为 skipped,
# 这里只拒绝真正失败的 gate,不把 skipped 当作缺失的验证。
if: ${{ !cancelled() && !contains(needs.*.result, 'failure') }}
runs-on: ubuntu-24.04
timeout-minutes: 20
permissions:
# Draft Releases are visible only to identities with push access.
contents: write
packages: read
outputs:
publication_state: ${{ steps.classify.outputs.publication_state }}
write_mode: ${{ steps.classify.outputs.write_mode }}
github_state: ${{ steps.inventory.outputs.github_state }}
github_target_sha: ${{ steps.inventory.outputs.github_target_sha }}
github_assets: ${{ steps.inventory.outputs.github_assets }}
ghcr_state: ${{ steps.inventory.outputs.ghcr_state }}
ghcr_digest: ${{ steps.inventory.outputs.ghcr_digest }}
ghcr_revision: ${{ steps.inventory.outputs.ghcr_revision }}
dockerhub_state: ${{ steps.inventory.outputs.dockerhub_state }}
dockerhub_digest: ${{ steps.inventory.outputs.dockerhub_digest }}
dockerhub_revision: ${{ steps.inventory.outputs.dockerhub_revision }}
ghcr_latest_digest: ${{ steps.inventory.outputs.ghcr_latest_digest }}
dockerhub_latest_digest: ${{ steps.inventory.outputs.dockerhub_latest_digest }}
steps:
- name: Checkout tag commit
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
fetch-depth: 0
- name: Download complete release assets
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
with:
name: release-assets
path: release
- name: Verify current-run tag and release assets
shell: bash
run: |
set -euo pipefail
test "$(git rev-parse HEAD)" = "${GITHUB_SHA}"
test "$(git rev-parse "${GITHUB_REF_NAME}^{commit}")" = "${GITHUB_SHA}"
git fetch --no-tags origin v2
git merge-base --is-ancestor "${GITHUB_SHA}" origin/v2
- name: Verify complete release asset set
run: .github/scripts/release-verify-assets.sh release
- name: Require publication credentials and verify read access
shell: bash
env:
GH_TOKEN: ${{ github.token }}
DOCKERHUB_USERNAME: ${{ secrets.DOCKERHUB_USERNAME }}
# 只读 token 是可选降级项,缺失时各只读 job 会回落到 DOCKERHUB_TOKEN;
# 这里声明的是各 job 实际会用到的解析结果,确保发布前它们都可用。
DOCKERHUB_READ_TOKEN: ${{ secrets.DOCKERHUB_READ_TOKEN || secrets.DOCKERHUB_TOKEN }}
DOCKERHUB_TOKEN: ${{ secrets.DOCKERHUB_TOKEN }}
run: |
set -euo pipefail
for credential in \
GH_TOKEN \
DOCKERHUB_USERNAME \
DOCKERHUB_READ_TOKEN \
DOCKERHUB_TOKEN; do
if [[ -z "${!credential}" ]]; then
printf 'required publication credential is missing: %s\n' \
"${credential}" >&2
exit 1
fi
done
gh auth status
gh api "repos/${GITHUB_REPOSITORY}" --silent
- name: Log in to GHCR for read-only inventory
uses: docker/login-action@371161bbe7024a29a25c5e19bfcbc0804fe9ad2c # v4.5.2
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Log in to Docker Hub for read-only inventory
uses: docker/login-action@371161bbe7024a29a25c5e19bfcbc0804fe9ad2c # v4.5.2
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_READ_TOKEN || secrets.DOCKERHUB_TOKEN }}
- name: Inventory publication channels
id: inventory
env:
GH_TOKEN: ${{ github.token }}
shell: bash
run: |
set -euo pipefail
github_state=absent
github_release_id=
github_target_sha=
github_assets=absent
github_draft=
github_prerelease=
if gh release view "${GITHUB_REF_NAME}" \
--repo "${GITHUB_REPOSITORY}" \
--json databaseId,targetCommitish,isDraft,isPrerelease,assets \
>/tmp/existing-release.json 2>/tmp/existing-release.error; then
github_state=present
github_release_id="$(
jq -r '.databaseId // empty' /tmp/existing-release.json
)"
github_target_sha="$(
jq -r '.targetCommitish // empty' /tmp/existing-release.json
)"
github_draft="$(
jq -r '.isDraft' /tmp/existing-release.json
)"
github_prerelease="$(
jq -r '.isPrerelease' /tmp/existing-release.json
)"
[[ "${github_release_id}" =~ ^[1-9][0-9]*$ ]]
test "${github_draft}" = "true"
test "${github_prerelease}" = \
"${{ needs.validate-tag.outputs.prerelease }}"
jq -r '.assets[].name' /tmp/existing-release.json |
LC_ALL=C sort >/tmp/existing-release-assets
github_assets=mismatch
if cmp .github/release-assets.txt /tmp/existing-release-assets; then
rm -rf existing-release
mkdir existing-release
gh release download "${GITHUB_REF_NAME}" \
--repo "${GITHUB_REPOSITORY}" \
--dir existing-release
# 已发布资产必须与本次 run 的 SHA256SUMS 逐字节一致;
# 传入可信副本后,远端那份 SHA256SUMS 只参与比对,不会被执行。
if .github/scripts/release-verify-assets.sh \
existing-release release/SHA256SUMS; then
github_assets=match
fi
fi
elif grep -Eqi \
'HTTP 404|Not Found|release not found' \
/tmp/existing-release.error; then
github_state=absent
else
cat /tmp/existing-release.error >&2
exit 1
fi
inventory_image() {
local image="$1"
local digest=
local revision=
local status=0
digest="$(.github/scripts/release-image-digest.sh "${image}")"
if [[ "${digest}" == "absent" ]]; then
printf 'absent|absent|\n'
return
fi
# 退出码 1 表示确实读到了不匹配的 revision,降级为 mismatch 交给分类器;
# 其他非零表示无法完成检查,此时让 job 失败以区分基础设施问题。
revision="$(
.github/scripts/release-verify-image-revision.sh \
"${image}" "${GITHUB_SHA}"
)" || status=$?
if ((status != 0)); then
test "${status}" = "1"
revision=mismatch
fi
printf 'present|%s|%s\n' "${digest}" "${revision}"
}
# 用命令替换而非进程替换:函数内的失败必须终止本 step,
# 而不是让 read 静默读到空状态。
ghcr_inventory="$(
inventory_image \
"ghcr.io/tbphp/gpt-load:${{ needs.validate-tag.outputs.image_exact }}"
)"
dockerhub_inventory="$(
inventory_image \
"tbphp/gpt-load:${{ needs.validate-tag.outputs.image_exact }}"
)"
IFS='|' read -r ghcr_state ghcr_digest ghcr_revision \
<<<"${ghcr_inventory}"
IFS='|' read -r dockerhub_state dockerhub_digest dockerhub_revision \
<<<"${dockerhub_inventory}"
ghcr_latest_digest="$(
.github/scripts/release-image-digest.sh ghcr.io/tbphp/gpt-load:latest
)"
dockerhub_latest_digest="$(
.github/scripts/release-image-digest.sh tbphp/gpt-load:latest
)"
{
printf 'github_state=%s\n' "${github_state}"
printf 'github_target_sha=%s\n' "${github_target_sha}"
printf 'github_assets=%s\n' "${github_assets}"
printf 'ghcr_state=%s\n' "${ghcr_state}"
printf 'ghcr_digest=%s\n' "${ghcr_digest}"
printf 'ghcr_revision=%s\n' "${ghcr_revision}"
printf 'dockerhub_state=%s\n' "${dockerhub_state}"
printf 'dockerhub_digest=%s\n' "${dockerhub_digest}"
printf 'dockerhub_revision=%s\n' "${dockerhub_revision}"
printf 'ghcr_latest_digest=%s\n' "${ghcr_latest_digest}"
printf 'dockerhub_latest_digest=%s\n' \
"${dockerhub_latest_digest}"
} >>"${GITHUB_OUTPUT}"
printf '%s\n' \
"github=${github_state}/${github_target_sha}/${github_assets}/${github_draft}/${github_prerelease}" \
"ghcr=${ghcr_state}/${ghcr_digest}/${ghcr_revision}" \
"dockerhub=${dockerhub_state}/${dockerhub_digest}/${dockerhub_revision}" \
"ghcr_latest=${ghcr_latest_digest}" \
"dockerhub_latest=${dockerhub_latest_digest}"
- name: Classify publication state
id: classify
shell: bash
env:
RELEASE_EXPECTED_SHA: ${{ github.sha }}
RELEASE_GITHUB_STATE: ${{ steps.inventory.outputs.github_state }}
RELEASE_GITHUB_TARGET_SHA: ${{ steps.inventory.outputs.github_target_sha }}
RELEASE_GITHUB_ASSETS: ${{ steps.inventory.outputs.github_assets }}
RELEASE_GHCR_STATE: ${{ steps.inventory.outputs.ghcr_state }}
RELEASE_GHCR_DIGEST: ${{ steps.inventory.outputs.ghcr_digest }}
RELEASE_GHCR_REVISION: ${{ steps.inventory.outputs.ghcr_revision }}
RELEASE_DOCKERHUB_STATE: ${{ steps.inventory.outputs.dockerhub_state }}
RELEASE_DOCKERHUB_DIGEST: ${{ steps.inventory.outputs.dockerhub_digest }}
RELEASE_DOCKERHUB_REVISION: ${{ steps.inventory.outputs.dockerhub_revision }}
run: |
set -euo pipefail
classification="$(.github/scripts/release-publication-state.sh)"
printf '%s\n' "${classification}"
while IFS='=' read -r key value; do
case "${key}" in
publication_state | write_mode)
printf '%s=%s\n' "${key}" "${value}" >>"${GITHUB_OUTPUT}"
;;
*)
printf 'unexpected classifier output\n' >&2
exit 1
;;
esac
done <<<"${classification}"
- name: Reject partial or conflicting publication
shell: bash
env:
WRITE_MODE: ${{ steps.classify.outputs.write_mode }}
run: |
set -euo pipefail
write_mode="${WRITE_MODE}"
test "${write_mode}" != "blocked"
publish-images:
needs:
- validate-tag
- publication-preflight
# GitHub Actions 的默认 job 条件会沿整条依赖链传播 skip:只要 race-tests 等
# 间接祖先因 CI 结论复用而 skipped,publish-images 也会被默认条件连带跳过,
# 即便这里列出的直接 needs 全部成功。显式条件只看直接 needs,绕开这个传播。
if: ${{ !cancelled() && needs.validate-tag.result == 'success' && needs.publication-preflight.result == 'success' }}
runs-on: ubuntu-24.04
timeout-minutes: 45
permissions:
contents: read
packages: write
steps:
- name: Checkout tag commit
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
- name: Download prebuilt Linux binaries
if: ${{ needs.publication-preflight.outputs.write_mode == 'publish' }}
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
with:
pattern: binary-gpt-load-linux-*
path: release
merge-multiple: true
- name: Set up QEMU
if: ${{ needs.publication-preflight.outputs.write_mode == 'publish' }}
uses: docker/setup-qemu-action@96fe6ef7f33517b61c61be40b68a1882f3264fb8 # v4.2.0
with:
platforms: arm64
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@bb05f3f5519dd87d3ba754cc423b652a5edd6d2c # v4.2.0
- name: Log in to Docker Hub for exact verification
if: ${{ needs.publication-preflight.outputs.write_mode == 'verify' }}
uses: docker/login-action@371161bbe7024a29a25c5e19bfcbc0804fe9ad2c # v4.5.2
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_READ_TOKEN || secrets.DOCKERHUB_TOKEN }}
- name: Log in to Docker Hub for publication
if: ${{ needs.publication-preflight.outputs.write_mode == 'publish' }}
uses: docker/login-action@371161bbe7024a29a25c5e19bfcbc0804fe9ad2c # v4.5.2
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Log in to GHCR
uses: docker/login-action@371161bbe7024a29a25c5e19bfcbc0804fe9ad2c # v4.5.2
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Generate exact image metadata
if: ${{ needs.publication-preflight.outputs.write_mode == 'publish' }}
id: meta
uses: docker/metadata-action@dc802804100637a589fabce1cb79ff13a1411302 # v6.2.0
with:
images: |
ghcr.io/tbphp/gpt-load
tbphp/gpt-load
tags: |
type=raw,value=${{ needs.validate-tag.outputs.image_exact }}
labels: |
org.opencontainers.image.source=${{ github.server_url }}/${{ github.repository }}
org.opencontainers.image.revision=${{ github.sha }}
org.opencontainers.image.version=${{ github.ref_name }}
- name: Build and publish exact multi-platform images
if: ${{ needs.publication-preflight.outputs.write_mode == 'publish' }}
uses: docker/build-push-action@53b7df96c91f9c12dcc8a07bcb9ccacbed38856a # v7.3.0
with:
context: .
# prebuilt 直接打包 build-binaries 已交叉编译好的产物,不在镜像内重复
# 编译 Go 与 Web;镜像内二进制与同名 native 发布产物是同一个文件。
target: prebuilt
platforms: linux/amd64,linux/arm64
push: true
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
build-args: |
VERSION=${{ github.ref_name }}
- name: Verify exact published images
shell: bash
run: |
set -euo pipefail
verify_exact_image() {
local image="$1"
.github/scripts/release-verify-image-revision.sh \
"${image}" "${GITHUB_SHA}" >/dev/null
.github/scripts/release-image-digest.sh "${image}"
}
ghcr_exact_digest="$(
verify_exact_image \
"ghcr.io/tbphp/gpt-load:${{ needs.validate-tag.outputs.image_exact }}"
)"
dockerhub_exact_digest="$(
verify_exact_image \
"tbphp/gpt-load:${{ needs.validate-tag.outputs.image_exact }}"
)"
test -n "${ghcr_exact_digest}"
test "${ghcr_exact_digest}" = "${dockerhub_exact_digest}"
- name: Update beta channel alias
if: ${{ needs.publication-preflight.outputs.write_mode == 'publish' && needs.validate-tag.outputs.beta == 'true' }}
shell: bash
run: |
set -euo pipefail
for repository in ghcr.io/tbphp/gpt-load tbphp/gpt-load; do
exact="${repository}:${{ needs.validate-tag.outputs.image_exact }}"
docker buildx imagetools create \
--tag "${repository}:v2beta" \
"${exact}"
done
- name: Update stable major and minor aliases
if: ${{ needs.publication-preflight.outputs.write_mode == 'publish' && needs.validate-tag.outputs.prerelease == 'false' }}
shell: bash
run: |
set -euo pipefail
for repository in ghcr.io/tbphp/gpt-load tbphp/gpt-load; do
exact="${repository}:${{ needs.validate-tag.outputs.image_exact }}"
docker buildx imagetools create \
--tag "${repository}:${{ needs.validate-tag.outputs.image_minor }}" \
"${exact}"
docker buildx imagetools create \
--tag "${repository}:${{ needs.validate-tag.outputs.image_major }}" \
"${exact}"
done
publish-github:
needs:
- validate-tag
- publication-preflight
- publish-images
# 同上:显式条件只看直接 needs,避免默认条件把间接祖先的 skip 传播下来。
if: ${{ !cancelled() && needs.validate-tag.result == 'success' && needs.publication-preflight.result == 'success' && needs.publish-images.result == 'success' }}
runs-on: ubuntu-24.04
timeout-minutes: 10
permissions:
contents: write
steps:
- name: Checkout tag commit
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
- name: Download complete release assets
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
with:
name: release-assets
path: release
# 上传步骤按 release/* 通配收集,这里的清单校验就是它的边界保证。
- name: Verify complete release asset set
run: .github/scripts/release-verify-assets.sh release
- name: Verify existing GitHub Release
if: ${{ needs.publication-preflight.outputs.write_mode == 'verify' }}
env:
GH_TOKEN: ${{ github.token }}
shell: bash
run: |
set -euo pipefail
metadata="$(
gh release view "${GITHUB_REF_NAME}" \
--repo "${GITHUB_REPOSITORY}" \
--json targetCommitish,isDraft,isPrerelease
)"
test "$(jq -r '.targetCommitish' <<<"${metadata}")" = "${GITHUB_SHA}"
test "$(jq -r '.isDraft' <<<"${metadata}")" = "true"
test "$(jq -r '.isPrerelease' <<<"${metadata}")" = \
"${{ needs.validate-tag.outputs.prerelease }}"
if latest_tag="$(
gh api \
"repos/${GITHUB_REPOSITORY}/releases/latest" \
--jq '.tag_name' 2>/tmp/latest-release.error
)"; then
test "${latest_tag}" != "${GITHUB_REF_NAME}"
elif ! grep -Eqi \
'HTTP 404|Not Found' \
/tmp/latest-release.error; then
cat /tmp/latest-release.error >&2
exit 1
fi
rm -rf existing-release
mkdir existing-release
gh release download "${GITHUB_REF_NAME}" \
--repo "${GITHUB_REPOSITORY}" \
--dir existing-release
.github/scripts/release-verify-assets.sh \
existing-release release/SHA256SUMS
- name: Create or update GitHub Release draft
if: ${{ needs.publication-preflight.outputs.write_mode == 'publish' }}
uses: softprops/action-gh-release@3d0d9888cb7fd7b750713d6e236d1fcb99157228 # v3.0.2
with:
name: ${{ github.ref_name }}
target_commitish: ${{ github.sha }}
draft: true
prerelease: ${{ needs.validate-tag.outputs.prerelease }}
make_latest: false
generate_release_notes: true
fail_on_unmatched_files: true
# 目录内容已由上面的清单校验锁定为 .github/release-assets.txt 的全集。
files: release/*
body: |
2.x is a rewrite and is not compatible with 1.x data, start 2.x from an empty database and a new `encryption.key`.
---
2.x 是全新重写版本,与 1.x 数据不兼容,请以全新数据库和新的 `encryption.key` 部署 2.x。
post-publish-image-smoke:
# GHCR 与 Docker Hub 的 exact tag 已在 publish-images 断言为同一 digest,
# 因此完整运行时 smoke 只需跑一遍;Docker Hub 侧的可拉取性与 digest 一致性
# 由 post-publish-verify 覆盖。
needs:
- validate-tag
- publish-images
# 同上:显式条件只看直接 needs,避免默认条件把间接祖先的 skip 传播下来。
if: ${{ !cancelled() && needs.validate-tag.result == 'success' && needs.publish-images.result == 'success' }}
runs-on: ubuntu-24.04
timeout-minutes: 30
permissions:
contents: read
packages: read
steps:
- name: Checkout tag commit
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
- name: Log in to GHCR for runtime smoke
uses: docker/login-action@371161bbe7024a29a25c5e19bfcbc0804fe9ad2c # v4.5.2
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Run full runtime smoke for published exact image
shell: bash
env:
RELEASE_VERSION: ${{ github.ref_name }}
RELEASE_SMOKE_SOURCE_IMAGE: ghcr.io/tbphp/gpt-load:${{ needs.validate-tag.outputs.image_exact }}
RELEASE_SMOKE_SUFFIX: ${{ github.run_id }}-ghcr
# 同一 commit 的镜像已在 docker-smoke 扫描过,这里不重复扫描。
RELEASE_SMOKE_SKIP_SCAN: "true"
run: .github/scripts/release-docker-smoke.sh
post-publish-verify:
needs:
- validate-tag
- publication-preflight
- publish-github
- publish-images
# 同上:显式条件只看直接 needs,避免默认条件把间接祖先的 skip 传播下来。
if: ${{ !cancelled() && needs.validate-tag.result == 'success' && needs.publication-preflight.result == 'success' && needs.publish-github.result == 'success' && needs.publish-images.result == 'success' }}
runs-on: ubuntu-24.04
timeout-minutes: 45
permissions:
# Draft Releases are visible only to identities with push access.
contents: write
packages: read
steps:
- name: Checkout tag commit
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
- name: Download complete release assets
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
with:
name: release-assets
path: release
- name: Download and verify exact GitHub Release asset inventory
env:
GH_TOKEN: ${{ github.token }}
shell: bash
run: |
set -euo pipefail
mkdir -p draft-release
gh release download "${GITHUB_REF_NAME}" \
--repo "${GITHUB_REPOSITORY}" \
--dir draft-release
# 以本次 run 的可信 SHA256SUMS 为准校验已上传的 draft 资产,
# 而不是相信 draft 自带的那一份。
.github/scripts/release-verify-assets.sh \
draft-release release/SHA256SUMS
- name: Verify GitHub Release channel metadata
env:
GH_TOKEN: ${{ github.token }}
shell: bash
run: |
set -euo pipefail
metadata="$(
gh release view "${GITHUB_REF_NAME}" \
--repo "${GITHUB_REPOSITORY}" \
--json targetCommitish,isDraft,isPrerelease
)"
test "$(jq -r '.targetCommitish' <<<"${metadata}")" = "${GITHUB_SHA}"
test "$(jq -r '.isDraft' <<<"${metadata}")" = "true"
test "$(jq -r '.isPrerelease' <<<"${metadata}")" = \
"${{ needs.validate-tag.outputs.prerelease }}"
if latest_tag="$(
gh api \
"repos/${GITHUB_REPOSITORY}/releases/latest" \
--jq '.tag_name' 2>/tmp/latest-release.error
)"; then
test "${latest_tag}" != "${GITHUB_REF_NAME}"
elif ! grep -Eqi \
'HTTP 404|Not Found' \
/tmp/latest-release.error; then
cat /tmp/latest-release.error >&2
exit 1
fi
- name: Log in to GHCR for manifest verification
uses: docker/login-action@371161bbe7024a29a25c5e19bfcbc0804fe9ad2c # v4.5.2
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Log in to Docker Hub for manifest verification
uses: docker/login-action@371161bbe7024a29a25c5e19bfcbc0804fe9ad2c # v4.5.2
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_READ_TOKEN || secrets.DOCKERHUB_TOKEN }}
- name: Verify published image manifests
shell: bash
run: |
set -euo pipefail
ghcr_exact_digest=
dockerhub_exact_digest=
for repository in ghcr.io/tbphp/gpt-load tbphp/gpt-load; do
exact="${repository}:${{ needs.validate-tag.outputs.image_exact }}"
.github/scripts/release-verify-image-revision.sh \
"${exact}" "${GITHUB_SHA}" >/dev/null
exact_digest="$(
.github/scripts/release-image-digest.sh "${exact}"
)"
test -n "${exact_digest}"
if [[ "${repository}" == ghcr.io/* ]]; then
ghcr_exact_digest="${exact_digest}"
latest_image="ghcr.io/tbphp/gpt-load:latest"
expected_latest_digest="${{ needs.publication-preflight.outputs.ghcr_latest_digest }}"
else
dockerhub_exact_digest="${exact_digest}"
latest_image="tbphp/gpt-load:latest"
expected_latest_digest="${{ needs.publication-preflight.outputs.dockerhub_latest_digest }}"
fi
latest_digest="$(.github/scripts/release-image-digest.sh "${latest_image}")"
test "${latest_digest}" = "${expected_latest_digest}"
test "${latest_digest}" != "${exact_digest}"
if [[ "${{ needs.validate-tag.outputs.beta }}" == "true" ]]; then
beta_digest="$(
.github/scripts/release-image-digest.sh \
"${repository}:v2beta"
)"
test "${beta_digest}" = "${exact_digest}"
fi
if [[ "${{ needs.validate-tag.outputs.prerelease }}" == "false" ]]; then
for alias in \
"${{ needs.validate-tag.outputs.image_minor }}" \
"${{ needs.validate-tag.outputs.image_major }}"; do
alias_digest="$(
docker buildx imagetools inspect \
"${repository}:${alias}" \
--format '{{.Manifest.Digest}}'
)"
test "${alias_digest}" = "${exact_digest}"
done
fi
done
test "${ghcr_exact_digest}" = "${dockerhub_exact_digest}"
- name: Verify Docker Hub exact image is pullable
shell: bash
env:
DOCKERHUB_EXACT_IMAGE: tbphp/gpt-load:${{ needs.validate-tag.outputs.image_exact }}
run: |
set -euo pipefail
# 完整运行时 smoke 只在 GHCR 侧跑一遍。manifest 校验只读取 config blob,
# 不保证 layer 已完整提交,这里补上 Docker Hub 侧的 layer 可达性证据。
docker pull --platform linux/amd64 "${DOCKERHUB_EXACT_IMAGE}"
test "$(
docker image inspect -f '{{.Config.User}}' "${DOCKERHUB_EXACT_IMAGE}"
)" = "10001:10001"
reconcile-publication:
if: ${{ always() }}
needs:
- publication-preflight
- publish-images
- publish-github
- post-publish-image-smoke
- post-publish-verify
runs-on: ubuntu-24.04
timeout-minutes: 10
permissions:
# Draft Releases are visible only to identities with push access.
contents: write
packages: read
steps:
- name: Checkout tag commit for read-only reconciliation
if: ${{ always() }}
continue-on-error: true
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
- name: Log in to GHCR for read-only reconciliation
if: ${{ always() }}
continue-on-error: true
uses: docker/login-action@371161bbe7024a29a25c5e19bfcbc0804fe9ad2c # v4.5.2
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Log in to Docker Hub for read-only reconciliation
if: ${{ always() }}
continue-on-error: true
uses: docker/login-action@371161bbe7024a29a25c5e19bfcbc0804fe9ad2c # v4.5.2
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_READ_TOKEN || secrets.DOCKERHUB_TOKEN }}
- name: Summarize exact publication inventory and job results
if: ${{ always() }}
continue-on-error: true
env:
GH_TOKEN: ${{ github.token }}
PREFLIGHT_RESULT: ${{ needs.publication-preflight.result }}
IMAGE_WRITER_RESULT: ${{ needs.publish-images.result }}
GITHUB_WRITER_RESULT: ${{ needs.publish-github.result }}
IMAGE_SMOKE_RESULT: ${{ needs.post-publish-image-smoke.result }}
VERIFY_POST_RESULT: ${{ needs.post-publish-verify.result }}
shell: bash
run: |
set +e
github_inventory="$(
gh release view "${GITHUB_REF_NAME}" \
--repo "${GITHUB_REPOSITORY}" \
--json targetCommitish,assets 2>&1
)"
if [[ $? -ne 0 ]]; then
github_inventory=absent-or-unavailable
fi
read_image_inventory() {
local image="$1"
local digest=
local revisions=
local status=0
if [[ ! -x .github/scripts/release-image-digest.sh ]] ||
[[ ! -x .github/scripts/release-verify-image-revision.sh ]]; then
printf 'unavailable'
return
fi
digest="$(.github/scripts/release-image-digest.sh "${image}" 2>/dev/null)"
if [[ $? -ne 0 ]]; then
printf 'unavailable'
return
fi
if [[ "${digest}" == "absent" ]]; then
printf 'absent'
return
fi
# 退出码 0/1 都带回可读的 revision 值,仅在无法读取清单时降级。
revisions="$(
.github/scripts/release-verify-image-revision.sh \
"${image}" "${GITHUB_SHA}" 2>/dev/null
)"
status=$?
if ((status > 1)) || [[ -z "${revisions}" ]]; then
revisions=revision-unavailable
fi
printf '%s/%s' "${digest}" "${revisions}"
}
ghcr_inventory="$(
read_image_inventory \
"ghcr.io/tbphp/gpt-load:${GITHUB_REF_NAME}"
)"
dockerhub_inventory="$(
read_image_inventory \
"tbphp/gpt-load:${GITHUB_REF_NAME}"
)"
ghcr_beta_inventory="$(
read_image_inventory ghcr.io/tbphp/gpt-load:v2beta
)"
dockerhub_beta_inventory="$(
read_image_inventory tbphp/gpt-load:v2beta
)"
{
printf '## Publication reconciliation\n\n'
printf -- '- preflight: `%s`\n' "${PREFLIGHT_RESULT}"
printf -- '- image writer: `%s`\n' "${IMAGE_WRITER_RESULT}"
printf -- '- GitHub writer: `%s`\n' "${GITHUB_WRITER_RESULT}"
printf -- '- published image smoke: `%s`\n' "${IMAGE_SMOKE_RESULT}"
printf -- '- draft and image verification: `%s`\n' "${VERIFY_POST_RESULT}"
printf -- '- GitHub exact inventory: `%s`\n' "${github_inventory}"
printf -- '- GHCR exact inventory: `%s`\n' "${ghcr_inventory}"
printf -- '- Docker Hub exact inventory: `%s`\n' \
"${dockerhub_inventory}"
printf -- '- GHCR v2beta inventory: `%s`\n' \
"${ghcr_beta_inventory}"
printf -- '- Docker Hub v2beta inventory: `%s`\n' \
"${dockerhub_beta_inventory}"
printf '\nmanual recovery: inspect the exact inventories and job logs; '
printf 'do not assume cross-system atomic rollback.\n'
} >>"${GITHUB_STEP_SUMMARY}"