mirror of
https://github.com/zyycn/codex-proxy-rs.git
synced 2026-11-06 08:06:19 +00:00
Allow staged or unstaged release notes and commit their final contents with the version update before tagging. Continue rejecting unrelated tracked changes and require notes to remain uncommitted. Update the release skill, skip duplicate push CI for release metadata alone, and check the publish script syntax in CI. Validated publication behavior in temporary Git repositories, Bash syntax, and Actionlint. Co-authored-by: Codex <noreply@openai.com>
173 lines
5.2 KiB
Bash
Executable File
173 lines
5.2 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"
|
|
TAG_PREFIX="v"
|
|
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 <noreply@openai.com>"
|
|
|
|
usage() {
|
|
cat <<USAGE
|
|
Usage:
|
|
release/publish <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.
|
|
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 gh
|
|
require_command ruby
|
|
|
|
gh auth status >/dev/null 2>&1 || die "gh authentication is required; run gh auth login first"
|
|
|
|
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"
|
|
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
|
|
ruby - "$NOTES_FILE" "$tag" <<'RUBY'
|
|
path, tag = ARGV
|
|
lines = File.readlines(path)
|
|
unless lines.first&.strip == "# #{tag}" && !lines.drop(1).join.strip.empty?
|
|
warn "#{path}: replace the contents with notes for #{tag}, starting with '# #{tag}'"
|
|
exit 1
|
|
end
|
|
RUBY
|
|
|
|
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" "$NOTES_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"
|
|
|
|
# Dispatch from the long-lived release branch instead of the tag so GitHub
|
|
# Actions caches remain reusable across releases.
|
|
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
|
|
Published $APP_NAME $version
|
|
Commit: $(git rev-parse --short HEAD)
|
|
Tag: $tag
|
|
Release workflow dispatched from: $remote_branch
|
|
DONE
|