diff --git a/docs/architecture.md b/docs/architecture.md index cc9494bda..90be03148 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -103,6 +103,8 @@ flowchart LR - `release/version.yaml`:产品版本和 tag 前缀。 - `release/platforms.yaml`:Release asset 构建平台、归档格式和 Docker 平台。 +日常发布只需要执行 `release/publish 0.1.4`。该命令会更新 `release/version.yaml`、生成带署名的版本提交、创建 `v0.1.4` tag,并 push 分支和 tag 触发发布 workflow。 + 发布 workflow 会校验触发 tag 必须等于 `tagPrefix + version`。发布构建时通过 `CPR_VERSION`、`CPR_GIT_SHA`、`CPR_BUILD_TIME` 和 `CPR_BUILD_TYPE=release` 注入编译期元数据。 `backend/build/build.rs` 负责把这些值写入二进制。没有 `CPR_VERSION` 时,版本回落到 `release/version.yaml`,最后才回落到 Cargo 的 `CARGO_PKG_VERSION`。 diff --git a/release/publish b/release/publish new file mode 100755 index 000000000..1e18d4f34 --- /dev/null +++ b/release/publish @@ -0,0 +1,136 @@ +#!/usr/bin/env bash +set -euo pipefail + +APP_NAME="codex-proxy-rs" +VERSION_FILE="release/version.yaml" +SEMVER='^(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)(-[0-9A-Za-z.-]+)?(\+[0-9A-Za-z.-]+)?$' +SIGNATURE="Co-authored-by: Codex " + +usage() { + cat < + +Example: + release/publish 0.1.4 +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 + +[[ "$#" -eq 1 ]] || { + usage >&2 + exit 2 +} + +require_command git +require_command ruby + +version="${1#v}" +[[ "$version" =~ $SEMVER ]] || die "version must be semver, for example 0.1.4" + +repo_root="$(git rev-parse --show-toplevel 2>/dev/null)" || die "not inside a git repository" +cd "$repo_root" + +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" + +if ! git diff --quiet --ignore-submodules --; then + die "tracked files have unstaged changes; commit or stash them first" +fi +if ! git diff --cached --quiet --ignore-submodules --; then + die "index has staged changes; 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_prefix="$(read_version_key tagPrefix)" +tag="${tag_prefix}${version}" + +if git rev-parse --quiet --verify "refs/tags/$tag" >/dev/null; then + die "local tag already exists: $tag" +fi + +git fetch "$remote" + +upstream="$remote/$remote_branch" +local_head="$(git rev-parse HEAD)" +upstream_head="$(git rev-parse "$upstream")" +merge_base="$(git merge-base HEAD "$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 git ls-remote --exit-code --tags "$remote" "refs/tags/$tag" >/dev/null 2>&1; then + die "remote tag already exists: $tag" +fi + +update_version_file "$version" + +git add "$VERSION_FILE" +git commit \ + -m "chore: bump version to $version" \ + -m "$SIGNATURE" + +git tag -a "$tag" -m "$tag" +git push --atomic "$remote" "HEAD:$remote_branch" "refs/tags/$tag" + +cat <`。该命令会更新 `release/version.yaml`、提交版本变更、创建 tag,并 push 分支和 tag 触发 workflow。 - 产品版本来源是 `release/version.yaml`;workflow 会校验触发 tag 等于 `tagPrefix + version`,并通过 `CPR_VERSION` build arg 写入编译期元数据。 - 发布平台来源是 `release/platforms.yaml`;新增/删除 Release asset 平台时优先改这个文件。 - 运行时版本接口展示编译期元数据:`CPR_VERSION`、`CPR_GIT_SHA`、`CPR_BUILD_TIME`。