Files
codex-proxy-rs/release/publish
T
ZyyandCodex 11b64b3026 fix(update): publish verified channels on main
Read update candidates from the published channel table and advance it only after the release assets are public. Keep the tagged preparation commit separate from the publication commit.

Co-authored-by: Codex <noreply@openai.com>
2026-11-07 13:47:32 +08:00

192 lines
6.6 KiB
Bash
Executable File

#!/usr/bin/env bash
set -euo pipefail
APP_NAME="codex-proxy-rs"
VERSION_FILE="release/version.yaml"
NOTES_FILE="release/notes.md"
PLUGIN_DEPRECATIONS_FILE="backend/crates/gateway-plugin/runtime/plugin-api-deprecations.json"
PLUGIN_COMPATIBILITY_FILE="backend/crates/gateway-plugin/runtime/plugin-host-compatibility.json"
TAG_PREFIX="v"
SIGNATURE="Co-authored-by: Codex <noreply@openai.com>"
usage() {
cat <<USAGE
Usage:
release/publish <version>
release/publish --check <version>
Example:
release/publish 0.1.4
Before publishing, replace release/notes.md with the new version's notes,
starting with "# v<version>". Leave the notes uncommitted; this command commits
them together with release/version.yaml. Commit other tracked changes first.
Use --check to validate without changing files, Git refs, or remote state.
USAGE
}
die() {
echo "error: $*" >&2
exit 1
}
require_command() {
command -v "$1" >/dev/null 2>&1 || die "$1 is required"
}
read_version_key() {
local key="$1"
ruby -ryaml -e '
path, key = ARGV
data = YAML.load_file(path)
value = data[key] if data.is_a?(Hash)
unless value.is_a?(String) && !value.strip.empty?
warn "#{path}: #{key} must be a non-empty string"
exit 1
end
puts value.strip
' "$VERSION_FILE" "$key"
}
update_version_file() {
local version="$1"
ruby - "$VERSION_FILE" "$version" <<'RUBY'
path, version = ARGV
content = File.read(path)
unless content.match?(/^version:/)
warn "#{path}: missing version key"
exit 1
end
content = content.sub(/^version:\s*.*$/, %(version: #{version}))
File.write(path, content)
RUBY
}
if [[ "${1:-}" == "-h" || "${1:-}" == "--help" ]]; then
usage
exit 0
fi
check_only=false
if [[ "${1:-}" == "--check" ]]; then
check_only=true
# 只读检查也禁止 Git 的可选索引刷新
export GIT_OPTIONAL_LOCKS=0
shift
fi
[[ "$#" -eq 1 ]] || {
usage >&2
exit 2
}
require_command git
require_command gh
require_command ruby
gh auth status >/dev/null 2>&1 || die "gh authentication is required; run gh auth login first"
version="$1"
repo_root="$(git rev-parse --show-toplevel 2>/dev/null)" || die "not inside a git repository"
cd "$repo_root"
version="$(ruby .github/scripts/release-metadata.rb validate "$version")"
ruby .github/scripts/toolchains.rb >/dev/null
ruby -r './.github/scripts/release-metadata' -e 'ReleaseMetadata.platforms'
git ls-files --error-unmatch "$VERSION_FILE" >/dev/null 2>&1 || die "$VERSION_FILE is not tracked"
branch="$(git symbolic-ref --quiet --short HEAD)" || die "publishing from detached HEAD is not supported"
remote="$(git config --get "branch.$branch.remote" || true)"
merge_ref="$(git config --get "branch.$branch.merge" || true)"
[[ -n "$remote" && -n "$merge_ref" ]] || die "current branch has no upstream"
remote_branch="${merge_ref#refs/heads/}"
[[ "$remote_branch" != "$merge_ref" && -n "$remote_branch" ]] || die "unsupported upstream ref: $merge_ref"
remote_url="$(git remote get-url "$remote")" || die "failed to resolve remote URL for $remote"
repository="$(gh repo view "$remote_url" --json nameWithOwner --jq '.nameWithOwner' 2>/dev/null)" ||
die "failed to resolve GitHub repository for $remote_url"
if ! git diff --quiet --ignore-submodules -- . ":(exclude)$NOTES_FILE"; then
die "tracked files other than $NOTES_FILE have unstaged changes; commit or stash them first"
fi
if ! git diff --cached --quiet --ignore-submodules -- . ":(exclude)$NOTES_FILE"; then
die "index has staged changes outside $NOTES_FILE; commit or unstage them first"
fi
current_version="$(read_version_key version)"
current_version="${current_version#v}"
[[ "$version" != "$current_version" ]] || die "$VERSION_FILE is already $version"
tag="${TAG_PREFIX}${version}"
git ls-files --error-unmatch "$NOTES_FILE" >/dev/null 2>&1 || die "$NOTES_FILE is not tracked"
if git diff HEAD --quiet -- "$NOTES_FILE"; then
die "$NOTES_FILE must have uncommitted changes so notes and version are committed together"
fi
if git rev-parse --quiet --verify "refs/tags/$tag" >/dev/null; then
die "local tag already exists: $tag"
fi
if [[ "$check_only" == false ]]; then
git fetch "$remote"
fi
upstream="$remote/$remote_branch"
local_head="$(git rev-parse HEAD)"
remote_refs="$(git ls-remote --refs "$remote" "$merge_ref" "refs/tags/$tag")" || die "failed to read remote refs"
upstream_head="$(awk -v ref="$merge_ref" '$2 == ref { print $1 }' <<< "$remote_refs")"
[[ -n "$upstream_head" ]] || die "upstream branch does not exist: $upstream"
if awk -v ref="refs/tags/$tag" '$2 == ref { found = 1 } END { exit !found }' <<< "$remote_refs"; then
die "remote tag already exists: $tag"
fi
git cat-file -e "$upstream_head^{commit}" 2>/dev/null || die "upstream has unseen commits; run git fetch $remote first"
merge_base="$(git merge-base HEAD "$upstream_head")" || die "branch has no common history with $upstream"
if [[ "$local_head" != "$upstream_head" && "$merge_base" != "$upstream_head" ]]; then
if [[ "$merge_base" == "$local_head" ]]; then
die "branch is behind $upstream; pull or rebase first"
fi
die "branch has diverged from $upstream"
fi
if [[ "$check_only" == true ]]; then
# 预检调用与正式更新相同的纯函数,不写回兼容窗口
ruby -r './backend/crates/gateway-plugin/runtime/plugin-api-lifecycle' -e '
deprecations, compatibility, version = ARGV
PluginApiLifecycle.advance(JSON.parse(File.read(deprecations)), JSON.parse(File.read(compatibility)), version)
' "$PLUGIN_DEPRECATIONS_FILE" "$PLUGIN_COMPATIBILITY_FILE" "$version"
echo "Preflight passed: $APP_NAME $version on $branch -> $repository ($remote_branch)"
echo "No files, Git refs, or remote state changed. CI and release artifact validation still run after dispatch."
exit 0
fi
ruby backend/crates/gateway-plugin/runtime/plugin-api-lifecycle.rb "$PLUGIN_DEPRECATIONS_FILE" "$PLUGIN_COMPATIBILITY_FILE" "$version"
update_version_file "$version"
git add -- "$VERSION_FILE" "$NOTES_FILE" "$PLUGIN_DEPRECATIONS_FILE"
git commit \
-m "chore(release): prepare $tag" \
-m "$SIGNATURE"
git tag -a "$tag" -m "$tag"
git push --atomic "$remote" "HEAD:$remote_branch" "refs/tags/$tag"
# 从长期发行分支触发,使 Actions 缓存可跨版本复用
if ! gh workflow run release.yml \
--repo "$repository" \
--ref "$remote_branch" \
--raw-field "tag=$tag"; then
echo "error: commit and tag were pushed, but the Release workflow was not dispatched" >&2
echo "retry: gh workflow run release.yml --repo $repository --ref $remote_branch --raw-field tag=$tag" >&2
exit 1
fi
cat <<DONE
Queued release for $APP_NAME $version
Commit: $(git rev-parse --short HEAD)
Tag: $tag
Release workflow dispatched from: $remote_branch
DONE