1707 lines
65 KiB
YAML
1707 lines
65 KiB
YAML
name: Release
|
||
|
||
on:
|
||
push:
|
||
tags:
|
||
- "v2.*"
|
||
|
||
permissions:
|
||
contents: read
|
||
|
||
# 竖线分隔在任何错误(含网络错误)时回退到下一个源;默认的逗号只在
|
||
# 404/410 时回退,proxy.golang.org 瞬时故障会直接让整个 job 失败。
|
||
# CI 与 Release 的确定性验证保留自托管 Runner 的本机 Go 缓存;Release 的制品、
|
||
# 发布和外部服务节点使用 GitHub 托管的临时 Runner。两条流程都不下载/上传
|
||
# setup-go 缓存,也无需 main push 预热。
|
||
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_beta: ${{ steps.tag.outputs.image_beta }}
|
||
image_major: ${{ steps.tag.outputs.image_major }}
|
||
release_kind: ${{ steps.tag.outputs.release_kind }}
|
||
channel_beta: ${{ steps.tag.outputs.channel_beta }}
|
||
promote_major: ${{ steps.tag.outputs.promote_major }}
|
||
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
|
||
release_kind=stable
|
||
channel_beta=false
|
||
promote_major=true
|
||
if [[ "${version}" == *-* ]]; then
|
||
prerelease=true
|
||
prerelease_value="${version#*-}"
|
||
release_kind=other
|
||
promote_major=false
|
||
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
|
||
|
||
channel_beta_regex='^v2\.(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)-beta\.(0|[1-9][0-9]*)$'
|
||
if [[ "${tag}" =~ ${channel_beta_regex} ]]; then
|
||
channel_beta=true
|
||
release_kind=beta
|
||
elif [[ "${tag}" =~ ^v2\.(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)-rc\.(0|[1-9][0-9]*)$ ]]; then
|
||
release_kind=rc
|
||
fi
|
||
|
||
IFS='.' read -r major minor _ <<<"${core}"
|
||
image_beta=
|
||
if [[ "${channel_beta}" == "true" ]]; then
|
||
image_beta="${major}.${minor}-beta"
|
||
fi
|
||
if [[ "${core}" == "2.0.0" ]] &&
|
||
[[ "${release_kind}" == "beta" || "${release_kind}" == "rc" ]]; then
|
||
promote_major=true
|
||
fi
|
||
{
|
||
printf 'version=%s\n' "${tag}"
|
||
printf 'prerelease=%s\n' "${prerelease}"
|
||
printf 'image_exact=%s\n' "${version}"
|
||
printf 'image_beta=%s\n' "${image_beta}"
|
||
printf 'image_major=%s\n' "${major}"
|
||
printf 'release_kind=%s\n' "${release_kind}"
|
||
printf 'channel_beta=%s\n' "${channel_beta}"
|
||
printf 'promote_major=%s\n' "${promote_major}"
|
||
} >>"${GITHUB_OUTPUT}"
|
||
# release-tag-validation:end
|
||
|
||
- name: Require tag commit in main history
|
||
shell: bash
|
||
run: |
|
||
git fetch --no-tags origin main
|
||
git merge-base --is-ancestor "${GITHUB_SHA}" origin/main
|
||
|
||
- 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: [self-hosted, macOS, ARM64]
|
||
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: false
|
||
|
||
- 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: 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: [self-hosted, macOS, ARM64]
|
||
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: false
|
||
|
||
- name: Run race-enabled tests
|
||
run: go test -race -vet=off -count=1 -timeout=15m . ./internal/...
|
||
|
||
race-cpa:
|
||
needs: validate-tag
|
||
if: ${{ needs.validate-tag.outputs.ci_verified != 'true' }}
|
||
runs-on: [self-hosted, Linux, ARM64]
|
||
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: false
|
||
|
||
- 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: [self-hosted, Linux, ARM64]
|
||
timeout-minutes: 15
|
||
strategy:
|
||
fail-fast: false
|
||
max-parallel: 2
|
||
matrix:
|
||
include:
|
||
- driver: mysql
|
||
image: mysql:8.4
|
||
data-dir: /var/lib/mysql
|
||
port: "3306"
|
||
health: mysqladmin ping -h 127.0.0.1 -proot
|
||
dsn: mysql://root:root@127.0.0.1:{0}/gpt_load?charset=utf8mb4&transaction_isolation=%27READ-COMMITTED%27
|
||
- driver: postgres
|
||
image: postgres:18
|
||
data-dir: /var/lib/postgresql
|
||
port: "5432"
|
||
health: pg_isready -U postgres -d gpt_load
|
||
dsn: postgres://postgres:postgres@127.0.0.1:{0}/gpt_load?sslmode=disable
|
||
# 仅临时合同测试数据库使用内存盘;每个实例按实际使用量占用,最多 2 GiB。
|
||
services:
|
||
database:
|
||
image: ${{ matrix.image }}
|
||
env:
|
||
MYSQL_ROOT_PASSWORD: root
|
||
MYSQL_DATABASE: gpt_load
|
||
POSTGRES_PASSWORD: postgres
|
||
POSTGRES_DB: gpt_load
|
||
ports:
|
||
- ${{ matrix.port }}
|
||
options: >-
|
||
--tmpfs ${{ matrix.data-dir }}:rw,nosuid,nodev,size=2g
|
||
--health-cmd="${{ matrix.health }}"
|
||
--health-interval=5s
|
||
--health-timeout=10s
|
||
--health-retries=20
|
||
# MySQL 两组使用独立实例,避免不同测试数据库争用同名 GET_LOCK。
|
||
secondary-mysql:
|
||
image: ${{ matrix.driver == 'mysql' && 'mysql:8.4' || '' }}
|
||
env:
|
||
MYSQL_ROOT_PASSWORD: root
|
||
MYSQL_DATABASE: gpt_load
|
||
ports:
|
||
- 3306
|
||
options: >-
|
||
--tmpfs /var/lib/mysql:rw,nosuid,nodev,size=2g
|
||
--health-cmd="mysqladmin ping -h 127.0.0.1 -proot"
|
||
--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: false
|
||
|
||
- name: Run external database contract
|
||
env:
|
||
GPT_LOAD_DATABASE_TEST_DSN: ${{ format(matrix.dsn, job.services.database.ports[format('{0}', matrix.port)]) }}
|
||
GPT_LOAD_DATABASE_SECONDARY_DSN: ${{ matrix.driver == 'mysql' && format(matrix.dsn, job.services.secondary-mysql.ports['3306']) || '' }}
|
||
run: .github/scripts/ci-database-contract.sh "${{ matrix.driver }}"
|
||
|
||
build-binaries:
|
||
needs: verify-and-build-web
|
||
runs-on: ${{ matrix.runner }}
|
||
timeout-minutes: 15
|
||
strategy:
|
||
fail-fast: true
|
||
max-parallel: 5
|
||
matrix:
|
||
include:
|
||
- goos: linux
|
||
runner: ubuntu-24.04
|
||
goarch: amd64
|
||
filename: gpt-load-linux-amd64
|
||
- goos: linux
|
||
runner: ubuntu-24.04
|
||
goarch: arm64
|
||
filename: gpt-load-linux-arm64
|
||
- goos: darwin
|
||
runner: macos-15
|
||
goarch: amd64
|
||
filename: gpt-load-macos-amd64
|
||
- goos: darwin
|
||
runner: macos-15
|
||
goarch: arm64
|
||
filename: gpt-load-macos-arm64
|
||
- goos: windows
|
||
runner: windows-2025
|
||
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: false
|
||
|
||
- name: Build release binary
|
||
shell: bash
|
||
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
|
||
|
||
build-windows-setup:
|
||
needs: build-binaries
|
||
runs-on: windows-2025
|
||
timeout-minutes: 10
|
||
steps:
|
||
- name: Checkout tag commit
|
||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||
with:
|
||
persist-credentials: false
|
||
|
||
- name: Download verified Windows binary
|
||
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
|
||
with:
|
||
name: binary-gpt-load-windows-amd64.exe
|
||
path: release
|
||
|
||
- name: Build Windows setup
|
||
shell: pwsh
|
||
env:
|
||
RELEASE_VERSION: ${{ github.ref_name }}
|
||
run: |
|
||
$ErrorActionPreference = "Stop"
|
||
$compiler = "${env:ProgramFiles(x86)}\Inno Setup 6\ISCC.exe"
|
||
if (-not (Test-Path $compiler)) {
|
||
throw "Inno Setup compiler is unavailable: $compiler"
|
||
}
|
||
& $compiler `
|
||
"/DAppVersion=$env:RELEASE_VERSION" `
|
||
"packaging/windows/gpt-load.iss"
|
||
if ($LASTEXITCODE -ne 0) {
|
||
throw "Inno Setup failed with exit code $LASTEXITCODE"
|
||
}
|
||
if (-not (Test-Path "release/gpt-load-windows-setup.exe")) {
|
||
throw "Windows setup was not generated"
|
||
}
|
||
|
||
- name: Upload Windows setup
|
||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||
with:
|
||
name: binary-gpt-load-windows-setup.exe
|
||
path: release/gpt-load-windows-setup.exe
|
||
if-no-files-found: error
|
||
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: false
|
||
|
||
- 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/Inno-Setup.txt release/Inno-Setup.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
|
||
- build-windows-setup
|
||
- 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
|
||
|
||
windows-installer-smoke:
|
||
needs: package-checksums
|
||
runs-on: windows-2025
|
||
timeout-minutes: 10
|
||
steps:
|
||
- name: Checkout tag commit
|
||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||
with:
|
||
persist-credentials: false
|
||
|
||
- name: Download Windows setup
|
||
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
|
||
with:
|
||
name: binary-gpt-load-windows-setup.exe
|
||
path: installer-smoke
|
||
|
||
- name: Download portable Windows binary
|
||
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
|
||
with:
|
||
name: binary-gpt-load-windows-amd64.exe
|
||
path: installer-smoke
|
||
|
||
- name: Download release checksums
|
||
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
|
||
with:
|
||
name: release-checksums
|
||
path: installer-smoke
|
||
|
||
- name: Smoke Windows installer
|
||
shell: pwsh
|
||
env:
|
||
RELEASE_VERSION: ${{ github.ref_name }}
|
||
RELEASE_WINDOWS_SETUP: ${{ github.workspace }}\installer-smoke\gpt-load-windows-setup.exe
|
||
RELEASE_WINDOWS_BINARY: ${{ github.workspace }}\installer-smoke\gpt-load-windows-amd64.exe
|
||
RELEASE_SMOKE_CHECKSUM_FILE: ${{ github.workspace }}\installer-smoke\SHA256SUMS
|
||
run: |
|
||
& .github/scripts/release-windows-installer-smoke.ps1
|
||
exit $LASTEXITCODE
|
||
|
||
docker-smoke:
|
||
needs: validate-tag
|
||
runs-on: ubuntu-24.04
|
||
timeout-minutes: 30
|
||
steps:
|
||
- name: Isolate Docker credentials
|
||
shell: bash
|
||
run: |
|
||
# 使用临时登录配置,避免覆盖宿主机 Docker 凭据。
|
||
docker_config="$(mktemp -d "${RUNNER_TEMP}/docker-config.XXXXXX")"
|
||
echo "DOCKER_CONFIG=${docker_config}" >> "${GITHUB_ENV}"
|
||
|
||
- 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 }}-${{ github.run_attempt }}
|
||
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: Isolate Docker credentials
|
||
shell: bash
|
||
run: |
|
||
# 使用临时登录配置,避免覆盖宿主机 Docker 凭据。
|
||
docker_config="$(mktemp -d "${RUNNER_TEMP}/docker-config.XXXXXX")"
|
||
echo "DOCKER_CONFIG=${docker_config}" >> "${GITHUB_ENV}"
|
||
|
||
- 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 }}-${{ github.run_attempt }}
|
||
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
|
||
- build-windows-setup
|
||
- package-checksums
|
||
- native-artifact-smoke
|
||
- windows-installer-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: Isolate Docker credentials
|
||
shell: bash
|
||
run: |
|
||
# 使用临时登录配置,避免覆盖宿主机 Docker 凭据。
|
||
docker_config="$(mktemp -d "${RUNNER_TEMP}/docker-config.XXXXXX")"
|
||
echo "DOCKER_CONFIG=${docker_config}" >> "${GITHUB_ENV}"
|
||
|
||
- 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 main
|
||
git merge-base --is-ancestor "${GITHUB_SHA}" origin/main
|
||
|
||
- 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@dbcb813823bdd20940b903addbd779551569679f # v4.6.0
|
||
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@dbcb813823bdd20940b903addbd779551569679f # v4.6.0
|
||
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: Isolate Docker credentials
|
||
shell: bash
|
||
run: |
|
||
# 使用临时登录配置,避免覆盖宿主机 Docker 凭据。
|
||
docker_config="$(mktemp -d "${RUNNER_TEMP}/docker-config.XXXXXX")"
|
||
echo "DOCKER_CONFIG=${docker_config}" >> "${GITHUB_ENV}"
|
||
|
||
- 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@1f40c72289eff860ee54a304f1438e3cff362e0a # v4.3.0
|
||
with:
|
||
platforms: arm64
|
||
|
||
- name: Set up Docker Buildx
|
||
uses: docker/setup-buildx-action@37fe631027851001ddb9b187196cc803df7f5f0e # v4.3.0
|
||
|
||
- name: Log in to Docker Hub for exact verification
|
||
if: ${{ needs.publication-preflight.outputs.write_mode == 'verify' }}
|
||
uses: docker/login-action@dbcb813823bdd20940b903addbd779551569679f # v4.6.0
|
||
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@dbcb813823bdd20940b903addbd779551569679f # v4.6.0
|
||
with:
|
||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||
|
||
- name: Log in to GHCR
|
||
uses: docker/login-action@dbcb813823bdd20940b903addbd779551569679f # v4.6.0
|
||
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}"
|
||
|
||
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@efb35369e0ad2afab669f228072c1b0d510eae64 # v3.0.3
|
||
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
|
||
# 串行上传避免单个 runner 的并发连接中断拖垮整组资产;重试时保留已成功资产。
|
||
preserve_order: true
|
||
overwrite_files: false
|
||
# 目录内容已由上面的清单校验锁定为 .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: Isolate Docker credentials
|
||
shell: bash
|
||
run: |
|
||
# 使用临时登录配置,避免覆盖宿主机 Docker 凭据。
|
||
docker_config="$(mktemp -d "${RUNNER_TEMP}/docker-config.XXXXXX")"
|
||
echo "DOCKER_CONFIG=${docker_config}" >> "${GITHUB_ENV}"
|
||
|
||
- 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@dbcb813823bdd20940b903addbd779551569679f # v4.6.0
|
||
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 }}-${{ github.run_attempt }}-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: Isolate Docker credentials
|
||
shell: bash
|
||
run: |
|
||
# 使用临时登录配置,避免覆盖宿主机 Docker 凭据。
|
||
docker_config="$(mktemp -d "${RUNNER_TEMP}/docker-config.XXXXXX")"
|
||
echo "DOCKER_CONFIG=${docker_config}" >> "${GITHUB_ENV}"
|
||
|
||
- 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@dbcb813823bdd20940b903addbd779551569679f # v4.6.0
|
||
with:
|
||
registry: ghcr.io
|
||
username: ${{ github.actor }}
|
||
password: ${{ secrets.GITHUB_TOKEN }}
|
||
|
||
- name: Log in to Docker Hub for manifest verification
|
||
uses: docker/login-action@dbcb813823bdd20940b903addbd779551569679f # v4.6.0
|
||
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}"
|
||
|
||
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"
|
||
|
||
promote-image-channels:
|
||
needs:
|
||
- validate-tag
|
||
- publication-preflight
|
||
- post-publish-image-smoke
|
||
- post-publish-verify
|
||
if: >-
|
||
${{
|
||
!cancelled() &&
|
||
needs.validate-tag.result == 'success' &&
|
||
needs.publication-preflight.result == 'success' &&
|
||
needs.post-publish-image-smoke.result == 'success' &&
|
||
needs.post-publish-verify.result == 'success'
|
||
}}
|
||
concurrency:
|
||
group: gpt-load-v2-image-channels
|
||
cancel-in-progress: false
|
||
queue: max
|
||
runs-on: ubuntu-24.04
|
||
timeout-minutes: 20
|
||
permissions:
|
||
contents: read
|
||
packages: write
|
||
outputs:
|
||
beta_current: ${{ steps.promote.outputs.beta_current }}
|
||
major_current: ${{ steps.promote.outputs.major_current }}
|
||
steps:
|
||
- name: Isolate Docker credentials
|
||
shell: bash
|
||
run: |
|
||
# 使用临时登录配置,避免覆盖宿主机 Docker 凭据。
|
||
docker_config="$(mktemp -d "${RUNNER_TEMP}/docker-config.XXXXXX")"
|
||
echo "DOCKER_CONFIG=${docker_config}" >> "${GITHUB_ENV}"
|
||
|
||
- name: Checkout tag commit
|
||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||
with:
|
||
persist-credentials: false
|
||
|
||
- name: Set up Docker Buildx
|
||
uses: docker/setup-buildx-action@37fe631027851001ddb9b187196cc803df7f5f0e # v4.3.0
|
||
|
||
- name: Log in to GHCR for channel promotion
|
||
uses: docker/login-action@dbcb813823bdd20940b903addbd779551569679f # v4.6.0
|
||
with:
|
||
registry: ghcr.io
|
||
username: ${{ github.actor }}
|
||
password: ${{ secrets.GITHUB_TOKEN }}
|
||
|
||
- name: Log in to Docker Hub for channel promotion
|
||
uses: docker/login-action@dbcb813823bdd20940b903addbd779551569679f # v4.6.0
|
||
with:
|
||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||
|
||
- name: Promote verified image channels
|
||
id: promote
|
||
shell: bash
|
||
env:
|
||
RELEASE_VERSION: ${{ needs.validate-tag.outputs.version }}
|
||
IMAGE_EXACT: ${{ needs.validate-tag.outputs.image_exact }}
|
||
IMAGE_BETA: ${{ needs.validate-tag.outputs.image_beta }}
|
||
IMAGE_MAJOR: ${{ needs.validate-tag.outputs.image_major }}
|
||
PROMOTE_BETA: ${{ needs.validate-tag.outputs.channel_beta }}
|
||
PROMOTE_MAJOR: ${{ needs.validate-tag.outputs.promote_major }}
|
||
EXPECTED_REVISION: ${{ github.sha }}
|
||
EXPECTED_GHCR_LATEST: ${{ needs.publication-preflight.outputs.ghcr_latest_digest }}
|
||
EXPECTED_DOCKERHUB_LATEST: ${{ needs.publication-preflight.outputs.dockerhub_latest_digest }}
|
||
run: .github/scripts/release-promote-image-channels.sh
|
||
|
||
deploy-render:
|
||
name: Deploy Render
|
||
needs:
|
||
- validate-tag
|
||
- promote-image-channels
|
||
if: >-
|
||
${{
|
||
!cancelled() &&
|
||
needs.validate-tag.result == 'success' &&
|
||
needs.promote-image-channels.result == 'success' &&
|
||
needs.promote-image-channels.outputs.major_current == 'true'
|
||
}}
|
||
concurrency:
|
||
group: gpt-load-render
|
||
cancel-in-progress: false
|
||
environment:
|
||
name: render
|
||
url: ${{ vars.RENDER_SERVICE_URL }}
|
||
runs-on: ubuntu-24.04
|
||
timeout-minutes: 30
|
||
permissions:
|
||
contents: read
|
||
env:
|
||
RENDER_SERVICE_ID: ${{ vars.RENDER_SERVICE_ID }}
|
||
RENDER_SERVICE_URL: ${{ vars.RENDER_SERVICE_URL }}
|
||
RELEASE_VERSION: ${{ needs.validate-tag.outputs.version }}
|
||
IMAGE: ghcr.io/tbphp/gpt-load:${{ needs.validate-tag.outputs.image_exact }}
|
||
RENDER_CLI_VERSION: "2.25.0"
|
||
RENDER_CLI_SHA256: "3b3f1f839ef36b81f12d84ac7288f1c96f9f7519b39c53fe6f866612f704e7cd"
|
||
steps:
|
||
- name: Validate deployment inputs
|
||
shell: bash
|
||
run: |
|
||
set -euo pipefail
|
||
[[ "${RENDER_SERVICE_ID}" =~ ^srv-[a-z0-9]+$ ]]
|
||
[[ "${RENDER_SERVICE_URL}" =~ ^https://[^/?#]+/?$ ]]
|
||
[[ "${RELEASE_VERSION}" =~ ^v2\.(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)(-[0-9A-Za-z-]+(\.[0-9A-Za-z-]+)*)?$ ]]
|
||
[[ "${IMAGE}" == "ghcr.io/tbphp/gpt-load:${RELEASE_VERSION#v}" ]]
|
||
|
||
- name: Install pinned Render CLI
|
||
shell: bash
|
||
run: |
|
||
set -euo pipefail
|
||
archive="cli_${RENDER_CLI_VERSION}_linux_amd64.zip"
|
||
archive_path="${RUNNER_TEMP}/${archive}"
|
||
extract_dir="${RUNNER_TEMP}/render-cli-${RENDER_CLI_VERSION}"
|
||
url="https://github.com/render-oss/cli/releases/download/v${RENDER_CLI_VERSION}/${archive}"
|
||
|
||
curl \
|
||
--fail \
|
||
--silent \
|
||
--show-error \
|
||
--location \
|
||
--retry 3 \
|
||
--retry-all-errors \
|
||
"${url}" \
|
||
--output "${archive_path}"
|
||
printf '%s %s\n' \
|
||
"${RENDER_CLI_SHA256}" \
|
||
"${archive_path}" | sha256sum --check
|
||
mkdir -p "${extract_dir}"
|
||
unzip -q "${archive_path}" -d "${extract_dir}"
|
||
install \
|
||
-m 0755 \
|
||
"${extract_dir}/cli_v${RENDER_CLI_VERSION}" \
|
||
"${RUNNER_TEMP}/render"
|
||
printf '%s\n' "${RUNNER_TEMP}" >>"${GITHUB_PATH}"
|
||
"${RUNNER_TEMP}/render" --version
|
||
|
||
- name: Deploy exact image and wait until live
|
||
shell: bash
|
||
env:
|
||
RENDER_API_KEY: ${{ secrets.RENDER_API_KEY }}
|
||
CI: "true"
|
||
run: |
|
||
set -euo pipefail
|
||
test -n "${RENDER_API_KEY}"
|
||
render deploys create \
|
||
"${RENDER_SERVICE_ID}" \
|
||
--image "${IMAGE}" \
|
||
--wait \
|
||
--confirm \
|
||
--output text
|
||
|
||
- name: Verify public health endpoint
|
||
shell: bash
|
||
run: |
|
||
set -euo pipefail
|
||
# render-health-verification:start
|
||
health_url="${RENDER_SERVICE_URL%/}/health"
|
||
deadline=$((SECONDS + 180))
|
||
while true; do
|
||
health_response=
|
||
if health_response="$(
|
||
curl \
|
||
--fail \
|
||
--silent \
|
||
--show-error \
|
||
--connect-timeout 5 \
|
||
--max-time 15 \
|
||
"${health_url}"
|
||
)" &&
|
||
jq -e \
|
||
--arg version "${RELEASE_VERSION}" \
|
||
'.status == "ok" and .version == $version' \
|
||
<<<"${health_response}" \
|
||
>/dev/null; then
|
||
break
|
||
fi
|
||
if ((SECONDS >= deadline)); then
|
||
printf 'Render health did not report version %s within 180 seconds\n' \
|
||
"${RELEASE_VERSION}" >&2
|
||
exit 1
|
||
fi
|
||
sleep 5
|
||
done
|
||
# render-health-verification:end
|
||
|
||
- name: Summarize deployment
|
||
shell: bash
|
||
run: |
|
||
{
|
||
printf '## Render deployment\n\n'
|
||
printf -- '- Image: `%s`\n' "${IMAGE}"
|
||
printf -- '- Service: `%s`\n' "${RENDER_SERVICE_ID}"
|
||
printf -- '- URL: %s\n' "${RENDER_SERVICE_URL}"
|
||
} >>"${GITHUB_STEP_SUMMARY}"
|
||
|
||
reconcile-publication:
|
||
if: ${{ always() }}
|
||
needs:
|
||
- validate-tag
|
||
- publication-preflight
|
||
- publish-images
|
||
- publish-github
|
||
- post-publish-image-smoke
|
||
- post-publish-verify
|
||
- promote-image-channels
|
||
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: Isolate Docker credentials
|
||
shell: bash
|
||
run: |
|
||
# 使用临时登录配置,避免覆盖宿主机 Docker 凭据。
|
||
docker_config="$(mktemp -d "${RUNNER_TEMP}/docker-config.XXXXXX")"
|
||
echo "DOCKER_CONFIG=${docker_config}" >> "${GITHUB_ENV}"
|
||
|
||
- 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@dbcb813823bdd20940b903addbd779551569679f # v4.6.0
|
||
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@dbcb813823bdd20940b903addbd779551569679f # v4.6.0
|
||
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 }}
|
||
PROMOTION_RESULT: ${{ needs.promote-image-channels.result }}
|
||
IMAGE_EXACT: ${{ needs.validate-tag.outputs.image_exact }}
|
||
IMAGE_BETA: ${{ needs.validate-tag.outputs.image_beta }}
|
||
IMAGE_MAJOR: ${{ needs.validate-tag.outputs.image_major }}
|
||
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:${IMAGE_EXACT}"
|
||
)"
|
||
dockerhub_inventory="$(
|
||
read_image_inventory \
|
||
"tbphp/gpt-load:${IMAGE_EXACT}"
|
||
)"
|
||
ghcr_beta_inventory=not-applicable
|
||
dockerhub_beta_inventory=not-applicable
|
||
if [[ -n "${IMAGE_BETA}" ]]; then
|
||
ghcr_beta_inventory="$(
|
||
read_image_inventory "ghcr.io/tbphp/gpt-load:${IMAGE_BETA}"
|
||
)"
|
||
dockerhub_beta_inventory="$(
|
||
read_image_inventory "tbphp/gpt-load:${IMAGE_BETA}"
|
||
)"
|
||
fi
|
||
ghcr_major_inventory="$(
|
||
read_image_inventory "ghcr.io/tbphp/gpt-load:${IMAGE_MAJOR}"
|
||
)"
|
||
dockerhub_major_inventory="$(
|
||
read_image_inventory "tbphp/gpt-load:${IMAGE_MAJOR}"
|
||
)"
|
||
|
||
{
|
||
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 -- '- image channel promotion: `%s`\n' "${PROMOTION_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 %s inventory: `%s`\n' "${IMAGE_BETA:-beta-not-applicable}" \
|
||
"${ghcr_beta_inventory}"
|
||
printf -- '- Docker Hub %s inventory: `%s`\n' "${IMAGE_BETA:-beta-not-applicable}" \
|
||
"${dockerhub_beta_inventory}"
|
||
printf -- '- GHCR %s inventory: `%s`\n' "${IMAGE_MAJOR}" \
|
||
"${ghcr_major_inventory}"
|
||
printf -- '- Docker Hub %s inventory: `%s`\n' "${IMAGE_MAJOR}" \
|
||
"${dockerhub_major_inventory}"
|
||
printf '\nmanual recovery: inspect the exact inventories and job logs; '
|
||
printf 'do not assume cross-system atomic rollback.\n'
|
||
} >>"${GITHUB_STEP_SUMMARY}"
|