chore(release): 规范 V2 预发布与协作流程
This commit is contained in:
8 files changed
+482
-59
No files matched your search
@@ -9,16 +9,37 @@ assignees: ""
|
||||
**例行检查 / Checklist**
|
||||
<!-- 方框内填 'x' / Put an 'x' in all the boxes that apply -->
|
||||
- [ ] 我已确认目前没有类似 issue (I have checked for similar issues)
|
||||
- [ ] 我已确认我已升级到最新版本 (I have updated to the latest version)
|
||||
- [ ] 我已完整查看过项目 README,尤其是常见问题部分 (I have read the README, especially the FAQ section)
|
||||
- [ ] 我已升级到当前主版本线的最新补丁版本 (I am using the latest patch release in my current major line)
|
||||
- [ ] 我已查看对应版本线的 README 和发布说明 (I have read the README and release notes for my release line)
|
||||
- [ ] 我已移除 `AUTH_KEY`、`ENCRYPTION_KEY`、AccessKey、上游密钥和令牌等敏感信息 (I have removed all credentials, keys, and tokens)
|
||||
- [ ] 我理解并愿意跟进此 issue,协助测试和提供反馈 (I am willing to follow up on this issue, assist with testing, and provide feedback)
|
||||
- [ ] 我理解并认可上述内容,并理解项目维护者精力有限,**不遵循规则的 issue 可能会被无视或直接关闭** (I understand and agree to the above, and I understand that the maintainers have limited time, so **issues that do not follow the rules may be ignored or closed directly**)
|
||||
|
||||
---
|
||||
|
||||
**问题描述 / Bug Description**
|
||||
<!-- 请清晰、简洁地描述问题。 / A clear and concise description of what the bug is. -->
|
||||
|
||||
**GPT-Load 版本 / Version**
|
||||
<!-- 请填写精确版本或 commit,不要只写 latest。 / Provide the exact version or commit, not only "latest". -->
|
||||
|
||||
**版本线 / Release Line**
|
||||
<!-- 请填写 1.x 或 2.x;二者不兼容且数据不互通。 / Specify 1.x or 2.x; they are incompatible and do not share data. -->
|
||||
|
||||
**部署环境 / Environment**
|
||||
<!--
|
||||
- 部署方式(Docker、原生二进制或源码)/ Deployment method
|
||||
- 操作系统与架构 / Operating system and architecture
|
||||
- 数据库及版本(SQLite、MySQL 或 PostgreSQL)/ Database and version
|
||||
- 浏览器或客户端 SDK 及版本 / Browser or client SDK and version
|
||||
-->
|
||||
|
||||
**渠道与客户端协议 / Channel and Client Protocol**
|
||||
<!-- 例如 OpenAI 官方渠道 + openai-responses;不适用时填写 N/A。请勿粘贴凭据。 -->
|
||||
<!-- Example: OpenAI official + openai-responses. Use N/A when not applicable. Never paste credentials. -->
|
||||
|
||||
**最小脱敏配置 / Minimal Sanitized Configuration**
|
||||
<!-- 仅提供复现所需配置;不得包含密钥、令牌、Cookie 或完整请求头。 -->
|
||||
<!-- Include only what is needed to reproduce; never include secrets, tokens, cookies, or complete authorization headers. -->
|
||||
|
||||
**复现步骤 / Steps to Reproduce**
|
||||
<!-- 请提供复现问题的具体步骤。 / Steps to reproduce the behavior. -->
|
||||
@@ -26,6 +47,9 @@ assignees: ""
|
||||
2.
|
||||
3.
|
||||
|
||||
**实际结果及脱敏日志 / Actual Result and Sanitized Logs**
|
||||
<!-- 提供完整错误信息或最小日志片段,并确认已经脱敏。 / Include the exact error or minimal logs after removing sensitive data. -->
|
||||
|
||||
**预期结果 / Expected Behavior**
|
||||
<!-- 请描述你预期的行为。 / A clear and concise description of what you expected to happen. -->
|
||||
|
||||
|
||||
@@ -9,16 +9,33 @@ assignees: ""
|
||||
**例行检查 / Checklist**
|
||||
<!-- 方框内填 'x' / Put an 'x' in all the boxes that apply -->
|
||||
- [ ] 我已确认目前没有类似 issue (I have checked for similar issues)
|
||||
- [ ] 我已确认我已升级到最新版本 (I have updated to the latest version)
|
||||
- [ ] 我已完整查看过项目 README,已确定现有版本无法满足需求 (I have read the README and confirmed the current version does not meet my needs)
|
||||
- [ ] 我已确认当前主版本线的最新补丁版本仍无法满足需求 (I have confirmed the latest patch release in my current major line does not meet this need)
|
||||
- [ ] 我已查看对应版本线的 README 和发布说明 (I have read the README and release notes for my release line)
|
||||
- [ ] 我不会在 issue 中提交任何凭据或令牌 (I will not include credentials or tokens in this issue)
|
||||
- [ ] 我理解并愿意跟进此 issue,协助测试和提供反馈 (I am willing to follow up on this issue, assist with testing, and provide feedback)
|
||||
- [ ] 我理解并认可上述内容,并理解项目维护者精力有限,**不遵循规则的 issue 可能会被无视或直接关闭** (I understand and agree to the above, and I understand that the maintainers have limited time, so **issues that do not follow the rules may be ignored or closed directly**)
|
||||
<!-- 不要提交任何凭据或令牌。 / Never submit credentials or tokens. -->
|
||||
|
||||
---
|
||||
|
||||
**目标版本线 / Target Release Line**
|
||||
<!-- 请填写 1.x 维护线或 2.x;二者不兼容且数据不互通。 / Specify the 1.x maintenance line or 2.x; they are incompatible and do not share data. -->
|
||||
|
||||
**功能描述 / Feature Description**
|
||||
<!-- 请清晰、简洁地描述你希望实现的功能。 / A clear and concise description of what you want to happen. -->
|
||||
|
||||
|
||||
**应用场景 / Use Case**
|
||||
<!-- 请描述该功能可以解决什么问题或在什么场景下使用。 / Please describe what problem this feature would solve or in what scenarios it would be used. -->
|
||||
|
||||
|
||||
**期望方案 / Desired Behavior**
|
||||
<!-- 请描述用户可观察到的结果,而不是只描述实现方式。 / Describe the user-visible outcome, not only an implementation idea. -->
|
||||
|
||||
|
||||
**兼容性、数据与安全影响 / Compatibility, Data, and Security Impact**
|
||||
<!-- 是否影响 API、配置、数据库、密钥或现有部署?不适用时填写 N/A。 -->
|
||||
<!-- Would this affect APIs, configuration, databases, credentials, or existing deployments? Use N/A when not applicable. -->
|
||||
|
||||
|
||||
**备选方案 / Alternatives Considered**
|
||||
<!-- 请说明当前使用的替代方法(如有)。 / Describe any current workaround or alternative. -->
|
||||
@@ -12,5 +12,8 @@ Closes #
|
||||
|
||||
|
||||
### 自查清单 / Checklist
|
||||
- [ ] 我已在本地测试过我的变更。 / I have tested my changes locally.
|
||||
- [ ] 我已更新了必要的文档。 / I have updated the necessary documentation.
|
||||
- [ ] 我已运行 `make check`,或在说明中写明无法运行的原因和未验证范围。 / I ran `make check`, or documented why it could not run and what remains unverified.
|
||||
- [ ] 本 PR 范围聚焦,未包含无关改动。 / This PR is focused and contains no unrelated changes.
|
||||
- [ ] 我已更新必要的公开文档或发布说明。 / I updated any required public documentation or release notes.
|
||||
- [ ] 我已确认提交、日志和测试数据不包含敏感信息。 / I confirmed that commits, logs, and fixtures contain no sensitive data.
|
||||
- [ ] 如适用,我已说明兼容性或数据迁移影响。 / Where applicable, I documented compatibility or data-migration impact.
|
||||
@@ -11,6 +11,10 @@ on:
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
concurrency:
|
||||
group: v2-ci-${{ github.event.pull_request.number || github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
test:
|
||||
runs-on: ubuntu-24.04
|
||||
@@ -18,6 +22,8 @@ jobs:
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Validate and build web UI
|
||||
uses: ./.github/actions/web-ci
|
||||
@@ -62,6 +68,8 @@ jobs:
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Set up Go
|
||||
uses: actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e # v7.0.0
|
||||
@@ -116,6 +124,8 @@ jobs:
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Set up Go
|
||||
uses: actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e # v7.0.0
|
||||
@@ -135,6 +145,8 @@ jobs:
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Set up Go
|
||||
uses: actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e # v7.0.0
|
||||
|
||||
+165
-38
@@ -22,10 +22,12 @@ jobs:
|
||||
image_exact: ${{ steps.tag.outputs.image_exact }}
|
||||
image_minor: ${{ steps.tag.outputs.image_minor }}
|
||||
image_major: ${{ steps.tag.outputs.image_major }}
|
||||
beta: ${{ steps.tag.outputs.beta }}
|
||||
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
|
||||
@@ -43,9 +45,14 @@ jobs:
|
||||
version="${tag#v}"
|
||||
core="${version%%-*}"
|
||||
prerelease=false
|
||||
beta=false
|
||||
if [[ "${version}" == *-* ]]; then
|
||||
prerelease=true
|
||||
prerelease_value="${version#*-}"
|
||||
if [[ "${prerelease_value}" == "beta" ||
|
||||
"${prerelease_value}" == beta.* ]]; then
|
||||
beta=true
|
||||
fi
|
||||
IFS='.' read -r -a identifiers <<<"${prerelease_value}"
|
||||
for identifier in "${identifiers[@]}"; do
|
||||
if [[ "${identifier}" =~ ^[0-9]+$ ]] &&
|
||||
@@ -64,6 +71,7 @@ jobs:
|
||||
printf 'image_exact=%s\n' "${tag}"
|
||||
printf 'image_minor=%s.%s\n' "${major}" "${minor}"
|
||||
printf 'image_major=%s\n' "${major}"
|
||||
printf 'beta=%s\n' "${beta}"
|
||||
} >>"${GITHUB_OUTPUT}"
|
||||
# release-tag-validation:end
|
||||
|
||||
@@ -79,6 +87,8 @@ jobs:
|
||||
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
|
||||
@@ -132,6 +142,8 @@ jobs:
|
||||
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
|
||||
@@ -187,6 +199,8 @@ jobs:
|
||||
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
|
||||
@@ -226,6 +240,8 @@ jobs:
|
||||
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
|
||||
@@ -265,6 +281,8 @@ jobs:
|
||||
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
|
||||
@@ -333,6 +351,8 @@ jobs:
|
||||
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
|
||||
@@ -369,6 +389,8 @@ jobs:
|
||||
steps:
|
||||
- name: Checkout tag commit
|
||||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Build and smoke local image
|
||||
shell: bash
|
||||
@@ -411,6 +433,7 @@ jobs:
|
||||
- name: Checkout tag commit
|
||||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
with:
|
||||
persist-credentials: false
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Download complete release assets
|
||||
@@ -495,15 +518,32 @@ jobs:
|
||||
set -euo pipefail
|
||||
|
||||
github_state=absent
|
||||
github_release_id=
|
||||
github_target_sha=
|
||||
github_assets=absent
|
||||
if gh api \
|
||||
"repos/${GITHUB_REPOSITORY}/releases/tags/${GITHUB_REF_NAME}" \
|
||||
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_target_sha="$(
|
||||
jq -r '.target_commitish // empty' /tmp/existing-release.json
|
||||
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 |
|
||||
sort >/tmp/existing-release-assets
|
||||
if cmp \
|
||||
@@ -605,7 +645,7 @@ jobs:
|
||||
"${dockerhub_latest_digest}"
|
||||
} >>"${GITHUB_OUTPUT}"
|
||||
printf '%s\n' \
|
||||
"github=${github_state}/${github_target_sha}/${github_assets}" \
|
||||
"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}" \
|
||||
@@ -662,6 +702,8 @@ jobs:
|
||||
steps:
|
||||
- name: Checkout tag commit
|
||||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Set up QEMU
|
||||
if: ${{ needs.publication-preflight.outputs.write_mode == 'publish' }}
|
||||
@@ -758,6 +800,18 @@ jobs:
|
||||
test -n "${ghcr_exact_digest}"
|
||||
test "${ghcr_exact_digest}" = "${dockerhub_exact_digest}"
|
||||
|
||||
- name: Update beta channel alias
|
||||
if: ${{ needs.publication-preflight.outputs.write_mode == 'publish' && needs.validate-tag.outputs.beta == 'true' }}
|
||||
shell: bash
|
||||
run: |
|
||||
set -euo pipefail
|
||||
for repository in ghcr.io/tbphp/gpt-load tbphp/gpt-load; do
|
||||
exact="${repository}:${{ needs.validate-tag.outputs.image_exact }}"
|
||||
docker buildx imagetools create \
|
||||
--tag "${repository}:v2beta" \
|
||||
"${exact}"
|
||||
done
|
||||
|
||||
- name: Update stable major and minor aliases
|
||||
if: ${{ needs.publication-preflight.outputs.write_mode == 'publish' && needs.validate-tag.outputs.prerelease == 'false' }}
|
||||
shell: bash
|
||||
@@ -800,13 +854,29 @@ jobs:
|
||||
shell: bash
|
||||
run: |
|
||||
set -euo pipefail
|
||||
target="$(
|
||||
metadata="$(
|
||||
gh release view "${GITHUB_REF_NAME}" \
|
||||
--repo "${GITHUB_REPOSITORY}" \
|
||||
--json targetCommitish \
|
||||
--jq '.targetCommitish'
|
||||
--json targetCommitish,isDraft,isPrerelease
|
||||
)"
|
||||
test "${target}" = "${GITHUB_SHA}"
|
||||
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}" \
|
||||
@@ -818,13 +888,16 @@ jobs:
|
||||
sha256sum --check ../release/SHA256SUMS
|
||||
)
|
||||
|
||||
- name: Publish one GitHub Release
|
||||
- name: Create or update GitHub Release draft
|
||||
if: ${{ needs.publication-preflight.outputs.write_mode == 'publish' }}
|
||||
uses: softprops/action-gh-release@3d0d9888cb7fd7b750713d6e236d1fcb99157228 # v3.0.2
|
||||
with:
|
||||
name: GPT-Load ${{ 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
|
||||
files: |
|
||||
release/gpt-load-linux-amd64
|
||||
@@ -840,8 +913,9 @@ jobs:
|
||||
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.
|
||||
Earlier pre-release 2.x databases are not supported. Start this release with an empty
|
||||
dedicated database and a matching new encryption key.
|
||||
New 2.x deployments must start with an empty dedicated database and a matching new
|
||||
encryption key. Existing supported 2.x installations must preserve their matching key
|
||||
files and follow the version-specific release notes before upgrading.
|
||||
|
||||
This release provides OpenAI, Anthropic, and Gemini client protocol gateways backed
|
||||
by channel presets for official, compatible, Azure OpenAI, AWS Bedrock, and Google
|
||||
@@ -855,9 +929,11 @@ jobs:
|
||||
provider SDK executes one selected logical attempt and performs supported protocol
|
||||
conversion; its name is not part of persisted domain identities.
|
||||
|
||||
Docker runs as a fixed non-root user with a named volume. The 2.x image channels
|
||||
do not move `latest`. Stop cleanly, then back up the selected database using its
|
||||
engine-native procedure and keep auth/encryption key files with the recovery set.
|
||||
Docker runs as a fixed non-root user with a named volume. Verified Beta images move
|
||||
the shared `v2beta` channel while retaining the exact version tag; stable releases
|
||||
move only their major/minor aliases. No 2.x release moves `latest`. Stop cleanly,
|
||||
then back up the selected database using its engine-native procedure and keep
|
||||
auth/encryption key files with the recovery set.
|
||||
Managed SQLite also requires its `-wal`/`-shm` sidecars. Usage and cost are best-effort
|
||||
estimates, not a billing ledger or provider invoice.
|
||||
|
||||
@@ -870,18 +946,16 @@ jobs:
|
||||
|
||||
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.
|
||||
bill. The current 2.x line does not support encryption key rotation.
|
||||
|
||||
Models.dev is the sole automatic price source. Prices are resolved by exact
|
||||
`channel_id + model_id`, may be overridden explicitly, and remain estimates.
|
||||
The automatic model catalog combines an embedded official catalog with Models.dev;
|
||||
embedded official records take precedence while other Models.dev records remain
|
||||
available. Prices resolve by exact `channel_id + model_id`; explicit user overrides
|
||||
remain available, and all costs remain estimates.
|
||||
|
||||
GPT-Load keeps a unified data/control dual-plane architecture: Groups select a
|
||||
channel and credentials, AccessKeys authorize data-plane access, and model discovery
|
||||
imports available models from the selected channel. 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.
|
||||
imports available models from the selected channel.
|
||||
|
||||
post-publish-native-smoke:
|
||||
needs:
|
||||
@@ -908,34 +982,36 @@ jobs:
|
||||
steps:
|
||||
- name: Checkout tag commit
|
||||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Download published GitHub Release assets
|
||||
- name: Download draft GitHub Release assets
|
||||
shell: bash
|
||||
env:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
mkdir -p public-release
|
||||
mkdir -p draft-release
|
||||
gh release download "${GITHUB_REF_NAME}" \
|
||||
--repo "${GITHUB_REPOSITORY}" \
|
||||
--dir public-release
|
||||
--dir draft-release
|
||||
|
||||
- name: Smoke published POSIX native artifact
|
||||
- name: Smoke drafted 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
|
||||
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 published Windows native artifact
|
||||
- name: Smoke drafted 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
|
||||
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-verify:
|
||||
@@ -953,6 +1029,8 @@ jobs:
|
||||
steps:
|
||||
- name: Checkout tag commit
|
||||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Download and verify exact GitHub Release asset inventory
|
||||
env:
|
||||
@@ -960,10 +1038,10 @@ jobs:
|
||||
shell: bash
|
||||
run: |
|
||||
set -euo pipefail
|
||||
mkdir -p public-release
|
||||
mkdir -p draft-release
|
||||
gh release download "${GITHUB_REF_NAME}" \
|
||||
--repo "${GITHUB_REPOSITORY}" \
|
||||
--dir public-release
|
||||
--dir draft-release
|
||||
|
||||
cat >/tmp/expected-release-assets <<'ASSETS'
|
||||
Apache-2.0.txt
|
||||
@@ -977,13 +1055,13 @@ jobs:
|
||||
gpt-load-macos-arm64
|
||||
gpt-load-windows-amd64.exe
|
||||
ASSETS
|
||||
find public-release -mindepth 1 -maxdepth 1 -type f -exec basename {} \; |
|
||||
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}" = "10"
|
||||
cmp /tmp/expected-release-assets /tmp/actual-release-assets
|
||||
|
||||
cd public-release
|
||||
cd draft-release
|
||||
test "$(wc -l <SHA256SUMS | tr -d ' ')" = "9"
|
||||
awk '{print $2}' SHA256SUMS | sort >/tmp/checksummed-release-assets
|
||||
grep -v '^SHA256SUMS$' /tmp/expected-release-assets \
|
||||
@@ -991,6 +1069,35 @@ jobs:
|
||||
cmp /tmp/expected-checksummed-release-assets /tmp/checksummed-release-assets
|
||||
sha256sum --check 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: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@bb05f3f5519dd87d3ba754cc423b652a5edd6d2c # v4.2.0
|
||||
|
||||
@@ -1048,6 +1155,14 @@ jobs:
|
||||
test "${latest_digest}" = "${expected_latest_digest}"
|
||||
test "${latest_digest}" != "${exact_digest}"
|
||||
|
||||
if [[ "${{ needs.validate-tag.outputs.beta }}" == "true" ]]; then
|
||||
beta_digest="$(
|
||||
.github/scripts/release-image-digest.sh \
|
||||
"${repository}:v2beta"
|
||||
)"
|
||||
test "${beta_digest}" = "${exact_digest}"
|
||||
fi
|
||||
|
||||
if [[ "${{ needs.validate-tag.outputs.prerelease }}" == "false" ]]; then
|
||||
for alias in \
|
||||
"${{ needs.validate-tag.outputs.image_minor }}" \
|
||||
@@ -1097,6 +1212,8 @@ jobs:
|
||||
if: ${{ always() }}
|
||||
continue-on-error: true
|
||||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Set up Docker Buildx for read-only reconciliation
|
||||
if: ${{ always() }}
|
||||
@@ -1191,18 +1308,28 @@ jobs:
|
||||
read_image_inventory \
|
||||
"tbphp/gpt-load:${GITHUB_REF_NAME}"
|
||||
)"
|
||||
ghcr_beta_inventory="$(
|
||||
read_image_inventory ghcr.io/tbphp/gpt-load:v2beta
|
||||
)"
|
||||
dockerhub_beta_inventory="$(
|
||||
read_image_inventory tbphp/gpt-load:v2beta
|
||||
)"
|
||||
|
||||
{
|
||||
printf '## Publication reconciliation\n\n'
|
||||
printf -- '- preflight: `%s`\n' "${PREFLIGHT_RESULT}"
|
||||
printf -- '- image writer: `%s`\n' "${IMAGE_WRITER_RESULT}"
|
||||
printf -- '- GitHub writer: `%s`\n' "${GITHUB_WRITER_RESULT}"
|
||||
printf -- '- published native smoke: `%s`\n' "${NATIVE_POST_RESULT}"
|
||||
printf -- '- post-publish verification: `%s`\n' "${VERIFY_POST_RESULT}"
|
||||
printf -- '- draft native smoke: `%s`\n' "${NATIVE_POST_RESULT}"
|
||||
printf -- '- draft and image verification: `%s`\n' "${VERIFY_POST_RESULT}"
|
||||
printf -- '- GitHub exact inventory: `%s`\n' "${github_inventory}"
|
||||
printf -- '- GHCR exact inventory: `%s`\n' "${ghcr_inventory}"
|
||||
printf -- '- Docker Hub exact inventory: `%s`\n' \
|
||||
"${dockerhub_inventory}"
|
||||
printf -- '- GHCR v2beta inventory: `%s`\n' \
|
||||
"${ghcr_beta_inventory}"
|
||||
printf -- '- Docker Hub v2beta inventory: `%s`\n' \
|
||||
"${dockerhub_beta_inventory}"
|
||||
printf '\nmanual recovery: inspect the exact inventories and job logs; '
|
||||
printf 'do not assume cross-system atomic rollback.\n'
|
||||
} >>"${GITHUB_STEP_SUMMARY}"
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
services:
|
||||
gpt-load:
|
||||
image: ghcr.io/tbphp/gpt-load:2
|
||||
image: ghcr.io/tbphp/gpt-load:v2beta
|
||||
# build:
|
||||
# context: .
|
||||
# dockerfile: Dockerfile
|
||||
|
||||
@@ -353,7 +353,7 @@ func TestComposeProjectsHaveIndependentNamesApplicationPortsAndVolumes(t *testin
|
||||
}
|
||||
}
|
||||
|
||||
func TestComposeResolvesNamedVolumeContainerPathsAndStableImage(t *testing.T) {
|
||||
func TestComposeResolvesNamedVolumeContainerPathsAndBetaChannelImage(t *testing.T) {
|
||||
t.Setenv("DATA_DIR", "/host/path/must-not-reach-container")
|
||||
t.Setenv("DATABASE_DSN", "/host/database/must-not-reach-container.db")
|
||||
|
||||
@@ -401,8 +401,8 @@ func TestComposeResolvesNamedVolumeContainerPathsAndStableImage(t *testing.T) {
|
||||
if !ok {
|
||||
t.Fatal("resolved Compose lacks gpt-load service")
|
||||
}
|
||||
if service.Image != "ghcr.io/tbphp/gpt-load:2" {
|
||||
t.Fatalf("resolved image = %q, want ghcr.io/tbphp/gpt-load:2", service.Image)
|
||||
if service.Image != "ghcr.io/tbphp/gpt-load:v2beta" {
|
||||
t.Fatalf("resolved image = %q, want ghcr.io/tbphp/gpt-load:v2beta", service.Image)
|
||||
}
|
||||
if service.Environment["DATA_DIR"] != "/app/data" {
|
||||
t.Fatalf("resolved DATA_DIR = %q, want /app/data", service.Environment["DATA_DIR"])
|
||||
|
||||
@@ -161,6 +161,41 @@ func TestWorkflowsPinExternalActionsAndHostedRunners(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestBranchWorkflowCancelsSupersededRuns(t *testing.T) {
|
||||
content := readRepositoryFile(t, ".github/workflows/ci.yml")
|
||||
concurrency := workflowTopLevelBlock(t, content, "concurrency")
|
||||
for _, required := range []string{
|
||||
`group: v2-ci-${{ github.event.pull_request.number || github.ref }}`,
|
||||
"cancel-in-progress: true",
|
||||
} {
|
||||
if !strings.Contains(concurrency, required) {
|
||||
t.Fatalf("branch CI concurrency does not contain %q:\n%s", required, concurrency)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestWorkflowsDisableCheckoutCredentialPersistence(t *testing.T) {
|
||||
for _, name := range []string{
|
||||
".github/workflows/ci.yml",
|
||||
".github/workflows/release.yml",
|
||||
} {
|
||||
content := readRepositoryFile(t, name)
|
||||
checkoutCount := strings.Count(content, "uses: "+checkoutActionRef)
|
||||
disabledCount := strings.Count(content, "persist-credentials: false")
|
||||
if checkoutCount == 0 {
|
||||
t.Fatalf("%s does not contain a checkout action", name)
|
||||
}
|
||||
if disabledCount != checkoutCount {
|
||||
t.Fatalf(
|
||||
"%s disables checkout credential persistence %d times, want %d",
|
||||
name,
|
||||
disabledCount,
|
||||
checkoutCount,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestGoFormattingScriptsFailClosed(t *testing.T) {
|
||||
for _, workflow := range []struct {
|
||||
name string
|
||||
@@ -253,7 +288,7 @@ func TestReleaseWorkflowUsesTagOnlyTriggerAndStrictSemverGuard(t *testing.T) {
|
||||
valid: true,
|
||||
wantOutput: []string{
|
||||
"version=v2.0.0", "prerelease=false", "image_exact=v2.0.0",
|
||||
"image_minor=2.0", "image_major=2",
|
||||
"image_minor=2.0", "image_major=2", "beta=false",
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -261,7 +296,15 @@ func TestReleaseWorkflowUsesTagOnlyTriggerAndStrictSemverGuard(t *testing.T) {
|
||||
valid: true,
|
||||
wantOutput: []string{
|
||||
"version=v2.0.0-rc.1", "prerelease=true", "image_exact=v2.0.0-rc.1",
|
||||
"image_minor=2.0", "image_major=2",
|
||||
"image_minor=2.0", "image_major=2", "beta=false",
|
||||
},
|
||||
},
|
||||
{
|
||||
tag: "v2.0.0-beta.1",
|
||||
valid: true,
|
||||
wantOutput: []string{
|
||||
"version=v2.0.0-beta.1", "prerelease=true", "image_exact=v2.0.0-beta.1",
|
||||
"image_minor=2.0", "image_major=2", "beta=true",
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -270,6 +313,7 @@ func TestReleaseWorkflowUsesTagOnlyTriggerAndStrictSemverGuard(t *testing.T) {
|
||||
wantOutput: []string{
|
||||
"version=v2.10.3-alpha-1.0", "prerelease=true",
|
||||
"image_exact=v2.10.3-alpha-1.0", "image_minor=2.10", "image_major=2",
|
||||
"beta=false",
|
||||
},
|
||||
},
|
||||
{tag: "v2.0.0.1", valid: false},
|
||||
@@ -873,6 +917,70 @@ func TestReleaseWorkflowUpdatesAliasesOnlyAfterExactVerification(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestReleaseWorkflowMaintainsBetaChannelAlias(t *testing.T) {
|
||||
content := readRepositoryFile(t, ".github/workflows/release.yml")
|
||||
validateJob := workflowJobBlock(t, content, "validate-tag")
|
||||
if !strings.Contains(validateJob, "beta: ${{ steps.tag.outputs.beta }}") {
|
||||
t.Fatalf("tag validation does not expose the beta channel decision:\n%s", validateJob)
|
||||
}
|
||||
|
||||
imageJob := workflowJobBlock(t, content, "publish-images")
|
||||
exactVerification := strings.Index(imageJob, "name: Verify exact published images")
|
||||
betaUpdate := strings.Index(imageJob, "name: Update beta channel alias")
|
||||
if exactVerification < 0 || betaUpdate < 0 || exactVerification >= betaUpdate {
|
||||
t.Fatalf(
|
||||
"beta alias update is not ordered after exact verification: exact=%d beta=%d\n%s",
|
||||
exactVerification,
|
||||
betaUpdate,
|
||||
imageJob,
|
||||
)
|
||||
}
|
||||
betaStep := workflowStepBlock(t, imageJob, "Update beta channel alias")
|
||||
for _, required := range []string{
|
||||
"needs.publication-preflight.outputs.write_mode == 'publish'",
|
||||
"needs.validate-tag.outputs.beta == 'true'",
|
||||
"docker buildx imagetools create",
|
||||
`exact="${repository}:${{ needs.validate-tag.outputs.image_exact }}"`,
|
||||
`--tag "${repository}:v2beta"`,
|
||||
} {
|
||||
if !strings.Contains(betaStep, required) {
|
||||
t.Fatalf("beta alias update does not contain %q:\n%s", required, betaStep)
|
||||
}
|
||||
}
|
||||
|
||||
verification := workflowStepBlock(
|
||||
t,
|
||||
workflowJobBlock(t, content, "post-publish-verify"),
|
||||
"Verify published image manifests",
|
||||
)
|
||||
for _, required := range []string{
|
||||
`needs.validate-tag.outputs.beta`,
|
||||
`"${repository}:v2beta"`,
|
||||
"beta_digest",
|
||||
`test "${beta_digest}" = "${exact_digest}"`,
|
||||
} {
|
||||
if !strings.Contains(verification, required) {
|
||||
t.Fatalf("beta alias verification does not contain %q:\n%s", required, verification)
|
||||
}
|
||||
}
|
||||
|
||||
reconciliation := workflowStepBlock(
|
||||
t,
|
||||
workflowJobBlock(t, content, "reconcile-publication"),
|
||||
"Summarize exact publication inventory and job results",
|
||||
)
|
||||
for _, required := range []string{
|
||||
"ghcr.io/tbphp/gpt-load:v2beta",
|
||||
"tbphp/gpt-load:v2beta",
|
||||
"GHCR v2beta inventory",
|
||||
"Docker Hub v2beta inventory",
|
||||
} {
|
||||
if !strings.Contains(reconciliation, required) {
|
||||
t.Fatalf("publication reconciliation does not contain %q:\n%s", required, reconciliation)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestReleaseWorkflowConsistentRerunIsVerifyOnly(t *testing.T) {
|
||||
content := readRepositoryFile(t, ".github/workflows/release.yml")
|
||||
for _, test := range []struct {
|
||||
@@ -887,7 +995,7 @@ func TestReleaseWorkflowConsistentRerunIsVerifyOnly(t *testing.T) {
|
||||
},
|
||||
{
|
||||
job: "publish-github",
|
||||
step: "Publish one GitHub Release",
|
||||
step: "Create or update GitHub Release draft",
|
||||
call: githubReleaseActionRef,
|
||||
},
|
||||
} {
|
||||
@@ -984,7 +1092,7 @@ func TestReleaseWorkflowReconciliationIsReadOnly(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestReleaseWorkflowPublishesStable2xTagsWithoutLatest(t *testing.T) {
|
||||
func TestReleaseWorkflowPreparesDraftsWithoutLatest(t *testing.T) {
|
||||
content := readRepositoryFile(t, ".github/workflows/release.yml")
|
||||
imageJob := workflowJobBlock(t, content, "publish-images")
|
||||
for _, required := range []string{
|
||||
@@ -1006,6 +1114,69 @@ func TestReleaseWorkflowPublishesStable2xTagsWithoutLatest(t *testing.T) {
|
||||
if strings.Contains(strings.ToLower(imageJob), "latest") {
|
||||
t.Fatalf("image publication job contains latest:\n%s", imageJob)
|
||||
}
|
||||
|
||||
githubJob := workflowJobBlock(t, content, "publish-github")
|
||||
draftStep := workflowStepBlock(t, githubJob, "Create or update GitHub Release draft")
|
||||
for _, required := range []string{
|
||||
"draft: true",
|
||||
"prerelease: ${{ needs.validate-tag.outputs.prerelease }}",
|
||||
"make_latest: false",
|
||||
"generate_release_notes: true",
|
||||
} {
|
||||
if !strings.Contains(draftStep, required) {
|
||||
t.Fatalf("GitHub Release draft does not contain %q:\n%s", required, draftStep)
|
||||
}
|
||||
}
|
||||
for _, forbidden := range []string{
|
||||
"draft: false",
|
||||
"Publish one GitHub Release",
|
||||
} {
|
||||
if strings.Contains(githubJob, forbidden) {
|
||||
t.Fatalf("GitHub Release draft job contains direct publication contract %q:\n%s", forbidden, githubJob)
|
||||
}
|
||||
}
|
||||
for _, verification := range []struct {
|
||||
job string
|
||||
step string
|
||||
}{
|
||||
{job: "publish-github", step: "Verify existing GitHub Release"},
|
||||
{job: "post-publish-verify", step: "Verify GitHub Release channel metadata"},
|
||||
} {
|
||||
step := workflowStepBlock(
|
||||
t,
|
||||
workflowJobBlock(t, content, verification.job),
|
||||
verification.step,
|
||||
)
|
||||
for _, required := range []string{
|
||||
`test "$(jq -r '.isDraft' <<<"${metadata}")" = "true"`,
|
||||
`test "$(jq -r '.isPrerelease' <<<"${metadata}")" = \`,
|
||||
`repos/${GITHUB_REPOSITORY}/releases/latest`,
|
||||
`test "${latest_tag}" != "${GITHUB_REF_NAME}"`,
|
||||
} {
|
||||
if !strings.Contains(step, required) {
|
||||
t.Fatalf("%s does not contain %q:\n%s", verification.step, required, step)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
inventoryStep := workflowStepBlock(
|
||||
t,
|
||||
workflowJobBlock(t, content, "publication-preflight"),
|
||||
"Inventory publication channels",
|
||||
)
|
||||
for _, required := range []string{
|
||||
"gh release view",
|
||||
"--json databaseId,targetCommitish,isDraft,isPrerelease,assets",
|
||||
`test "${github_draft}" = "true"`,
|
||||
`test "${github_prerelease}" = \`,
|
||||
} {
|
||||
if !strings.Contains(inventoryStep, required) {
|
||||
t.Fatalf("draft-aware publication inventory does not contain %q:\n%s", required, inventoryStep)
|
||||
}
|
||||
}
|
||||
if strings.Contains(inventoryStep, "/releases/tags/") {
|
||||
t.Fatalf("publication inventory uses an endpoint that cannot discover drafts:\n%s", inventoryStep)
|
||||
}
|
||||
}
|
||||
|
||||
func TestReleaseWorkflowIncludesCompleteS5Notes(t *testing.T) {
|
||||
@@ -1020,15 +1191,18 @@ func TestReleaseWorkflowIncludesCompleteS5Notes(t *testing.T) {
|
||||
"Stop cleanly",
|
||||
"`-wal`/`-shm`",
|
||||
"recovery set",
|
||||
"v2beta",
|
||||
"missing",
|
||||
"partial",
|
||||
"unpriced",
|
||||
"compatible upstream",
|
||||
"encryption key rotation",
|
||||
"2026-07-27",
|
||||
"https://developers.openai.com/api/docs/pricing",
|
||||
"https://platform.claude.com/docs/en/about-claude/pricing",
|
||||
"https://ai.google.dev/gemini-api/docs/pricing",
|
||||
"New 2.x deployments",
|
||||
"supported 2.x installations",
|
||||
"version-specific release notes",
|
||||
"embedded official catalog",
|
||||
"Models.dev",
|
||||
"explicit user overrides",
|
||||
"unified data/control dual-plane architecture",
|
||||
"Groups",
|
||||
"AccessKeys",
|
||||
@@ -1041,6 +1215,72 @@ func TestReleaseWorkflowIncludesCompleteS5Notes(t *testing.T) {
|
||||
if strings.Contains(releaseJob, "app.notion.com") {
|
||||
t.Fatalf("public release notes depend on a private Notion page:\n%s", releaseJob)
|
||||
}
|
||||
for _, stale := range []string{
|
||||
"Earlier pre-release 2.x databases are not supported",
|
||||
"Models.dev is the sole automatic price source",
|
||||
"Built-in prices were reviewed on",
|
||||
"GPT-Load 2.0.0 does not support encryption key rotation",
|
||||
} {
|
||||
if strings.Contains(releaseJob, stale) {
|
||||
t.Fatalf("release notes contain stale statement %q:\n%s", stale, releaseJob)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestCommunityTemplatesCaptureVersionCompatibilityAndSecretSafety(t *testing.T) {
|
||||
bug := readRepositoryFile(t, ".github/ISSUE_TEMPLATE/bug_report.md")
|
||||
for _, required := range []string{
|
||||
"当前主版本线的最新补丁版本",
|
||||
"GPT-Load 版本",
|
||||
"1.x 或 2.x",
|
||||
"部署方式",
|
||||
"操作系统与架构",
|
||||
"数据库",
|
||||
"客户端协议",
|
||||
"实际结果及脱敏日志",
|
||||
"AUTH_KEY",
|
||||
"ENCRYPTION_KEY",
|
||||
"AccessKey",
|
||||
} {
|
||||
if !strings.Contains(bug, required) {
|
||||
t.Fatalf("bug report template does not contain %q", required)
|
||||
}
|
||||
}
|
||||
|
||||
feature := readRepositoryFile(t, ".github/ISSUE_TEMPLATE/feature_request.md")
|
||||
for _, required := range []string{
|
||||
"当前主版本线的最新补丁版本",
|
||||
"目标版本线",
|
||||
"1.x 维护线或 2.x",
|
||||
"应用场景",
|
||||
"兼容性、数据与安全影响",
|
||||
"不要提交任何凭据或令牌",
|
||||
} {
|
||||
if !strings.Contains(feature, required) {
|
||||
t.Fatalf("feature request template does not contain %q", required)
|
||||
}
|
||||
}
|
||||
|
||||
pullRequest := readRepositoryFile(t, ".github/pull_request_template.md")
|
||||
for _, heading := range []string{
|
||||
"### 关联 Issue / Related Issue",
|
||||
"### 变更内容 / Change Content",
|
||||
"### 自查清单 / Checklist",
|
||||
} {
|
||||
if count := strings.Count(pullRequest, heading); count != 1 {
|
||||
t.Fatalf("pull request template contains heading %q %d times, want once", heading, count)
|
||||
}
|
||||
}
|
||||
for _, required := range []string{
|
||||
"make check",
|
||||
"未包含无关改动",
|
||||
"敏感信息",
|
||||
"兼容性或数据迁移",
|
||||
} {
|
||||
if !strings.Contains(pullRequest, required) {
|
||||
t.Fatalf("pull request template does not contain %q", required)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestReleaseWorkflowVerifiesDownloadedNativeChecksumsAndGeneratedKeys(t *testing.T) {
|
||||
@@ -1220,7 +1460,7 @@ func TestReleaseWorkflowPostPublishDownloadsExactAssetsAndRunsFiveNativeSmokes(t
|
||||
inventoryJob := workflowJobBlock(t, content, "post-publish-verify")
|
||||
for _, required := range []string{
|
||||
"gh release download",
|
||||
"public-release",
|
||||
"draft-release",
|
||||
"SHA256SUMS",
|
||||
"sha256sum --check",
|
||||
"asset_count",
|
||||
|
||||
Reference in new issue
Block a user