1087 lines
40 KiB
YAML
1087 lines
40 KiB
YAML
name: Release 2.x
|
|
|
|
on:
|
|
push:
|
|
tags:
|
|
- "v2.*"
|
|
|
|
permissions:
|
|
contents: read
|
|
|
|
concurrency:
|
|
group: release-${{ github.ref }}
|
|
cancel-in-progress: false
|
|
|
|
jobs:
|
|
validate-tag:
|
|
runs-on: ubuntu-24.04
|
|
timeout-minutes: 5
|
|
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 }}
|
|
steps:
|
|
- name: Checkout tag commit
|
|
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
|
with:
|
|
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
|
|
if [[ "${version}" == *-* ]]; then
|
|
prerelease=true
|
|
prerelease_value="${version#*-}"
|
|
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}"
|
|
} >>"${GITHUB_OUTPUT}"
|
|
# release-tag-validation:end
|
|
|
|
- name: Require tag commit in v2 history
|
|
run: |
|
|
git fetch --no-tags origin v2
|
|
git merge-base --is-ancestor "${GITHUB_SHA}" origin/v2
|
|
|
|
verify-and-build-web:
|
|
needs: validate-tag
|
|
runs-on: ubuntu-24.04
|
|
timeout-minutes: 30
|
|
steps:
|
|
- name: Checkout tag commit
|
|
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
|
|
|
- name: Validate and build web UI
|
|
uses: ./.github/actions/web-ci
|
|
|
|
- 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: Audit Go dependencies
|
|
run: go run golang.org/x/vuln/cmd/govulncheck@v1.1.4 ./...
|
|
|
|
- name: Run Go vet
|
|
run: go vet ./...
|
|
|
|
- name: Build backend
|
|
run: go build -trimpath -o gpt-load .
|
|
|
|
- name: Run race-enabled tests
|
|
run: go test -race -count=1 . ./internal/...
|
|
|
|
- name: Install Chromium for browser E2E
|
|
run: pnpm --dir web exec playwright install --with-deps chromium
|
|
|
|
- name: Run production browser E2E
|
|
run: pnpm --dir web run test:e2e
|
|
|
|
- name: Check repository invariants
|
|
run: git diff --check
|
|
|
|
- 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
|
|
retention-days: 14
|
|
|
|
build-binaries:
|
|
needs: verify-and-build-web
|
|
runs-on: ubuntu-24.04
|
|
timeout-minutes: 15
|
|
strategy:
|
|
fail-fast: true
|
|
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
|
|
|
|
- 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
|
|
retention-days: 14
|
|
|
|
package-checksums:
|
|
needs: build-binaries
|
|
runs-on: ubuntu-24.04
|
|
timeout-minutes: 5
|
|
steps:
|
|
- name: Download all release binaries
|
|
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
|
|
with:
|
|
pattern: binary-gpt-load-*
|
|
path: release
|
|
merge-multiple: true
|
|
|
|
- name: Generate SHA256SUMS
|
|
working-directory: release
|
|
run: |
|
|
sha256sum \
|
|
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
|
|
|
|
- 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
|
|
|
|
native-artifact-smoke:
|
|
needs:
|
|
- build-binaries
|
|
- package-checksums
|
|
timeout-minutes: 10
|
|
strategy:
|
|
fail-fast: true
|
|
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
|
|
|
|
- name: Download complete release assets
|
|
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
|
|
with:
|
|
name: release-assets
|
|
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
|
|
- verify-and-build-web
|
|
runs-on: ubuntu-24.04
|
|
timeout-minutes: 30
|
|
steps:
|
|
- name: Checkout tag commit
|
|
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
|
|
|
- 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
|
|
|
|
publication-preflight:
|
|
needs:
|
|
- validate-tag
|
|
- verify-and-build-web
|
|
- build-binaries
|
|
- package-checksums
|
|
- native-artifact-smoke
|
|
- docker-smoke
|
|
runs-on: ubuntu-24.04
|
|
timeout-minutes: 20
|
|
permissions:
|
|
contents: read
|
|
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:
|
|
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
|
|
|
|
cat >/tmp/expected-release-assets <<'ASSETS'
|
|
SHA256SUMS
|
|
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 ' ')" = "5"
|
|
sha256sum --check SHA256SUMS
|
|
|
|
- name: Require publication credentials and verify read access
|
|
shell: bash
|
|
env:
|
|
GH_TOKEN: ${{ github.token }}
|
|
DOCKERHUB_USERNAME: ${{ secrets.DOCKERHUB_USERNAME }}
|
|
DOCKERHUB_READ_TOKEN: ${{ secrets.DOCKERHUB_READ_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: Set up Docker Buildx
|
|
uses: docker/setup-buildx-action@bb05f3f5519dd87d3ba754cc423b652a5edd6d2c # v4.2.0
|
|
|
|
- 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 }}
|
|
|
|
- name: Inventory publication channels
|
|
id: inventory
|
|
env:
|
|
GH_TOKEN: ${{ github.token }}
|
|
shell: bash
|
|
run: |
|
|
set -euo pipefail
|
|
|
|
github_state=absent
|
|
github_target_sha=
|
|
github_assets=absent
|
|
if gh api \
|
|
"repos/${GITHUB_REPOSITORY}/releases/tags/${GITHUB_REF_NAME}" \
|
|
>/tmp/existing-release.json 2>/tmp/existing-release.error; then
|
|
github_state=present
|
|
github_target_sha="$(
|
|
jq -r '.target_commitish // empty' /tmp/existing-release.json
|
|
)"
|
|
jq -r '.assets[].name' /tmp/existing-release.json |
|
|
sort >/tmp/existing-release-assets
|
|
if cmp \
|
|
/tmp/expected-release-assets \
|
|
/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
|
|
fi
|
|
else
|
|
github_assets=mismatch
|
|
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 inspection=
|
|
local revision=mismatch
|
|
|
|
digest="$(.github/scripts/release-image-digest.sh "${image}")"
|
|
if [[ "${digest}" == "absent" ]]; then
|
|
printf 'absent|absent|\n'
|
|
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}"
|
|
fi
|
|
# release-image-revision-validation:end
|
|
printf 'present|%s|%s\n' "${digest}" "${revision}"
|
|
}
|
|
|
|
IFS='|' read -r ghcr_state ghcr_digest ghcr_revision < <(
|
|
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 }}"
|
|
)
|
|
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}" \
|
|
"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
|
|
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
|
|
|
|
- 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 }}
|
|
|
|
- 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: .
|
|
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"
|
|
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-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 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
|
|
runs-on: ubuntu-24.04
|
|
timeout-minutes: 10
|
|
permissions:
|
|
contents: write
|
|
steps:
|
|
- 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
|
|
|
|
- name: Verify existing GitHub Release
|
|
if: ${{ needs.publication-preflight.outputs.write_mode == 'verify' }}
|
|
env:
|
|
GH_TOKEN: ${{ github.token }}
|
|
shell: bash
|
|
run: |
|
|
set -euo pipefail
|
|
target="$(
|
|
gh release view "${GITHUB_REF_NAME}" \
|
|
--repo "${GITHUB_REPOSITORY}" \
|
|
--json targetCommitish \
|
|
--jq '.targetCommitish'
|
|
)"
|
|
test "${target}" = "${GITHUB_SHA}"
|
|
rm -rf existing-release
|
|
mkdir existing-release
|
|
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
|
|
)
|
|
|
|
- name: Publish one GitHub Release
|
|
if: ${{ needs.publication-preflight.outputs.write_mode == 'publish' }}
|
|
uses: softprops/action-gh-release@3d0d9888cb7fd7b750713d6e236d1fcb99157228 # v3.0.2
|
|
with:
|
|
name: GPT-Load ${{ github.ref_name }}
|
|
target_commitish: ${{ github.sha }}
|
|
prerelease: ${{ needs.validate-tag.outputs.prerelease }}
|
|
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/SHA256SUMS
|
|
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.
|
|
|
|
This release provides native OpenAI, Anthropic, and Gemini protocol gateways,
|
|
an embedded management UI, scheduling and health controls, request logs,
|
|
usage estimates, and model price management. It is single-instance and SQLite-only.
|
|
|
|
GPT-Load uses a unified data/control dual-plane architecture: Groups define upstream
|
|
routing and key pools, AccessKeys authorize data-plane access, and model discovery
|
|
imports available models from compatible upstreams.
|
|
|
|
Docker runs as a fixed non-root user with a named volume. The 2.x image channels
|
|
do not move `latest`. Stop cleanly, then keep the SQLite database, its `-wal`/`-shm`
|
|
sidecars, and auth/encryption key files together as one recovery set. Usage and cost
|
|
are best-effort estimates, not a billing ledger or provider invoice.
|
|
|
|
Follow the [public operations baseline](https://github.com/${{ github.repository }}/blob/${{ github.ref_name }}/README.md#public-operations-baseline)
|
|
for deployment, backup/restore, and 1.x cutover and rollback.
|
|
This release contains five raw binaries plus `SHA256SUMS`.
|
|
If publication stops between channels, inspect the reconciliation inventory and
|
|
recover manually; the workflow does not claim cross-system atomic rollback.
|
|
|
|
Usage quality can be `missing`, `partial`, or `unpriced`; a compatible upstream may
|
|
never provide authoritative terminal usage. Cost remains an estimate, not a provider
|
|
bill. GPT-Load 2.0.0 does not support encryption key rotation.
|
|
|
|
Built-in prices were reviewed on 2026-07-27 against the official
|
|
[OpenAI](https://developers.openai.com/api/docs/pricing),
|
|
[Anthropic](https://platform.claude.com/docs/en/about-claude/pricing), and
|
|
[Gemini](https://ai.google.dev/gemini-api/docs/pricing) sources.
|
|
|
|
post-publish-native-smoke:
|
|
needs:
|
|
- publish-github
|
|
- publish-images
|
|
timeout-minutes: 15
|
|
permissions:
|
|
contents: read
|
|
strategy:
|
|
fail-fast: true
|
|
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
|
|
|
|
- name: Download published GitHub Release assets
|
|
shell: bash
|
|
env:
|
|
GH_TOKEN: ${{ github.token }}
|
|
run: |
|
|
set -euo pipefail
|
|
mkdir -p public-release
|
|
gh release download "${GITHUB_REF_NAME}" \
|
|
--repo "${GITHUB_REPOSITORY}" \
|
|
--dir public-release
|
|
|
|
- name: Smoke published POSIX native artifact
|
|
if: runner.os != 'Windows'
|
|
shell: bash
|
|
env:
|
|
RELEASE_VERSION: ${{ github.ref_name }}
|
|
RELEASE_SMOKE_BINARY: ${{ github.workspace }}/public-release/${{ matrix.filename }}
|
|
RELEASE_SMOKE_CHECKSUM_FILE: ${{ github.workspace }}/public-release/SHA256SUMS
|
|
run: .github/scripts/release-native-smoke.sh
|
|
|
|
- name: Smoke published Windows native artifact
|
|
if: runner.os == 'Windows'
|
|
shell: pwsh
|
|
env:
|
|
RELEASE_VERSION: ${{ github.ref_name }}
|
|
RELEASE_SMOKE_BINARY: ${{ github.workspace }}/public-release/${{ matrix.filename }}
|
|
RELEASE_SMOKE_CHECKSUM_FILE: ${{ github.workspace }}/public-release/SHA256SUMS
|
|
run: .github/scripts/release-native-smoke.ps1
|
|
|
|
post-publish-verify:
|
|
needs:
|
|
- validate-tag
|
|
- publication-preflight
|
|
- publish-github
|
|
- publish-images
|
|
- post-publish-native-smoke
|
|
runs-on: ubuntu-24.04
|
|
timeout-minutes: 45
|
|
permissions:
|
|
contents: read
|
|
packages: read
|
|
steps:
|
|
- name: Checkout tag commit
|
|
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
|
|
|
- name: Download and verify exact GitHub Release asset inventory
|
|
env:
|
|
GH_TOKEN: ${{ github.token }}
|
|
shell: bash
|
|
run: |
|
|
set -euo pipefail
|
|
mkdir -p public-release
|
|
gh release download "${GITHUB_REF_NAME}" \
|
|
--repo "${GITHUB_REPOSITORY}" \
|
|
--dir public-release
|
|
|
|
cat >/tmp/expected-release-assets <<'ASSETS'
|
|
SHA256SUMS
|
|
gpt-load-linux-amd64
|
|
gpt-load-linux-arm64
|
|
gpt-load-macos-amd64
|
|
gpt-load-macos-arm64
|
|
gpt-load-windows-amd64.exe
|
|
ASSETS
|
|
find public-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}" = "6"
|
|
cmp /tmp/expected-release-assets /tmp/actual-release-assets
|
|
|
|
cd public-release
|
|
test "$(wc -l <SHA256SUMS | tr -d ' ')" = "5"
|
|
awk '{print $2}' SHA256SUMS | sort >/tmp/checksummed-release-assets
|
|
tail -n +2 /tmp/expected-release-assets >/tmp/expected-checksummed-release-assets
|
|
cmp /tmp/expected-checksummed-release-assets /tmp/checksummed-release-assets
|
|
sha256sum --check SHA256SUMS
|
|
|
|
- name: Set up Docker Buildx
|
|
uses: docker/setup-buildx-action@bb05f3f5519dd87d3ba754cc423b652a5edd6d2c # v4.2.0
|
|
|
|
- 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 }}
|
|
|
|
- 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 }}"
|
|
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
|
|
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.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: Run full runtime smoke for both published exact images
|
|
shell: bash
|
|
env:
|
|
RELEASE_VERSION: ${{ github.ref_name }}
|
|
run: |
|
|
set -euo pipefail
|
|
for source_and_suffix in \
|
|
"ghcr.io/tbphp/gpt-load:${{ needs.validate-tag.outputs.image_exact }}|ghcr" \
|
|
"tbphp/gpt-load:${{ needs.validate-tag.outputs.image_exact }}|dockerhub"; do
|
|
source_image="${source_and_suffix%%|*}"
|
|
smoke_suffix="${source_and_suffix##*|}"
|
|
RELEASE_SMOKE_SOURCE_IMAGE="${source_image}" \
|
|
RELEASE_SMOKE_SUFFIX="${GITHUB_RUN_ID}-${smoke_suffix}" \
|
|
.github/scripts/release-docker-smoke.sh
|
|
done
|
|
|
|
reconcile-publication:
|
|
if: ${{ always() }}
|
|
needs:
|
|
- publication-preflight
|
|
- publish-images
|
|
- publish-github
|
|
- post-publish-native-smoke
|
|
- post-publish-verify
|
|
runs-on: ubuntu-24.04
|
|
timeout-minutes: 10
|
|
permissions:
|
|
contents: read
|
|
packages: read
|
|
steps:
|
|
- name: Checkout tag commit for read-only reconciliation
|
|
if: ${{ always() }}
|
|
continue-on-error: true
|
|
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
|
|
|
- name: Set up Docker Buildx for read-only reconciliation
|
|
if: ${{ always() }}
|
|
continue-on-error: true
|
|
uses: docker/setup-buildx-action@bb05f3f5519dd87d3ba754cc423b652a5edd6d2c # v4.2.0
|
|
|
|
- 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 }}
|
|
|
|
- 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 }}
|
|
NATIVE_POST_RESULT: ${{ needs.post-publish-native-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 inspection=
|
|
local revisions=
|
|
|
|
if [[ ! -x .github/scripts/release-image-digest.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
|
|
inspection="$(
|
|
docker buildx imagetools inspect \
|
|
"${image}" \
|
|
--format '{{json .}}' 2>/dev/null
|
|
)"
|
|
if [[ $? -ne 0 ]]; then
|
|
printf '%s/revision-unavailable' "${digest}"
|
|
return
|
|
fi
|
|
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
|
|
)"
|
|
printf '%s/%s' "${digest}" "${revisions:-revision-unavailable}"
|
|
}
|
|
|
|
ghcr_inventory="$(
|
|
read_image_inventory \
|
|
"ghcr.io/tbphp/gpt-load:${GITHUB_REF_NAME}"
|
|
)"
|
|
dockerhub_inventory="$(
|
|
read_image_inventory \
|
|
"tbphp/gpt-load:${GITHUB_REF_NAME}"
|
|
)"
|
|
|
|
{
|
|
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 native smoke: `%s`\n' "${NATIVE_POST_RESULT}"
|
|
printf -- '- post-publish 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 '\nmanual recovery: inspect the exact inventories and job logs; '
|
|
printf 'do not assume cross-system atomic rollback.\n'
|
|
} >>"${GITHUB_STEP_SUMMARY}"
|