ci(release): 复用 CI 结论并消除发布流程重复验证
This commit is contained in:
8 files changed
+627
-355
No files matched your search
@@ -23,7 +23,9 @@ runs:
|
||||
|
||||
- name: Audit web dependencies
|
||||
shell: bash
|
||||
run: pnpm --dir web audit --audit-level high
|
||||
# 只审计生产依赖:devDependencies 经 Vite 构建后不进入 internal/webui/dist,
|
||||
# 让它们的 CVE 阻断发布只会制造噪音。
|
||||
run: pnpm --dir web audit --prod --audit-level high
|
||||
|
||||
- name: Lint web source
|
||||
shell: bash
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
Apache-2.0.txt
|
||||
LICENSE
|
||||
MIT.txt
|
||||
MPL-2.0.txt
|
||||
SHA256SUMS
|
||||
THIRD_PARTY_NOTICES.md
|
||||
bom.cdx.json
|
||||
gpt-load-linux-amd64
|
||||
gpt-load-linux-arm64
|
||||
gpt-load-macos-amd64
|
||||
gpt-load-macos-arm64
|
||||
gpt-load-windows-amd64.exe
|
||||
@@ -6,6 +6,14 @@ release_version="${RELEASE_VERSION:-v2.0.0-local}"
|
||||
suffix="${RELEASE_SMOKE_SUFFIX:-local-$$}"
|
||||
source_image="${RELEASE_SMOKE_SOURCE_IMAGE:-}"
|
||||
trivy_image="${RELEASE_SMOKE_TRIVY_IMAGE:-aquasec/trivy:0.72.0@sha256:cffe3f5161a47a6823fbd23d985795b3ed72a4c806da4c4df16266c02accdd6f}"
|
||||
skip_scan="${RELEASE_SMOKE_SKIP_SCAN:-false}"
|
||||
case "${skip_scan}" in
|
||||
true | false) ;;
|
||||
*)
|
||||
printf 'RELEASE_SMOKE_SKIP_SCAN must be true or false\n' >&2
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
case "${suffix}" in
|
||||
*[!A-Za-z0-9_-]*)
|
||||
printf 'RELEASE_SMOKE_SUFFIX contains unsupported characters\n' >&2
|
||||
@@ -122,15 +130,22 @@ else
|
||||
-t "${image}" .
|
||||
fi
|
||||
smoke_stage="scan-image"
|
||||
docker run --rm \
|
||||
--volume /var/run/docker.sock:/var/run/docker.sock \
|
||||
"${trivy_image}" image \
|
||||
--scanners vuln \
|
||||
--severity CRITICAL,HIGH \
|
||||
--ignore-unfixed \
|
||||
--exit-code 1 \
|
||||
--no-progress \
|
||||
"${image}"
|
||||
# 发布后阶段拉取的是发布前已扫描过的同一 commit 镜像,重复扫描没有新信息,
|
||||
# 由调用方通过 RELEASE_SMOKE_SKIP_SCAN=true 显式跳过。
|
||||
scanned=true
|
||||
if [[ "${skip_scan}" == "true" ]]; then
|
||||
scanned=false
|
||||
else
|
||||
docker run --rm \
|
||||
--volume /var/run/docker.sock:/var/run/docker.sock \
|
||||
"${trivy_image}" image \
|
||||
--scanners vuln \
|
||||
--severity CRITICAL,HIGH \
|
||||
--ignore-unfixed \
|
||||
--exit-code 1 \
|
||||
--no-progress \
|
||||
"${image}"
|
||||
fi
|
||||
test "$(docker image inspect -f '{{.Config.User}}' "${image}")" = "10001:10001"
|
||||
docker image inspect -f '{{range .Config.Env}}{{println .}}{{end}}' "${image}" |
|
||||
grep -Fx 'HOST=0.0.0.0' >/dev/null
|
||||
@@ -456,6 +471,7 @@ smoke_stage="verify-secret-free-artifacts"
|
||||
summary_file="${task_tmp}/summary.txt"
|
||||
{
|
||||
printf 'platform=%s\n' "${platform}"
|
||||
printf 'vulnerability_scanned=%s\n' "${scanned}"
|
||||
printf 'configured_user=10001:10001\n'
|
||||
printf 'image_and_container_host=0.0.0.0\n'
|
||||
printf 'direct_docker_run_publish_reachable=true\n'
|
||||
|
||||
Executable
+57
@@ -0,0 +1,57 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
# 校验一个发布资产目录是否与 .github/release-assets.txt 完全一致:
|
||||
# 1. 目录内的普通文件清单必须逐字等于清单文件;
|
||||
# 2. SHA256SUMS 必须恰好覆盖清单中除自身以外的全部资产;
|
||||
# 3. 每一项校验和都必须通过。
|
||||
# 清单文件是发布资产的唯一事实源,新增或改名资产只需要改动它。
|
||||
#
|
||||
# 用法:
|
||||
# release-verify-assets.sh <directory>
|
||||
# release-verify-assets.sh <directory> <trusted-checksum-file>
|
||||
#
|
||||
# 校验从远端下载的资产时必须传入第二个参数。此时目录内那份 SHA256SUMS 是不可信
|
||||
# 输入,只用于身份比对,绝不执行;实际校验一律使用当前 run 生成的可信副本。
|
||||
|
||||
directory="${1:?release asset directory is required}"
|
||||
trusted_checksums="${2:-}"
|
||||
manifest="${RELEASE_ASSET_MANIFEST:-.github/release-assets.txt}"
|
||||
|
||||
absolute_path() {
|
||||
printf '%s/%s' "$(cd "$(dirname "$1")" && pwd)" "$(basename "$1")"
|
||||
}
|
||||
|
||||
test -d "${directory}"
|
||||
test -s "${manifest}"
|
||||
manifest="$(absolute_path "${manifest}")"
|
||||
if [[ -n "${trusted_checksums}" ]]; then
|
||||
test -s "${trusted_checksums}"
|
||||
trusted_checksums="$(absolute_path "${trusted_checksums}")"
|
||||
fi
|
||||
|
||||
work="$(mktemp -d)"
|
||||
trap 'rm -rf "${work}"' EXIT
|
||||
|
||||
# 清单本身必须保持 C 序排序,否则后续所有 cmp 都会随 locale 漂移。
|
||||
LC_ALL=C sort "${manifest}" >"${work}/expected"
|
||||
cmp "${manifest}" "${work}/expected"
|
||||
|
||||
find "${directory}" -mindepth 1 -maxdepth 1 -type f -exec basename {} \; |
|
||||
LC_ALL=C sort >"${work}/actual"
|
||||
cmp "${work}/expected" "${work}/actual"
|
||||
|
||||
grep -vFx 'SHA256SUMS' "${work}/expected" >"${work}/expected-checksummed"
|
||||
|
||||
cd "${directory}"
|
||||
awk '{print $2}' SHA256SUMS | LC_ALL=C sort >"${work}/actual-checksummed"
|
||||
cmp "${work}/expected-checksummed" "${work}/actual-checksummed"
|
||||
|
||||
# release-assets-trusted-checksum:start
|
||||
if [[ -n "${trusted_checksums}" ]]; then
|
||||
cmp "${trusted_checksums}" SHA256SUMS
|
||||
sha256sum --check "${trusted_checksums}"
|
||||
else
|
||||
sha256sum --check SHA256SUMS
|
||||
fi
|
||||
# release-assets-trusted-checksum:end
|
||||
+56
@@ -0,0 +1,56 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
# 断言多架构镜像的 linux/amd64 与 linux/arm64 两个 config 标签
|
||||
# org.opencontainers.image.revision 都等于期望的 commit SHA。
|
||||
#
|
||||
# registry 返回的标签值是不可信输入,只允许在 jq 比较内部使用:进入 shell 的
|
||||
# 永远只有受信的 ${expected} 或字面量 mismatch,实际读到的值仅直接写入 stderr。
|
||||
#
|
||||
# 退出码约定,调用方据此区分「确实不匹配」与「无法完成检查」:
|
||||
# 0 = 两个架构都匹配,stdout 输出该 revision
|
||||
# 1 = 镜像可读但 revision 不匹配,stderr 输出实际读到的标签
|
||||
# 2 = 无法读取镜像清单(网络、认证或镜像缺失),stderr 输出原始错误
|
||||
|
||||
image="${1:?image is required}"
|
||||
expected="${2:?expected revision is required}"
|
||||
|
||||
if ! inspection="$(
|
||||
docker buildx imagetools inspect "${image}" --format '{{json .}}' 2>&1
|
||||
)"; then
|
||||
printf '%s\n' "${inspection}" >&2
|
||||
exit 2
|
||||
fi
|
||||
|
||||
# release-image-revision-validation:start
|
||||
revision=mismatch
|
||||
if jq -e \
|
||||
--arg expected "${expected}" \
|
||||
'[
|
||||
.image["linux/amd64"].config.Labels[
|
||||
"org.opencontainers.image.revision"
|
||||
],
|
||||
.image["linux/arm64"].config.Labels[
|
||||
"org.opencontainers.image.revision"
|
||||
]
|
||||
] | all(type == "string" and . == $expected)' \
|
||||
<<<"${inspection}" >/dev/null 2>&1; then
|
||||
revision="${expected}"
|
||||
fi
|
||||
# release-image-revision-validation:end
|
||||
|
||||
if [[ "${revision}" == "mismatch" ]]; then
|
||||
printf 'image revision mismatch for %s (expected %s), actual labels: ' \
|
||||
"${image}" "${expected}" >&2
|
||||
jq -c '[
|
||||
.image["linux/amd64"].config.Labels[
|
||||
"org.opencontainers.image.revision"
|
||||
],
|
||||
.image["linux/arm64"].config.Labels[
|
||||
"org.opencontainers.image.revision"
|
||||
]
|
||||
]' <<<"${inspection}" >&2 2>/dev/null || printf 'unreadable\n' >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
printf '%s' "${revision}"
|
||||
+162
-272
@@ -16,6 +16,10 @@ 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 }}
|
||||
@@ -23,6 +27,7 @@ jobs:
|
||||
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
|
||||
@@ -76,10 +81,39 @@ jobs:
|
||||
# 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
|
||||
@@ -99,7 +133,8 @@ jobs:
|
||||
name: verified-web-dist
|
||||
path: internal/webui/dist
|
||||
if-no-files-found: error
|
||||
retention-days: 14
|
||||
# 仅在本次 run 内传递给 build-binaries,不作为发布产物保留。
|
||||
retention-days: 1
|
||||
|
||||
static-checks:
|
||||
needs: validate-tag
|
||||
@@ -147,6 +182,8 @@ jobs:
|
||||
|
||||
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:
|
||||
@@ -166,6 +203,7 @@ jobs:
|
||||
|
||||
race-cpa:
|
||||
needs: validate-tag
|
||||
if: ${{ needs.validate-tag.outputs.ci_verified != 'true' }}
|
||||
runs-on: ubuntu-24.04
|
||||
timeout-minutes: 10
|
||||
steps:
|
||||
@@ -188,6 +226,7 @@ jobs:
|
||||
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:
|
||||
@@ -301,7 +340,8 @@ jobs:
|
||||
name: binary-${{ matrix.filename }}
|
||||
path: release/${{ matrix.filename }}
|
||||
if-no-files-found: error
|
||||
retention-days: 14
|
||||
# 仅在本次 run 内汇总进 release-assets,不单独保留。
|
||||
retention-days: 1
|
||||
|
||||
package-metadata:
|
||||
needs: validate-tag
|
||||
@@ -336,7 +376,8 @@ jobs:
|
||||
name: release-metadata
|
||||
path: release
|
||||
if-no-files-found: error
|
||||
retention-days: 14
|
||||
# 仅在本次 run 内汇总进 release-assets,不单独保留。
|
||||
retention-days: 1
|
||||
|
||||
package-checksums:
|
||||
needs:
|
||||
@@ -345,6 +386,11 @@ jobs:
|
||||
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:
|
||||
@@ -359,21 +405,16 @@ jobs:
|
||||
path: release
|
||||
|
||||
- name: Generate SHA256SUMS
|
||||
working-directory: release
|
||||
shell: bash
|
||||
run: |
|
||||
sha256sum \
|
||||
Apache-2.0.txt \
|
||||
LICENSE \
|
||||
MIT.txt \
|
||||
MPL-2.0.txt \
|
||||
THIRD_PARTY_NOTICES.md \
|
||||
bom.cdx.json \
|
||||
gpt-load-linux-amd64 \
|
||||
gpt-load-linux-arm64 \
|
||||
gpt-load-macos-amd64 \
|
||||
gpt-load-macos-arm64 \
|
||||
gpt-load-windows-amd64.exe >SHA256SUMS
|
||||
sha256sum --check SHA256SUMS
|
||||
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
|
||||
@@ -384,9 +425,8 @@ jobs:
|
||||
retention-days: 14
|
||||
|
||||
native-artifact-smoke:
|
||||
needs:
|
||||
- build-binaries
|
||||
- package-checksums
|
||||
# package-checksums 已经依赖 build-binaries,无需重复声明。
|
||||
needs: package-checksums
|
||||
timeout-minutes: 10
|
||||
strategy:
|
||||
fail-fast: true
|
||||
@@ -455,6 +495,9 @@ jobs:
|
||||
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
|
||||
@@ -466,6 +509,9 @@ jobs:
|
||||
- native-artifact-smoke
|
||||
- docker-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:
|
||||
@@ -508,32 +554,16 @@ jobs:
|
||||
git fetch --no-tags origin v2
|
||||
git merge-base --is-ancestor "${GITHUB_SHA}" origin/v2
|
||||
|
||||
cat >/tmp/expected-release-assets <<'ASSETS'
|
||||
Apache-2.0.txt
|
||||
LICENSE
|
||||
MIT.txt
|
||||
MPL-2.0.txt
|
||||
SHA256SUMS
|
||||
THIRD_PARTY_NOTICES.md
|
||||
bom.cdx.json
|
||||
gpt-load-linux-amd64
|
||||
gpt-load-linux-arm64
|
||||
gpt-load-macos-amd64
|
||||
gpt-load-macos-arm64
|
||||
gpt-load-windows-amd64.exe
|
||||
ASSETS
|
||||
find release -mindepth 1 -maxdepth 1 -type f -exec basename {} \; |
|
||||
sort >/tmp/current-run-release-assets
|
||||
cmp /tmp/expected-release-assets /tmp/current-run-release-assets
|
||||
cd release
|
||||
test "$(wc -l <SHA256SUMS | tr -d ' ')" = "11"
|
||||
sha256sum --check SHA256SUMS
|
||||
- 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: |
|
||||
@@ -604,29 +634,20 @@ jobs:
|
||||
test "${github_prerelease}" = \
|
||||
"${{ needs.validate-tag.outputs.prerelease }}"
|
||||
jq -r '.assets[].name' /tmp/existing-release.json |
|
||||
sort >/tmp/existing-release-assets
|
||||
if cmp \
|
||||
/tmp/expected-release-assets \
|
||||
/tmp/existing-release-assets; then
|
||||
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
|
||||
if cmp release/SHA256SUMS existing-release/SHA256SUMS; then
|
||||
if (
|
||||
cd existing-release
|
||||
sha256sum --check ../release/SHA256SUMS
|
||||
); then
|
||||
github_assets=match
|
||||
else
|
||||
github_assets=mismatch
|
||||
fi
|
||||
else
|
||||
github_assets=mismatch
|
||||
# 已发布资产必须与本次 run 的 SHA256SUMS 逐字节一致;
|
||||
# 传入可信副本后,远端那份 SHA256SUMS 只参与比对,不会被执行。
|
||||
if .github/scripts/release-verify-assets.sh \
|
||||
existing-release release/SHA256SUMS; then
|
||||
github_assets=match
|
||||
fi
|
||||
else
|
||||
github_assets=mismatch
|
||||
fi
|
||||
elif grep -Eqi \
|
||||
'HTTP 404|Not Found|release not found' \
|
||||
@@ -640,8 +661,8 @@ jobs:
|
||||
inventory_image() {
|
||||
local image="$1"
|
||||
local digest=
|
||||
local inspection=
|
||||
local revision=mismatch
|
||||
local revision=
|
||||
local status=0
|
||||
|
||||
digest="$(.github/scripts/release-image-digest.sh "${image}")"
|
||||
if [[ "${digest}" == "absent" ]]; then
|
||||
@@ -649,40 +670,33 @@ jobs:
|
||||
return
|
||||
fi
|
||||
|
||||
inspection="$(
|
||||
docker buildx imagetools inspect \
|
||||
"${image}" \
|
||||
--format '{{json .}}'
|
||||
)"
|
||||
# release-image-revision-validation:start
|
||||
if jq -e \
|
||||
--arg expected "${GITHUB_SHA}" \
|
||||
'[
|
||||
.image["linux/amd64"].config.Labels[
|
||||
"org.opencontainers.image.revision"
|
||||
],
|
||||
.image["linux/arm64"].config.Labels[
|
||||
"org.opencontainers.image.revision"
|
||||
]
|
||||
] | all(type == "string" and . == $expected)' \
|
||||
<<<"${inspection}" >/dev/null; then
|
||||
revision="${GITHUB_SHA}"
|
||||
# 退出码 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
|
||||
# release-image-revision-validation:end
|
||||
printf 'present|%s|%s\n' "${digest}" "${revision}"
|
||||
}
|
||||
|
||||
IFS='|' read -r ghcr_state ghcr_digest ghcr_revision < <(
|
||||
# 用命令替换而非进程替换:函数内的失败必须终止本 step,
|
||||
# 而不是让 read 静默读到空状态。
|
||||
ghcr_inventory="$(
|
||||
inventory_image \
|
||||
"ghcr.io/tbphp/gpt-load:${{ needs.validate-tag.outputs.image_exact }}"
|
||||
)
|
||||
IFS='|' read -r \
|
||||
dockerhub_state \
|
||||
dockerhub_digest \
|
||||
dockerhub_revision < <(
|
||||
inventory_image \
|
||||
"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
|
||||
)"
|
||||
@@ -820,6 +834,10 @@ jobs:
|
||||
labels: ${{ steps.meta.outputs.labels }}
|
||||
build-args: |
|
||||
VERSION=${{ github.ref_name }}
|
||||
# 复用上一次发布的依赖层(go mod download / pnpm install);
|
||||
# Go 与 Web 源码层每个 tag 必然失效,这里只回收不变的依赖安装成本。
|
||||
cache-from: type=gha,scope=release-image
|
||||
cache-to: type=gha,scope=release-image,mode=max
|
||||
|
||||
- name: Verify exact published images
|
||||
shell: bash
|
||||
@@ -827,24 +845,9 @@ jobs:
|
||||
set -euo pipefail
|
||||
verify_exact_image() {
|
||||
local image="$1"
|
||||
local inspection=
|
||||
|
||||
inspection="$(
|
||||
docker buildx imagetools inspect \
|
||||
"${image}" \
|
||||
--format '{{json .}}'
|
||||
)"
|
||||
jq -e \
|
||||
--arg expected "${GITHUB_SHA}" \
|
||||
'[
|
||||
.image["linux/amd64"].config.Labels[
|
||||
"org.opencontainers.image.revision"
|
||||
],
|
||||
.image["linux/arm64"].config.Labels[
|
||||
"org.opencontainers.image.revision"
|
||||
]
|
||||
] | all(type == "string" and . == $expected)' \
|
||||
<<<"${inspection}" >/dev/null
|
||||
.github/scripts/release-verify-image-revision.sh \
|
||||
"${image}" "${GITHUB_SHA}" >/dev/null
|
||||
.github/scripts/release-image-digest.sh "${image}"
|
||||
}
|
||||
|
||||
@@ -896,15 +899,20 @@ jobs:
|
||||
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
|
||||
|
||||
- name: Verify release checksums
|
||||
working-directory: release
|
||||
run: sha256sum --check SHA256SUMS
|
||||
# 上传步骤按 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' }}
|
||||
@@ -941,11 +949,8 @@ jobs:
|
||||
gh release download "${GITHUB_REF_NAME}" \
|
||||
--repo "${GITHUB_REPOSITORY}" \
|
||||
--dir existing-release
|
||||
cmp release/SHA256SUMS existing-release/SHA256SUMS
|
||||
(
|
||||
cd existing-release
|
||||
sha256sum --check ../release/SHA256SUMS
|
||||
)
|
||||
.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' }}
|
||||
@@ -958,19 +963,8 @@ jobs:
|
||||
make_latest: false
|
||||
generate_release_notes: true
|
||||
fail_on_unmatched_files: true
|
||||
files: |
|
||||
release/gpt-load-linux-amd64
|
||||
release/gpt-load-linux-arm64
|
||||
release/gpt-load-macos-amd64
|
||||
release/gpt-load-macos-arm64
|
||||
release/gpt-load-windows-amd64.exe
|
||||
release/Apache-2.0.txt
|
||||
release/LICENSE
|
||||
release/MIT.txt
|
||||
release/MPL-2.0.txt
|
||||
release/THIRD_PARTY_NOTICES.md
|
||||
release/bom.cdx.json
|
||||
release/SHA256SUMS
|
||||
# 目录内容已由上面的清单校验锁定为 .github/release-assets.txt 的全集。
|
||||
files: release/*
|
||||
body: |
|
||||
GPT-Load 2.x is a greenfield rewrite and is not data-compatible with 1.x.
|
||||
Do not open a 1.x database with this release; rebuild configuration and cut over manually.
|
||||
@@ -1018,66 +1012,10 @@ jobs:
|
||||
channel and credentials, AccessKeys authorize data-plane access, and model discovery
|
||||
imports available models from the selected channel.
|
||||
|
||||
post-publish-native-smoke:
|
||||
needs:
|
||||
- publish-github
|
||||
- publish-images
|
||||
timeout-minutes: 15
|
||||
permissions:
|
||||
# Draft Releases are visible only to identities with push access.
|
||||
contents: write
|
||||
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 draft GitHub Release assets
|
||||
shell: bash
|
||||
env:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
mkdir -p draft-release
|
||||
gh release download "${GITHUB_REF_NAME}" \
|
||||
--repo "${GITHUB_REPOSITORY}" \
|
||||
--dir draft-release
|
||||
|
||||
- name: Smoke drafted POSIX native artifact
|
||||
if: runner.os != 'Windows'
|
||||
shell: bash
|
||||
env:
|
||||
RELEASE_VERSION: ${{ github.ref_name }}
|
||||
RELEASE_SMOKE_BINARY: ${{ github.workspace }}/draft-release/${{ matrix.filename }}
|
||||
RELEASE_SMOKE_CHECKSUM_FILE: ${{ github.workspace }}/draft-release/SHA256SUMS
|
||||
run: .github/scripts/release-native-smoke.sh
|
||||
|
||||
- name: Smoke drafted Windows native artifact
|
||||
if: runner.os == 'Windows'
|
||||
shell: pwsh
|
||||
env:
|
||||
RELEASE_VERSION: ${{ github.ref_name }}
|
||||
RELEASE_SMOKE_BINARY: ${{ github.workspace }}/draft-release/${{ matrix.filename }}
|
||||
RELEASE_SMOKE_CHECKSUM_FILE: ${{ github.workspace }}/draft-release/SHA256SUMS
|
||||
run: .github/scripts/release-native-smoke.ps1
|
||||
|
||||
post-publish-image-smoke:
|
||||
# GHCR 与 Docker Hub 的 exact tag 已在 publish-images 断言为同一 digest,
|
||||
# 因此完整运行时 smoke 只需跑一遍;Docker Hub 侧的可拉取性与 digest 一致性
|
||||
# 由 post-publish-verify 覆盖。
|
||||
needs:
|
||||
- validate-tag
|
||||
- publish-images
|
||||
@@ -1086,17 +1024,6 @@ jobs:
|
||||
permissions:
|
||||
contents: read
|
||||
packages: read
|
||||
strategy:
|
||||
fail-fast: false
|
||||
max-parallel: 2
|
||||
matrix:
|
||||
include:
|
||||
- registry: ghcr
|
||||
repository: ghcr.io/tbphp/gpt-load
|
||||
suffix: ghcr
|
||||
- registry: dockerhub
|
||||
repository: tbphp/gpt-load
|
||||
suffix: dockerhub
|
||||
steps:
|
||||
- name: Checkout tag commit
|
||||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
@@ -1104,26 +1031,20 @@ jobs:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Log in to GHCR for runtime smoke
|
||||
if: ${{ matrix.registry == 'ghcr' }}
|
||||
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 runtime smoke
|
||||
if: ${{ matrix.registry == 'dockerhub' }}
|
||||
uses: docker/login-action@371161bbe7024a29a25c5e19bfcbc0804fe9ad2c # v4.5.2
|
||||
with:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_READ_TOKEN || secrets.DOCKERHUB_TOKEN }}
|
||||
|
||||
- name: Run full runtime smoke for published exact image
|
||||
shell: bash
|
||||
env:
|
||||
RELEASE_VERSION: ${{ github.ref_name }}
|
||||
RELEASE_SMOKE_SOURCE_IMAGE: ${{ matrix.repository }}:${{ needs.validate-tag.outputs.image_exact }}
|
||||
RELEASE_SMOKE_SUFFIX: ${{ github.run_id }}-${{ matrix.suffix }}
|
||||
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:
|
||||
@@ -1144,6 +1065,12 @@ jobs:
|
||||
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 }}
|
||||
@@ -1154,34 +1081,10 @@ jobs:
|
||||
gh release download "${GITHUB_REF_NAME}" \
|
||||
--repo "${GITHUB_REPOSITORY}" \
|
||||
--dir draft-release
|
||||
|
||||
cat >/tmp/expected-release-assets <<'ASSETS'
|
||||
Apache-2.0.txt
|
||||
LICENSE
|
||||
MIT.txt
|
||||
MPL-2.0.txt
|
||||
SHA256SUMS
|
||||
THIRD_PARTY_NOTICES.md
|
||||
bom.cdx.json
|
||||
gpt-load-linux-amd64
|
||||
gpt-load-linux-arm64
|
||||
gpt-load-macos-amd64
|
||||
gpt-load-macos-arm64
|
||||
gpt-load-windows-amd64.exe
|
||||
ASSETS
|
||||
find draft-release -mindepth 1 -maxdepth 1 -type f -exec basename {} \; |
|
||||
sort >/tmp/actual-release-assets
|
||||
asset_count="$(wc -l </tmp/actual-release-assets | tr -d ' ')"
|
||||
test "${asset_count}" = "12"
|
||||
cmp /tmp/expected-release-assets /tmp/actual-release-assets
|
||||
|
||||
cd draft-release
|
||||
test "$(wc -l <SHA256SUMS | tr -d ' ')" = "11"
|
||||
awk '{print $2}' SHA256SUMS | sort >/tmp/checksummed-release-assets
|
||||
grep -v '^SHA256SUMS$' /tmp/expected-release-assets \
|
||||
>/tmp/expected-checksummed-release-assets
|
||||
cmp /tmp/expected-checksummed-release-assets /tmp/checksummed-release-assets
|
||||
sha256sum --check SHA256SUMS
|
||||
# 以本次 run 的可信 SHA256SUMS 为准校验已上传的 draft 资产,
|
||||
# 而不是相信 draft 自带的那一份。
|
||||
.github/scripts/release-verify-assets.sh \
|
||||
draft-release release/SHA256SUMS
|
||||
|
||||
- name: Verify GitHub Release channel metadata
|
||||
env:
|
||||
@@ -1236,22 +1139,8 @@ jobs:
|
||||
dockerhub_exact_digest=
|
||||
for repository in ghcr.io/tbphp/gpt-load tbphp/gpt-load; do
|
||||
exact="${repository}:${{ needs.validate-tag.outputs.image_exact }}"
|
||||
inspection="$(
|
||||
docker buildx imagetools inspect \
|
||||
"${exact}" \
|
||||
--format '{{json .}}'
|
||||
)"
|
||||
jq -e \
|
||||
--arg expected "${GITHUB_SHA}" \
|
||||
'[
|
||||
.image["linux/amd64"].config.Labels[
|
||||
"org.opencontainers.image.revision"
|
||||
],
|
||||
.image["linux/arm64"].config.Labels[
|
||||
"org.opencontainers.image.revision"
|
||||
]
|
||||
] | all(type == "string" and . == $expected)' \
|
||||
<<<"${inspection}" >/dev/null
|
||||
.github/scripts/release-verify-image-revision.sh \
|
||||
"${exact}" "${GITHUB_SHA}" >/dev/null
|
||||
exact_digest="$(
|
||||
.github/scripts/release-image-digest.sh "${exact}"
|
||||
)"
|
||||
@@ -1292,13 +1181,25 @@ jobs:
|
||||
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-native-smoke
|
||||
- post-publish-image-smoke
|
||||
- post-publish-verify
|
||||
runs-on: ubuntu-24.04
|
||||
@@ -1345,7 +1246,6 @@ jobs:
|
||||
PREFLIGHT_RESULT: ${{ needs.publication-preflight.result }}
|
||||
IMAGE_WRITER_RESULT: ${{ needs.publish-images.result }}
|
||||
GITHUB_WRITER_RESULT: ${{ needs.publish-github.result }}
|
||||
NATIVE_POST_RESULT: ${{ needs.post-publish-native-smoke.result }}
|
||||
IMAGE_SMOKE_RESULT: ${{ needs.post-publish-image-smoke.result }}
|
||||
VERIFY_POST_RESULT: ${{ needs.post-publish-verify.result }}
|
||||
shell: bash
|
||||
@@ -1363,10 +1263,11 @@ jobs:
|
||||
read_image_inventory() {
|
||||
local image="$1"
|
||||
local digest=
|
||||
local inspection=
|
||||
local revisions=
|
||||
local status=0
|
||||
|
||||
if [[ ! -x .github/scripts/release-image-digest.sh ]]; then
|
||||
if [[ ! -x .github/scripts/release-image-digest.sh ]] ||
|
||||
[[ ! -x .github/scripts/release-verify-image-revision.sh ]]; then
|
||||
printf 'unavailable'
|
||||
return
|
||||
fi
|
||||
@@ -1379,26 +1280,16 @@ jobs:
|
||||
printf 'absent'
|
||||
return
|
||||
fi
|
||||
inspection="$(
|
||||
docker buildx imagetools inspect \
|
||||
"${image}" \
|
||||
--format '{{json .}}' 2>/dev/null
|
||||
)"
|
||||
if [[ $? -ne 0 ]]; then
|
||||
printf '%s/revision-unavailable' "${digest}"
|
||||
return
|
||||
fi
|
||||
# 退出码 0/1 都带回可读的 revision 值,仅在无法读取清单时降级。
|
||||
revisions="$(
|
||||
jq -c '[
|
||||
.image["linux/amd64"].config.Labels[
|
||||
"org.opencontainers.image.revision"
|
||||
],
|
||||
.image["linux/arm64"].config.Labels[
|
||||
"org.opencontainers.image.revision"
|
||||
]
|
||||
]' <<<"${inspection}" 2>/dev/null
|
||||
.github/scripts/release-verify-image-revision.sh \
|
||||
"${image}" "${GITHUB_SHA}" 2>/dev/null
|
||||
)"
|
||||
printf '%s/%s' "${digest}" "${revisions:-revision-unavailable}"
|
||||
status=$?
|
||||
if ((status > 1)) || [[ -z "${revisions}" ]]; then
|
||||
revisions=revision-unavailable
|
||||
fi
|
||||
printf '%s/%s' "${digest}" "${revisions}"
|
||||
}
|
||||
|
||||
ghcr_inventory="$(
|
||||
@@ -1421,7 +1312,6 @@ jobs:
|
||||
printf -- '- preflight: `%s`\n' "${PREFLIGHT_RESULT}"
|
||||
printf -- '- image writer: `%s`\n' "${IMAGE_WRITER_RESULT}"
|
||||
printf -- '- GitHub writer: `%s`\n' "${GITHUB_WRITER_RESULT}"
|
||||
printf -- '- draft native smoke: `%s`\n' "${NATIVE_POST_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}"
|
||||
|
||||
Reference in new issue
Block a user