Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 23 additions & 0 deletions .github/codex/prompts/release-review.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
# Release readiness review

You are Codex running in CI. Produce a release readiness report for this repository.

Steps:
1. Determine the latest release tag (use local tags only):
- `git tag -l 'v*' --sort=-v:refname | head -n1`
2. Set TARGET to the current commit SHA: `git rev-parse HEAD`.
3. Collect diff context for BASE_TAG...TARGET:
- `git diff --stat BASE_TAG...TARGET`
- `git diff --dirstat=files,0 BASE_TAG...TARGET`
- `git diff --name-status BASE_TAG...TARGET`
- `git log --oneline --reverse BASE_TAG..TARGET`
4. Review `.codex/skills/final-release-review/references/review-checklist.md` and analyze the diff.

Output:
- Write the report in the exact format used by `$final-release-review` (see `.codex/skills/final-release-review/SKILL.md`).
- Use the compare URL: `https://github.com/${GITHUB_REPOSITORY}/compare/BASE_TAG...TARGET`.
- Include clear ship/block call and risk levels.
- If no risks are found, include "No material risks identified".

Constraints:
- Output only the report (no code fences, no extra commentary).
84 changes: 84 additions & 0 deletions .github/workflows/release-pr-update.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
name: Update release PR on main updates

on:
push:
branches:
- main

concurrency:
group: release-pr-update
cancel-in-progress: true

permissions:
contents: write
pull-requests: write

jobs:
update-release-pr:
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v6
with:
fetch-depth: 0
- name: Fetch tags
run: git fetch origin --tags --prune
- name: Configure git
run: |
git config user.name "github-actions[bot]"
git config user.email "github-actions[bot]@users.noreply.github.com"
- name: Find release PR
id: find
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
set -euo pipefail
base_branch="main"
prs_json="$(gh pr list \
--base "$base_branch" \
--state open \
--search "head:release/v" \
--limit 200 \
--json number,headRefName,isCrossRepository,headRepositoryOwner)"
count="$(echo "$prs_json" | jq '[.[] | select(.isCrossRepository == false) | select(.headRefName|startswith("release/v"))] | length')"
if [ "$count" -eq 0 ]; then
echo "found=false" >> "$GITHUB_OUTPUT"
exit 0
fi
if [ "$count" -gt 1 ]; then
echo "Multiple release PRs found; expected a single release PR." >&2
exit 1
fi
number="$(echo "$prs_json" | jq -r '.[] | select(.isCrossRepository == false) | select(.headRefName|startswith("release/v")) | .number')"
branch="$(echo "$prs_json" | jq -r '.[] | select(.isCrossRepository == false) | select(.headRefName|startswith("release/v")) | .headRefName')"
echo "found=true" >> "$GITHUB_OUTPUT"
echo "number=$number" >> "$GITHUB_OUTPUT"
echo "branch=$branch" >> "$GITHUB_OUTPUT"
- name: Rebase release branch
if: steps.find.outputs.found == 'true'
env:
RELEASE_BRANCH: ${{ steps.find.outputs.branch }}
run: |
set -euo pipefail
git fetch origin main "$RELEASE_BRANCH"
git checkout -B "$RELEASE_BRANCH" "origin/$RELEASE_BRANCH"
git rebase origin/main
- name: Run Codex release review
if: steps.find.outputs.found == 'true'
uses: openai/codex-action@v1
with:
openai-api-key: ${{ secrets.PROD_OPENAI_API_KEY }}
prompt-file: .github/codex/prompts/release-review.md
output-file: release-review.md
safety-strategy: drop-sudo
sandbox: read-only
- name: Update PR body and push
if: steps.find.outputs.found == 'true'
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
PR_NUMBER: ${{ steps.find.outputs.number }}
RELEASE_BRANCH: ${{ steps.find.outputs.branch }}
run: |
set -euo pipefail
git push --force-with-lease origin "$RELEASE_BRANCH"
gh pr edit "$PR_NUMBER" --body-file release-review.md
126 changes: 126 additions & 0 deletions .github/workflows/release-pr.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
name: Create release PR

on:
workflow_dispatch:
inputs:
version:
description: "Version to release (e.g., 0.6.6)"
required: true

permissions:
contents: write
pull-requests: write

jobs:
release-pr:
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v6
with:
fetch-depth: 0
ref: main
- name: Setup uv
uses: astral-sh/setup-uv@v5
with:
enable-cache: true
- name: Fetch tags
run: git fetch origin --tags --prune
- name: Ensure release branch does not exist
env:
RELEASE_VERSION: ${{ inputs.version }}
run: |
branch="release/v${RELEASE_VERSION}"
if git ls-remote --exit-code --heads origin "$branch" >/dev/null 2>&1; then
echo "Branch $branch already exists on origin." >&2
exit 1
fi
- name: Update version
env:
RELEASE_VERSION: ${{ inputs.version }}
run: |
python - <<'PY'
import os
import pathlib
import re
import sys

