mirror of
https://github.com/zyycn/codex-proxy-rs.git
synced 2026-11-06 16:06:08 +00:00
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]*)(-(alpha|beta|rc|exp)\.[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 X.Y.Z or X.Y.Z-{alpha,beta,rc,exp}.N (N >= 1), optionally with +build metadata"
|
|
|
|
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): $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
|