* feat(gateway): 升级 SDK 并封口兼容合同 * fix: 修正升级后的格式与模型过滤 * fix(ci): 更新 Go 漏洞扫描器 * fix(gateway): 拒绝非标准服务层级字段
1367 lines
52 KiB
YAML
1367 lines
52 KiB
YAML
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}"
|