version = os.environ["RELEASE_VERSION"]
if version.startswith("v"):
print("Version must not start with 'v' (use x.y.z...).", file=sys.stderr)
sys.exit(1)
if ".." in version:
print("Version contains consecutive dots (use x.y.z...).", file=sys.stderr)
sys.exit(1)
if not re.match(r"^\d+\.\d+(\.\d+)*([a-zA-Z0-9\.-]+)?$", version):
print(
"Version must be semver-like (e.g., 0.6.6, 0.6.6-rc1, 0.6.6.dev1).",
file=sys.stderr,
)
sys.exit(1)
path = pathlib.Path("pyproject.toml")
text = path.read_text()
updated, count = re.subn(
r'(?m)^version\s*=\s*"[^\"]+"',
f'version = "{version}"',
text,
)
if count != 1:
print("Expected to update exactly one version line.", file=sys.stderr)
sys.exit(1)
if updated == text:
print("Version already set; no changes made.", file=sys.stderr)
sys.exit(1)
path.write_text(updated)
PY
- name: Sync dependencies
run: make sync
- name: Configure git
run: |
git config user.name "github-actions[bot]"
git config user.email "github-actions[bot]@users.noreply.github.com"
- name: Create release branch and commit
env:
RELEASE_VERSION: ${{ inputs.version }}
run: |
branch="release/v${RELEASE_VERSION}"
git checkout -b "$branch"
git add pyproject.toml uv.lock
if git diff --cached --quiet; then
echo "No changes to commit." >&2
exit 1
fi
git commit -m "Bump version to ${RELEASE_VERSION}"
git push --set-upstream origin "$branch"
- name: Run Codex release review
uses: openai/codex-action@v1
with:
openai-api-key: ${{ secrets.PROD_OPENAI_API_KEY }}
prompt-file: .github/codex/prompts/release-review.md
output-file: release-review.md
safety-strategy: drop-sudo
sandbox: read-only
- name: Build PR body
run: |
python - <<'PY'
import pathlib

report = pathlib.Path("release-review.md").read_text()
pathlib.Path("pr-body.md").write_text(report)
PY
- name: Create or update PR
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
RELEASE_VERSION: ${{ inputs.version }}
run: |
head_branch="release/v${RELEASE_VERSION}"
pr_number="$(gh pr list --head "$head_branch" --base "main" --json number --jq '.[0].number // empty')"
if [ -z "$pr_number" ]; then
gh pr create \
--title "Release ${RELEASE_VERSION}" \
--body-file pr-body.md \
--base "main" \
--head "$head_branch"
else
gh pr edit "$pr_number" --title "Release ${RELEASE_VERSION}" --body-file pr-body.md
fi
83 changes: 83 additions & 0 deletions .github/workflows/release-tag.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
name: Tag release on merge

on:
pull_request:
types:
- closed
branches:
- main

permissions:
contents: write

jobs:
tag-release:
if: >-
github.event.pull_request.merged == true &&
startsWith(github.event.pull_request.head.ref, 'release/v')
runs-on: ubuntu-latest
steps:
- name: Validate merge commit
env:
MERGE_SHA: ${{ github.event.pull_request.merge_commit_sha }}
run: |
if [ -z "$MERGE_SHA" ]; then
echo "merge_commit_sha is empty; refusing to tag to avoid tagging the wrong commit." >&2
exit 1
fi
- name: Checkout merge commit
uses: actions/checkout@v6
with:
fetch-depth: 0
ref: ${{ github.event.pull_request.merge_commit_sha }}
- name: Setup Python
uses: actions/setup-python@v5
with:
python-version: "3.11"
- name: Configure git
run: |
git config user.name "github-actions[bot]"
git config user.email "github-actions[bot]@users.noreply.github.com"
- name: Fetch tags
run: git fetch origin --tags --prune
- name: Resolve version
id: version
env:
HEAD_REF: ${{ github.event.pull_request.head.ref }}
run: |
python - <<'PY'
import os
import pathlib
import sys
import tomllib

path = pathlib.Path("pyproject.toml")
data = tomllib.loads(path.read_text())
version = data.get("project", {}).get("version")
if not version:
print("Missing project.version in pyproject.toml.", file=sys.stderr)
sys.exit(1)

head_ref = os.environ.get("HEAD_REF", "")
if head_ref.startswith("release/v"):
expected = head_ref[len("release/v") :]
if expected != version:
print(
f"Version mismatch: branch {expected} vs pyproject {version}.",
file=sys.stderr,
)
sys.exit(1)

output_path = pathlib.Path(os.environ["GITHUB_OUTPUT"])
output_path.write_text(f"version={version}\n")
PY
- name: Create tag
env:
VERSION: ${{ steps.version.outputs.version }}
run: |
if git tag -l "v${VERSION}" | grep -q "v${VERSION}"; then
echo "Tag v${VERSION} already exists; skipping."
exit 0
fi
git tag -a "v${VERSION}" -m "Release v${VERSION}"
git push origin "v${VERSION}"