mirror of
https://github.com/openai/codex.git
synced 2026-04-24 14:45:27 +00:00
Add release_gen.sh
This commit is contained in:
1
.gitignore
vendored
1
.gitignore
vendored
@@ -81,3 +81,4 @@ CHANGELOG.ignore.md
|
||||
# nix related
|
||||
.direnv
|
||||
.envrc
|
||||
scripts/releases/
|
||||
|
||||
321
scripts/release_gen.sh
Executable file
321
scripts/release_gen.sh
Executable file
@@ -0,0 +1,321 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
# Simple stderr logger
|
||||
header() { echo "==> $*" >&2; }
|
||||
|
||||
# Generate summarized release notes using Codex CLI based on PR dump.
|
||||
# Can also generate just the dump via --dump-only.
|
||||
|
||||
usage() {
|
||||
cat <<'USAGE'
|
||||
Usage: scripts/release_gen.sh [--dump-only] [-q|--quiet] [owner/repo] <from_tag> <to_tag> [version]
|
||||
|
||||
Examples:
|
||||
scripts/release_gen.sh openai/codex v0.23.0 v0.24.0
|
||||
scripts/release_gen.sh v0.23.0 v0.24.0 # auto-detect repo from git remote
|
||||
scripts/release_gen.sh v0.23.0 v0.24.0 0.24.0 # auto-detect with explicit version
|
||||
scripts/release_gen.sh --dump-only v0.23.0 v0.24.0 # only generate releases/release_dump_<ver>.txt
|
||||
scripts/release_gen.sh -q v0.23.0 v0.24.0 # quiet Codex call with progress dots
|
||||
|
||||
Notes:
|
||||
- Requires: gh and jq for dump generation; codex CLI for note generation.
|
||||
- If release_dump_<ver>.txt does not exist, it will be created automatically.
|
||||
- Then runs codex to generate <ver>.txt based on the dump (unless --dump-only).
|
||||
- If you omit tags, the script lists the last 20 releases for the repo.
|
||||
USAGE
|
||||
}
|
||||
|
||||
# Parse flags (currently: --dump-only, --quiet)
|
||||
DUMP_ONLY=0
|
||||
QUIET=0
|
||||
ARGS=()
|
||||
for arg in "$@"; do
|
||||
case "$arg" in
|
||||
--dump-only)
|
||||
DUMP_ONLY=1
|
||||
;;
|
||||
-q|--quiet)
|
||||
QUIET=1
|
||||
;;
|
||||
-h|--help)
|
||||
usage
|
||||
exit 0
|
||||
;;
|
||||
*)
|
||||
ARGS+=("$arg")
|
||||
;;
|
||||
esac
|
||||
done
|
||||
# Reset positional args safely under set -u, even if ARGS is empty
|
||||
if ((${#ARGS[@]})); then
|
||||
set -- "${ARGS[@]}"
|
||||
else
|
||||
set --
|
||||
fi
|
||||
|
||||
if [[ ${1:-} == "-h" || ${1:-} == "--help" ]]; then
|
||||
usage
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Resolve repo: allow optional first arg; otherwise detect from git remote
|
||||
detect_repo() {
|
||||
local remote
|
||||
remote=$(git remote get-url origin 2>/dev/null || git remote get-url upstream 2>/dev/null || true)
|
||||
if [[ -z "$remote" ]]; then
|
||||
echo ""; return 1
|
||||
fi
|
||||
# Normalize and extract owner/repo from SSH or HTTPS/HTTP URL
|
||||
local path="$remote"
|
||||
# Strip protocols and user@
|
||||
path="${path#git@}"
|
||||
path="${path#ssh://}"
|
||||
path="${path#https://}"
|
||||
path="${path#http://}"
|
||||
path="${path#*@}"
|
||||
# If contains github.com:, take after ':'; else after 'github.com/' if present
|
||||
if [[ "$path" == *":"* ]]; then
|
||||
path="${path#*:}"
|
||||
fi
|
||||
if [[ "$path" == *github.com/* ]]; then
|
||||
path="${path#*github.com/}"
|
||||
fi
|
||||
# Trim leading slashes
|
||||
path="${path#/}"
|
||||
# Drop trailing .git
|
||||
path="${path%.git}"
|
||||
# Ensure only owner/repo
|
||||
echo "$path" | awk -F/ '{print $1"/"$2}'
|
||||
}
|
||||
|
||||
if [[ ${1:-} == */* ]]; then
|
||||
REPO="$1"; shift
|
||||
else
|
||||
REPO="$(detect_repo || true)"
|
||||
if [[ -z "$REPO" ]]; then
|
||||
echo "Error: failed to auto-detect repository from git remote. Provide [owner/repo] explicitly." >&2
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
|
||||
# Show a recent releases list if tags are missing
|
||||
show_recent_releases_and_exit() {
|
||||
local repo="$1"
|
||||
echo "" >&2
|
||||
echo "Please pass a source/target release." >&2
|
||||
echo "" >&2
|
||||
echo "e.g.: ./scripts/release_gen.sh rust-v0.23.0 rust-v0.24.0" >&2
|
||||
echo "" >&2
|
||||
header "Recent releases for $repo:"
|
||||
echo "" >&2
|
||||
local list
|
||||
list=$(gh release list --repo "$repo" --limit 20 2>/dev/null || true)
|
||||
if [[ -z "$list" ]]; then
|
||||
echo "Error: unable to fetch releases for $repo" >&2
|
||||
exit 1
|
||||
fi
|
||||
# Print only the tag (first column) as bullets to stderr
|
||||
printf '%s\n' "$list" | awk '{print "- " $1}' >&2
|
||||
exit 1
|
||||
}
|
||||
|
||||
if [[ $# -lt 2 ]]; then
|
||||
show_recent_releases_and_exit "$REPO"
|
||||
fi
|
||||
|
||||
FROM_TAG="$1"
|
||||
TO_TAG="$2"
|
||||
VER="${3:-$TO_TAG}"
|
||||
VER="${VER#v}"
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
||||
RELEASES_DIR="$SCRIPT_DIR/releases"
|
||||
DUMP_FILE="$RELEASES_DIR/release_dump_$VER.txt"
|
||||
GEN_FILE="$RELEASES_DIR/$VER.txt"
|
||||
|
||||
# Ensure releases directory exists (under scripts/)
|
||||
mkdir -p "$RELEASES_DIR"
|
||||
|
||||
abspath() {
|
||||
local p="$1"
|
||||
if command -v realpath >/dev/null 2>&1; then
|
||||
realpath "$p"
|
||||
else
|
||||
python3 -c 'import os,sys;print(os.path.abspath(sys.argv[1]))' "$p" 2>/dev/null || echo "$(pwd)/$p"
|
||||
fi
|
||||
}
|
||||
|
||||
# ========== Dump generation logic (ported from release_dump_util.sh) ==========
|
||||
header() { echo "==> $*" >&2; }
|
||||
|
||||
# Get an ISO 8601 datetime for a tag. Prefer release publish date; fallback to tag/commit date.
|
||||
get_tag_datetime_iso() {
|
||||
local repo="$1" tag="$2"
|
||||
# Try release by tag
|
||||
local ts
|
||||
ts=$(gh release view "$tag" --repo "$repo" --json publishedAt --jq '.publishedAt' 2>/dev/null || true)
|
||||
if [[ -n "$ts" && "$ts" != "null" ]]; then
|
||||
echo "$ts"; return 0
|
||||
fi
|
||||
# Fallback: tag ref -> (annotated tag ->) commit -> date
|
||||
local ref obj_type obj_url commit_sha commit
|
||||
ref=$(gh api "repos/$repo/git/ref/tags/$tag")
|
||||
obj_type=$(jq -r '.object.type' <<<"$ref")
|
||||
obj_url=$(jq -r '.object.url' <<<"$ref")
|
||||
if [[ "$obj_type" == "tag" ]]; then
|
||||
local tag_obj
|
||||
tag_obj=$(gh api "$obj_url")
|
||||
commit_sha=$(jq -r '.object.sha' <<<"$tag_obj")
|
||||
else
|
||||
commit_sha=$(jq -r '.object.sha' <<<"$ref")
|
||||
fi
|
||||
commit=$(gh api "repos/$repo/commits/$commit_sha")
|
||||
jq -r '.commit.committer.date' <<<"$commit"
|
||||
}
|
||||
|
||||
collect_prs_within_range() {
|
||||
local repo="$1" from_iso="$2" to_iso="$3"
|
||||
gh pr list --repo "$repo" --state merged --limit 1000 \
|
||||
--json number,title,mergedAt,author,body | \
|
||||
jq -c --arg from "$from_iso" --arg to "$to_iso" \
|
||||
'[ .[]
|
||||
| select(.mergedAt != null and .mergedAt >= $from and .mergedAt <= $to)
|
||||
| {
|
||||
number: .number,
|
||||
title: .title,
|
||||
merged_at: .mergedAt,
|
||||
author: (.author.login // "-"),
|
||||
body: (.body // "")
|
||||
}
|
||||
] | sort_by(.merged_at) | .[]'
|
||||
}
|
||||
|
||||
format_related_issues() {
|
||||
# shellcheck disable=SC2016
|
||||
sed 's/\r//g' | \
|
||||
grep -Eio '(close|closed|closes|fix|fixed|fixes|resolve|resolved|resolves)[[:space:]:]+([[:alnum:]_.-]+\/[[:alnum:]_.-]+)?#[0-9]+' || true | \
|
||||
grep -Eo '#[0-9]+' | tr -d '#' | sort -n -u | sed 's/^/#/' | paste -sd ', ' -
|
||||
}
|
||||
|
||||
generate_dump() {
|
||||
local repo="$1" from_tag="$2" to_tag="$3" out_file="$4"
|
||||
command -v gh >/dev/null 2>&1 || { echo "Error: gh (GitHub CLI) is required" >&2; exit 1; }
|
||||
command -v jq >/dev/null 2>&1 || { echo "Error: jq is required" >&2; exit 1; }
|
||||
|
||||
header "Resolving tag dates ($from_tag -> $to_tag)"
|
||||
local from_iso to_iso
|
||||
from_iso=$(get_tag_datetime_iso "$repo" "$from_tag")
|
||||
to_iso=$(get_tag_datetime_iso "$repo" "$to_tag")
|
||||
if [[ -z "$from_iso" || -z "$to_iso" ]]; then
|
||||
echo "Error: failed to resolve tag dates. from=$from_tag ($from_iso) to=$to_tag ($to_iso)" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
header "Collecting merged PRs via gh pr list"
|
||||
local tmpdir sorted
|
||||
tmpdir=$(mktemp -d)
|
||||
sorted="$tmpdir/prs.sorted.ndjson"
|
||||
collect_prs_within_range "$repo" "$from_iso" "$to_iso" > "$sorted"
|
||||
|
||||
local count
|
||||
count=$(wc -l < "$sorted" | tr -d ' ')
|
||||
|
||||
header "Writing $out_file (Total PRs: $count)"
|
||||
{
|
||||
echo "Repository: $repo"
|
||||
echo "Range: $from_tag ($from_iso) -> $to_tag ($to_iso)"
|
||||
echo "Generated: $(date -u +%Y-%m-%dT%H:%M:%SZ)"
|
||||
echo "Total PRs: $count"
|
||||
echo ""
|
||||
} > "$out_file"
|
||||
|
||||
if [[ "$count" -eq 0 ]]; then
|
||||
return 0
|
||||
fi
|
||||
|
||||
while IFS= read -r line; do
|
||||
local title number merged_at author body issues
|
||||
title=$(jq -r '.title' <<<"$line")
|
||||
number=$(jq -r '.number' <<<"$line")
|
||||
merged_at=$(jq -r '.merged_at' <<<"$line")
|
||||
author=$(jq -r '.author' <<<"$line")
|
||||
body=$(jq -r '.body' <<<"$line")
|
||||
|
||||
issues=$(printf '%s' "$body" | format_related_issues || true)
|
||||
[[ -z "$issues" ]] && issues="-"
|
||||
|
||||
{
|
||||
echo "PR #$number: $title"
|
||||
echo "Merged: $merged_at | Author: $author"
|
||||
echo "Related issues: $issues"
|
||||
echo ""
|
||||
# Skip verbose descriptions for Dependabot PRs
|
||||
if [[ "$author" != "app/dependabot" && "$author" != "dependabot[bot]" && ! "$author" =~ [Dd]ependabot ]]; then
|
||||
echo "Description:"
|
||||
# Limit descriptions to 2000 characters; add ellipses if truncated
|
||||
local max=2000
|
||||
if (( ${#body} > max )); then
|
||||
printf '%s\n' "${body:0:max}..."
|
||||
else
|
||||
printf '%s\n' "$body"
|
||||
fi
|
||||
echo ""
|
||||
fi
|
||||
echo "-----"
|
||||
echo ""
|
||||
} >> "$out_file"
|
||||
done < "$sorted"
|
||||
|
||||
header "Done -> $out_file"
|
||||
}
|
||||
|
||||
# ========== Orchestrate dump + optional codex generation ==========
|
||||
|
||||
# Create dump if missing
|
||||
if [[ ! -f "$DUMP_FILE" ]]; then
|
||||
header "Dump not found: $DUMP_FILE. Generating..."
|
||||
generate_dump "$REPO" "$FROM_TAG" "$TO_TAG" "$DUMP_FILE"
|
||||
else
|
||||
header "Using existing dump: $DUMP_FILE"
|
||||
fi
|
||||
|
||||
if (( DUMP_ONLY )); then
|
||||
# Dump-only mode: no stdout output
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# Now run codex to generate notes
|
||||
command -v codex >/dev/null 2>&1 || { echo "Error: codex CLI is required for generation. Use --dump-only to skip." >&2; exit 1; }
|
||||
|
||||
DUMP_PATH="$(abspath "$DUMP_FILE")"
|
||||
PROMPT="`cat ${DUMP_PATH}`\n\n---\n\nPlease generate a summarized release note based on the list of PRs above. Then, write a file called $GEN_FILE with your suggested release notes. It should follow this structure (+ the style/tone/brevity in this example):\n\n\"- Highlights:\n - New commands and controls: support /mcp in TUI (#2430) and a slash command /approvals to control approvals (#2474).\n - Reasoning controls: change reasoning effort and model at runtime (#2435) /model; add “minimal” effort for GPT‑5 models (#2326).\n - Auth improvements: show login options when not signed in with ChatGPT (#2440) and auto‑refresh ChatGPT auth token (#2484).\n - UI/UX polish: Ghostty Ctrl‑b/Ctrl‑f fallback (#2427), Ctrl+H as backspace (#2412), cursor position tweak after tab completion (#2442), color/accessibility updates (#2401, #2421).\n - Distribution/infra: zip archived binaries added to releases (#2438) and DotSlash entry for Windows x86_64 (#2361); upgraded to Rust 1.89 (#2465, #2467).\n- Full list of merged PRs:\n\n - #2352 tui: skip identical consecutive entries in local composer history\n - #2355 fix: introduce codex-protocol crate\n...\"\n\nMake sure you limit the highlights to, at most, 5 bullet points."
|
||||
|
||||
header "Calling codex to generate $GEN_FILE"
|
||||
if (( QUIET )); then
|
||||
# Quiet mode: run Codex silently and show progress dots
|
||||
(
|
||||
set +x 2>/dev/null || true
|
||||
codex exec --yolo "$PROMPT" >/dev/null 2>&1
|
||||
) &
|
||||
CODEX_PID=$!
|
||||
while :; do
|
||||
kill -0 "$CODEX_PID" 2>/dev/null || break
|
||||
printf "." >&2
|
||||
sleep 1
|
||||
done
|
||||
wait "$CODEX_PID" || true
|
||||
CODEX_STATUS=$?
|
||||
echo "" >&2
|
||||
else
|
||||
# Normal mode: stream Codex output to stderr as before
|
||||
codex exec --yolo "$PROMPT" 1>&2
|
||||
fi
|
||||
|
||||
if [[ -f "$GEN_FILE" ]]; then
|
||||
# On success, output only the generated release notes to stdout
|
||||
cat "$GEN_FILE"
|
||||
else
|
||||
echo "Warning: $GEN_FILE not created. Check codex output." >&2
|
||||
exit 1
|
||||
fi
|
||||
Reference in New Issue
Block a user