From 8256b6a389edca3f38e47731f380c67804aab0e0 Mon Sep 17 00:00:00 2001 From: Justin Beckwith Date: Mon, 28 Sep 2026 09:20:04 -0700 Subject: [PATCH 1/5] ci: configure release-please with GitHub Actions authentication --- .github/RELEASING.md | 34 +++++++++++++ .github/scripts/detect-changes.sh | 2 +- .github/workflows/release-please.yml | 32 +++++++++++++ .release-please-manifest.json | 3 ++ release-please-config.json | 33 +++++++++++++ tests/test_change_detection.py | 3 ++ tests/test_release_please.py | 71 ++++++++++++++++++++++++++++ 7 files changed, 177 insertions(+), 1 deletion(-) create mode 100644 .github/workflows/release-please.yml create mode 100644 .release-please-manifest.json create mode 100644 release-please-config.json create mode 100644 tests/test_release_please.py diff --git a/.github/RELEASING.md b/.github/RELEASING.md index 5a776af11b..802a225958 100644 --- a/.github/RELEASING.md +++ b/.github/RELEASING.md @@ -2,6 +2,39 @@ Release tags are created manually by authorized maintainers. Merging a release pull request does not create a tag. +## Prepare the release pull request + +`.github/workflows/release-please.yml` maintains a draft release pull request after pushes to `main`. Maintainers can also run the workflow manually on `main`. Release Please uses conventional commit messages to propose the next version and release notes, following the `openai-python` configuration. Review the proposed version, especially for breaking changes before 1.0. + +The bot updates `pyproject.toml`, the editable `openai-agents` version in `uv.lock`, the source-checkout fallback in `src/agents/version.py`, `.release-please-manifest.json`, and `CHANGELOG.md`. Installed packages continue to read their version from package metadata. The configuration selects the project's lockfile entry by package name, so dependency versions remain unchanged and lockfile regeneration does not remove a required marker comment. The TOML selector uses `name.value` because the pinned Release Please updater wraps parsed values with source-position metadata; verify that selector when upgrading the action. + +Release Please does not regenerate the public API snapshot. Before marking the release PR ready for review: + +1. Check out the bot's release PR branch in a clean checkout and bring it up to date with `main`. Review the complete diff, including the proposed version and changelog. +2. Set `RELEASE_VERSION` to the proposed `project.version` and regenerate the snapshot with the existing commands: + + ```bash + RELEASE_VERSION="" + make sync + make update-released-api-contract VERSION="$RELEASE_VERSION" + make check-released-api-contract VERSION="$RELEASE_VERSION" + ``` + + The generator records the checked-out source commit and freezes the API surface for the proposed version. Review the generated `tests/fixtures/released_api_contract.json` diff, then commit and push it to the release PR branch. Do not merely replace its version string: new exports and signatures must be captured too. If the bot or another maintainer updates the candidate's source or version, regenerate and review the snapshot again before merging. +3. Run the required verification and wait for CI on the final candidate. The initial bot PR may fail the snapshot-version test until step 2 is complete. Mark the PR ready for review only after the snapshot and metadata agree. + +The standalone `$release-candidate-prep` skill still prepares a manual three-file candidate; it does not complete a bot PR. For this automated PR route, follow the steps above. If releasing through the manual route, also synchronize `.release-please-manifest.json` and the changelog in the reviewed release PR so the next automated proposal starts from the version actually released. + +### Temporary GitHub Actions authentication + +The workflow uses the repository's `GITHUB_TOKEN`, appearing as `github-actions[bot]`, with Contents, Issues, and Pull requests write permissions only on the release PR job. It runs only for `openai/openai-agents-python` on `main` and does not check out or execute release PR code. + +An administrator must allow GitHub Actions to create pull requests under Settings > Actions > General. Existing organization rules may additionally restrict bot branch writes; verify that the job can open and update a release PR without weakening repository protections. Under GitHub's current behavior, pull-request workflows created by `GITHUB_TOKEN` require a user with write access to select **Approve workflows to run**. Approve checks when prompted and require all checks on the final PR revision before merging. See [GitHub's workflow-trigger documentation](https://docs.github.com/en/actions/how-tos/write-workflows/choose-when-workflows-run/trigger-a-workflow). + +`skip-github-release: true` is intentional: a GitHub Release created with `GITHUB_TOKEN` would not trigger `publish.yml`. An authorized maintainer must create the tag and publish the GitHub Release using the procedure below. + +For a later switch to the `openai-sdks` App, provision the `OPENAI_SDKS_APP_CLIENT_ID` variable and `OPENAI_SDKS_APP_PRIVATE_KEY` secret in a protected, main-only `release` environment. Switch authentication in a separately reviewed workflow change after installation and credentials are verified. Keep the existing `pypi` environment and trusted-publishing configuration. + ## Required release review Before merging a release pull request, obtain at least one approving review from a code owner listed in `.github/CODEOWNERS`, resolve review conversations, and wait for all required checks to pass. The author cannot approve their own pull request. Changes after approval require a fresh code-owner review; the most recent reviewable push must also be approved by someone other than its pusher. @@ -39,3 +72,4 @@ The shared release policy requires code-owner approval before merging the releas If the tag already exists, stop and investigate. Do not overwrite, delete, or move an existing release tag. 3. Publish a GitHub Release using that existing tag and the reviewed release notes. This starts `.github/workflows/publish.yml`. 4. After the build succeeds, a designated reviewer confirms the release tag and commit and approves the `pypi` deployment. When Prevent self-review is enabled, another designated reviewer must approve. +5. After publishing the matching GitHub Release, remove `autorelease: pending` from the merged release PR and add `autorelease: tagged`. Release Please's automatic release step normally manages these labels; in this PR-only setup, a pending merged release can block the next proposal. Do not mark a release tagged before its matching tag and GitHub Release exist. Retry a failed package publication through the existing publishing workflow; do not move the tag or merge another release PR to retry the same version. diff --git a/.github/scripts/detect-changes.sh b/.github/scripts/detect-changes.sh index 479aa34a2d..8d56f47fb3 100755 --- a/.github/scripts/detect-changes.sh +++ b/.github/scripts/detect-changes.sh @@ -76,7 +76,7 @@ fi case "$mode" in code) - pattern='^(src/|tests/|integration_tests/|examples/|docs/scripts/|\.agents/skills/(code-change-verification|examples-auto-run|examples-run-analysis|integration-tests)/|\.github/scripts/|\.github/workflows/(tests|docs|publish|repo-skills)\.yml$|pyproject\.toml$|uv\.lock$|Makefile$|pyrightconfig\.json$)' + pattern='^(src/|tests/|integration_tests/|examples/|docs/scripts/|\.agents/skills/(code-change-verification|examples-auto-run|examples-run-analysis|integration-tests)/|\.github/scripts/|\.github/workflows/(tests|docs|publish|repo-skills|release-please)\.yml$|release-please-config\.json$|\.release-please-manifest\.json$|pyproject\.toml$|uv\.lock$|Makefile$|pyrightconfig\.json$)' ;; docs) pattern='^(docs/|mkdocs\.yml$|uv\.lock$|\.github/workflows/docs\.yml$)' diff --git a/.github/workflows/release-please.yml b/.github/workflows/release-please.yml new file mode 100644 index 0000000000..390d67ea35 --- /dev/null +++ b/.github/workflows/release-please.yml @@ -0,0 +1,32 @@ +name: Release Please + +on: + push: + branches: + - main + workflow_dispatch: + +permissions: {} + +concurrency: + group: release-please-main + cancel-in-progress: false + +jobs: + release-pr: + if: github.repository == 'openai/openai-agents-python' && github.ref == 'refs/heads/main' + runs-on: ubuntu-latest + permissions: + contents: write + issues: write + pull-requests: write + steps: + - name: Update the release pull request + uses: googleapis/release-please-action@45996ed1f6d02564a971a2fa1b5860e934307cf7 # v5.0.0 + with: + token: ${{ secrets.GITHUB_TOKEN }} + target-branch: main + config-file: release-please-config.json + manifest-file: .release-please-manifest.json + # Keep manual publication: GITHUB_TOKEN release events do not start publish.yml. + skip-github-release: true diff --git a/.release-please-manifest.json b/.release-please-manifest.json new file mode 100644 index 0000000000..4801c06d44 --- /dev/null +++ b/.release-please-manifest.json @@ -0,0 +1,3 @@ +{ + ".": "0.22.3" +} diff --git a/release-please-config.json b/release-please-config.json new file mode 100644 index 0000000000..efd0a3ac11 --- /dev/null +++ b/release-please-config.json @@ -0,0 +1,33 @@ +{ + "$schema": "https://raw.githubusercontent.com/googleapis/release-please/main/schemas/config.json", + "packages": { + ".": {} + }, + "release-type": "python", + "include-v-in-tag": true, + "include-component-in-tag": false, + "bump-minor-pre-major": true, + "bump-patch-for-minor-pre-major": false, + "draft-pull-request": true, + "pull-request-title-pattern": "release: ${version}", + "pull-request-header": "Automated release PR. Before marking ready for review, regenerate tests/fixtures/released_api_contract.json, review the release notes, and run the required checks. See .github/RELEASING.md. Tags and GitHub Releases are published manually.", + "extra-files": [ + { + "type": "toml", + "path": "uv.lock", + "jsonpath": "$.package[?(@.name.value=='openai-agents')].version" + } + ], + "changelog-sections": [ + { "type": "feat", "section": "Features" }, + { "type": "fix", "section": "Bug Fixes" }, + { "type": "perf", "section": "Performance Improvements" }, + { "type": "revert", "section": "Reverts" }, + { "type": "chore", "section": "Chores" }, + { "type": "docs", "section": "Documentation" }, + { "type": "refactor", "section": "Refactors" }, + { "type": "build", "section": "Build System" }, + { "type": "test", "section": "Tests", "hidden": true }, + { "type": "ci", "section": "Continuous Integration", "hidden": true } + ] +} diff --git a/tests/test_change_detection.py b/tests/test_change_detection.py index c1f8d5adb6..234c544490 100644 --- a/tests/test_change_detection.py +++ b/tests/test_change_detection.py @@ -114,6 +114,9 @@ def _detect( (".github/workflows/docs.yml", True, True, False), (".github/workflows/publish.yml", True, False, False), (".github/workflows/repo-skills.yml", True, False, False), + (".github/workflows/release-please.yml", True, False, False), + ("release-please-config.json", True, False, False), + (".release-please-manifest.json", True, False, False), ("pyproject.toml", True, False, False), ("uv.lock", True, True, False), ("Makefile", True, False, False), diff --git a/tests/test_release_please.py b/tests/test_release_please.py new file mode 100644 index 0000000000..3f940e8a3d --- /dev/null +++ b/tests/test_release_please.py @@ -0,0 +1,71 @@ +from __future__ import annotations + +import json +import re +import sys +from pathlib import Path + +import yaml + +if sys.version_info >= (3, 11): + import tomllib +else: + import tomli as tomllib + +ROOT = Path(__file__).resolve().parents[1] + + +def test_release_metadata_tracks_only_the_editable_project() -> None: + project = tomllib.loads((ROOT / "pyproject.toml").read_text())["project"] + manifest = json.loads((ROOT / ".release-please-manifest.json").read_text()) + config = json.loads((ROOT / "release-please-config.json").read_text()) + lock_text = (ROOT / "uv.lock").read_text() + packages = tomllib.loads(lock_text)["package"] + editable = next(package for package in packages if package["name"] == project["name"]) + + assert manifest == {".": project["version"]} + assert editable["version"] == project["version"] + assert editable["source"] == {"editable": "."} + assert config["release-type"] == "python" + assert config["packages"] == {".": {}} + assert config["extra-files"] == [ + { + "type": "toml", + "path": "uv.lock", + "jsonpath": "$.package[?(@.name.value=='openai-agents')].version", + } + ] + assert config["include-v-in-tag"] is True + assert config["include-component-in-tag"] is False + assert config["draft-pull-request"] is True + + +def test_release_bot_does_not_execute_pr_code_or_publish() -> None: + workflow = yaml.load( + (ROOT / ".github/workflows/release-please.yml").read_text(), Loader=yaml.BaseLoader + ) + assert workflow["on"] == {"push": {"branches": ["main"]}, "workflow_dispatch": ""} + assert workflow["permissions"] == {} + assert workflow["concurrency"]["cancel-in-progress"] == "false" + assert set(workflow["jobs"]) == {"release-pr"} + job = workflow["jobs"]["release-pr"] + assert job["if"] == ( + "github.repository == 'openai/openai-agents-python' && github.ref == 'refs/heads/main'" + ) + assert job["permissions"] == { + "contents": "write", + "issues": "write", + "pull-requests": "write", + } + # No checkout, PR-controlled shell code, or package build in the privileged job. + assert len(job["steps"]) == 1 + step = job["steps"][0] + assert re.fullmatch(r"googleapis/release-please-action@[0-9a-f]{40}", step["uses"]) + assert "run" not in step + assert step["with"] == { + "token": "${{ secrets.GITHUB_TOKEN }}", + "target-branch": "main", + "config-file": "release-please-config.json", + "manifest-file": ".release-please-manifest.json", + "skip-github-release": "true", + } From 94746f0ad3f6303e925aba866eb006bea806c974 Mon Sep 17 00:00:00 2001 From: Justin Beckwith Date: Mon, 28 Sep 2026 09:35:05 -0700 Subject: [PATCH 2/5] fix: synchronize manual release candidates with release-please --- .agents/skills/final-release-review/SKILL.md | 2 +- .../references/review-checklist.md | 2 +- .../skills/release-candidate-prep/SKILL.md | 22 +++++++++---------- .../release-candidate-prep/scripts/prepare.py | 15 ++++++++++--- .../scripts/test_prepare.py | 16 ++++++++++++++ .github/RELEASING.md | 6 ++--- AGENTS.md | 2 +- 7 files changed, 45 insertions(+), 20 deletions(-) diff --git a/.agents/skills/final-release-review/SKILL.md b/.agents/skills/final-release-review/SKILL.md index 3ded694904..9340a15029 100644 --- a/.agents/skills/final-release-review/SKILL.md +++ b/.agents/skills/final-release-review/SKILL.md @@ -105,7 +105,7 @@ In final-candidate mode, when the caller provides a dedicated checkout or worktr - Resolve and record the checkout root, current branch, `HEAD`, and clean status before auditing. Do not switch to a different checkout that happens to share the same Git object database. - Require `TARGET=HEAD` to resolve to the checked-out commit. Treat detached HEAD, a mismatched release branch, uncommitted release-owned files, or unrelated changed paths as candidate inconsistency. -- Read `pyproject.toml`, `uv.lock`, and `tests/fixtures/released_api_contract.json` from that checkout. Verify the intended version, editable `openai-agents` lock entry, contract baseline, and contract `baseline_commit` against the release branch and commit parent. +- Read `pyproject.toml`, `uv.lock`, `.release-please-manifest.json`, and `tests/fixtures/released_api_contract.json` from that checkout. Verify the intended version, editable `openai-agents` lock entry, root Release Please manifest version, contract baseline, and contract `baseline_commit` against the release branch and commit parent. - Inspect the exact commit diff and confirm that the materialized release commit owns only its expected release manifest when the invoking workflow defines one. - Keep the checkout path as local evidence for the caller, but do not put local paths into copy-ready release text. diff --git a/.agents/skills/final-release-review/references/review-checklist.md b/.agents/skills/final-release-review/references/review-checklist.md index 6946b297ad..b9a64d4365 100644 --- a/.agents/skills/final-release-review/references/review-checklist.md +++ b/.agents/skills/final-release-review/references/review-checklist.md @@ -19,7 +19,7 @@ Compare the diff with the released BASE contract. Use `minor` as the minimum for - a breaking change to a non-beta public API, protocol, configuration, environment, or durable serialized boundary; - a major user-facing feature addition that warrants a minor release under repository policy. -Use `patch` otherwise. For a final candidate, verify the intended version against the branch name, `pyproject.toml`, `uv.lock`, and built package metadata when relevant. For planning mode, do not interpret unchanged version metadata as a declared patch candidate. +Use `patch` otherwise. For a final candidate, verify the intended version against the branch name, `pyproject.toml`, `uv.lock`, `.release-please-manifest.json`, and built package metadata when relevant. For planning mode, do not interpret unchanged version metadata as a declared patch candidate. Capture: diff --git a/.agents/skills/release-candidate-prep/SKILL.md b/.agents/skills/release-candidate-prep/SKILL.md index 05ddcaeda6..4b1c04663a 100644 --- a/.agents/skills/release-candidate-prep/SKILL.md +++ b/.agents/skills/release-candidate-prep/SKILL.md @@ -9,10 +9,10 @@ Use this skill only when the user explicitly invokes `$release-candidate-prep` a ## Non-negotiable boundaries -- Treat explicit invocation as authorization to fetch `origin/main`, create one dedicated detached release worktree, run branch-free release-readiness gates there, create or replace the local `release/v` in that worktree only after those gates pass, update the three release-owned files, and create one local commit. If the branch already exists locally or remotely, the required final local state is still exact current `origin/main` plus only the new release commit; an existing local branch may be replaced only when it is not checked out in another worktree. +- Treat explicit invocation as authorization to fetch `origin/main`, create one dedicated detached release worktree, run branch-free release-readiness gates there, create or replace the local `release/v` in that worktree only after those gates pass, update the four release-owned files, and create one local commit. If the branch already exists locally or remotely, the required final local state is still exact current `origin/main` plus only the new release commit; an existing local branch may be replaced only when it is not checked out in another worktree. - Keep the user's source checkout on its existing clean `main` commit. Do not fast-forward it, switch its branch, or materialize release files there. Leave the dedicated release worktree in place for green handoff, blocked review, or recoverable failure. - Never push, open or edit a pull request, add labels or milestones, create a release, or otherwise mutate GitHub. Never run `gh`. -- Own exactly `pyproject.toml`, `uv.lock`, and `tests/fixtures/released_api_contract.json`. Runtime, documentation, workflow, or other repository changes must land on `main` before release preparation. +- Own exactly `pyproject.toml`, `uv.lock`, `.release-please-manifest.json`, and `tests/fixtures/released_api_contract.json`. Runtime, documentation, workflow, or other repository changes must land on `main` before release preparation. - Do not stash, delete, overwrite or remove an existing worktree, or work around unrelated local changes. Fail before branch creation when the initial checkout is dirty or is not on `main`, the dedicated worktree is not clean and detached at refreshed `origin/main`, an existing local release branch is checked out in another worktree, the prospective packaged-contract gate fails after the allowed dependency-bootstrap recovery, the planning review blocks, or `origin/main` advances after those gates run. - Treat `$final-release-review` as the controlling release checker, not only as a report generator. Its planning gate must be green before branch creation, and its final-candidate gate must inspect the materialized worktree and be green before PR-ready handoff. Any candidate content, commit, or base change invalidates the previous green result. - Remove inherited `OPENAI_API_KEY` from every child command. Release preparation does not require a live OpenAI API request. @@ -89,10 +89,10 @@ The helper must complete all of these operations or fail with an actionable erro 1. Repeat the source-root, clean `main`, version, registered-worktree, detached-HEAD, and release-branch replaceability checks. 2. Refresh `origin/main` again without moving the source checkout. 3. Require refreshed `origin/main` and `` HEAD to equal ``. If `origin/main` advanced, retain the old detached worktree and rerun preflight plus both readiness gates in a new exact-base worktree. -4. Keep the worktree detached while updating the single project version declaration in `pyproject.toml`. +4. Keep the worktree detached while updating the single project version declaration in `pyproject.toml` and the root version in `.release-please-manifest.json`. 5. Run `make sync` with `UV_DEFAULT_INDEX=https://pypi.org/simple`. 6. Run `make update-released-api-contract VERSION=` and then `make check-released-api-contract VERSION=`. -7. Require exactly the three release-owned paths to be modified in ``, leave them unstaged and uncommitted, and confirm that the source checkout remains unchanged. +7. Require exactly the four release-owned paths to be modified in ``, leave them unstaged and uncommitted, and confirm that the source checkout remains unchanged. 8. Only after those candidate checks pass, create or reset the local `release/v` inside `` to exact `` while preserving the validated unstaged manifest. Do not retain commits or content from an older local or remote candidate. This delayed replacement must leave an existing local branch unchanged when candidate generation fails. If the helper fails after branch creation, preserve its local branch, dedicated worktree, and working-tree evidence. Report the failing command and state rather than guessing whether a partial run is safe to resume. Never remove the worktree as automatic cleanup. @@ -104,21 +104,21 @@ Run the remaining commands from ``. Inspect all release-owned ```bash git status --short git diff --check -git diff -- pyproject.toml uv.lock tests/fixtures/released_api_contract.json +git diff -- pyproject.toml uv.lock .release-please-manifest.json tests/fixtures/released_api_contract.json ``` Confirm all of the following: -- `pyproject.toml` and the editable `openai-agents` entry in `uv.lock` declare the requested version. +- `pyproject.toml`, the editable `openai-agents` entry in `uv.lock`, and the root entry in `.release-please-manifest.json` declare the requested version. - The API contract baseline is `v` and its `baseline_commit` is the exact `origin/main` source commit on which the release branch is based. - The generated contract preserves the previous release and freezes intended new exports and signatures. - Any intended `public_properties`, `canonical_imports`, or `public_modules` policy additions have been reviewed explicitly; the updater deliberately does not infer them. -- No path outside the three-file release manifest is changed, staged, or untracked. +- No path outside the four-file release manifest is changed, staged, or untracked. Stage only the manifest and create exactly one local commit: ```bash -git add pyproject.toml uv.lock tests/fixtures/released_api_contract.json +git add pyproject.toml uv.lock .release-please-manifest.json tests/fixtures/released_api_contract.json git commit -m "release: " ``` @@ -126,7 +126,7 @@ Do not amend unrelated content into the commit. ## 6. Run the final-candidate release review -Invoke `$final-release-review` from `` in final-candidate mode with the release commit as `TARGET=HEAD`. This invocation is a release checker: it must inspect the complete candidate diff and the actual checked-out `release/v` contents, including `pyproject.toml`, the editable `openai-agents` entry in `uv.lock`, and `tests/fixtures/released_api_contract.json`. The branch, package metadata, lockfile, contract baseline, contract `baseline_commit`, and intended version must agree. +Invoke `$final-release-review` from `` in final-candidate mode with the release commit as `TARGET=HEAD`. This invocation is a release checker: it must inspect the complete candidate diff and the actual checked-out `release/v` contents, including `pyproject.toml`, the editable `openai-agents` entry in `uv.lock`, `.release-please-manifest.json`, and `tests/fixtures/released_api_contract.json`. The branch, package metadata, lockfile, Release Please manifest, contract baseline, contract `baseline_commit`, and intended version must agree. If the review is blocked, stop. Return its unblock checklist, retain the local branch, commit, and worktree for follow-up, and do not present the candidate as PR-ready. A report body does not authorize continuation when the release call is blocked. After any fix, regenerate the API contract when the public surface may have changed, restore a single release commit, and rerun the complete final-candidate review. @@ -134,7 +134,7 @@ The earlier planning review proves that the source commit was ready before branc ## 7. Recheck main freshness -After a green review, fetch `origin main` again without credentials from `` and compare it with the release commit's parent. If they differ, the candidate is stale. First verify that the branch is clean, has exactly one local commit, and that the commit changes only the three-file release manifest. Rebase that commit onto the new `origin/main` so Git detects any conflicting release metadata. After a clean rebase, move the local release branch back to `origin/main` with a mixed reset, which preserves the rebased release tree as unstaged task-owned changes. Restore all three release-owned files (`pyproject.toml`, `uv.lock`, and `tests/fixtures/released_api_contract.json`) from `origin/main`, run `make sync`, and require the worktree to be clean at the new base. Run `make check-prospective-released-api-contract` only in that internally consistent base state, where the installed project version and frozen contract baseline agree. Then update `pyproject.toml` to ``, run `make sync`, run `make update-released-api-contract VERSION=` and `make check-released-api-contract VERSION=`, review the exact manifest again, and recreate the single `release: ` commit. The base and candidate content changed, so the previous green check is invalid: rerun `$final-release-review` from the worktree and require a new green release call. Repeat until the reviewed local branch is exactly one commit ahead of current `origin/main` and that commit changes only the three-file release manifest. +After a green review, fetch `origin main` again without credentials from `` and compare it with the release commit's parent. If they differ, the candidate is stale. First verify that the branch is clean, has exactly one local commit, and that the commit changes only the four-file release manifest. Rebase that commit onto the new `origin/main` so Git detects any conflicting release metadata. After a clean rebase, move the local release branch back to `origin/main` with a mixed reset, which preserves the rebased release tree as unstaged task-owned changes. Restore all four release-owned files (`pyproject.toml`, `uv.lock`, `.release-please-manifest.json`, and `tests/fixtures/released_api_contract.json`) from `origin/main`, run `make sync`, and require the worktree to be clean at the new base. Run `make check-prospective-released-api-contract` only in that internally consistent base state, where the installed project version and frozen contract baseline agree. Then update `pyproject.toml` and the root entry in `.release-please-manifest.json` to ``, run `make sync`, run `make update-released-api-contract VERSION=` and `make check-released-api-contract VERSION=`, review the exact manifest again, and recreate the single `release: ` commit. The base and candidate content changed, so the previous green check is invalid: rerun `$final-release-review` from the worktree and require a new green release call. Repeat until the reviewed local branch is exactly one commit ahead of current `origin/main` and that commit changes only the four-file release manifest. If replay conflicts or another path changes, stop with recoverable evidence. Do not force a resolution that expands the release commit beyond its manifest. @@ -164,7 +164,7 @@ Release Apply the repository's GitHub paste-readiness rules to the report. Use native `#123` references for this repository and `owner/repo#123` for another repository. Keep the required compare URL. Do not include local paths, Codex citations, operational diagnostics, or app directives inside the copy-ready description. -Also report the dedicated worktree path, local branch, commit SHA, parent `origin/main` commit, and the exact three-file manifest outside the copy-ready block. State explicitly that the source checkout was left unchanged, nothing was pushed, and no pull request was created. Leave the worktree in place for the user's handoff. +Also report the dedicated worktree path, local branch, commit SHA, parent `origin/main` commit, and the exact four-file manifest outside the copy-ready block. State explicitly that the source checkout was left unchanged, nothing was pushed, and no pull request was created. Leave the worktree in place for the user's handoff. If `release/v` already exists on `origin`, inspect its exact current commit with credential-free `git ls-remote --heads origin release/v` immediately before handoff and record it as ``. State explicitly that the local branch has replaced the old candidate and now contains exact current `origin/main` plus only the new `release: ` commit. Because this skill never mutates GitHub, provide the user with the exact `git push --force-with-lease=refs/heads/release/v: origin release/v` command to replace the remote branch themselves; never run it. A normal push or an unspecified lease is insufficient for this replacement case. If the remote branch changes after inspection, the explicit lease must reject the push instead of overwriting unseen work. diff --git a/.agents/skills/release-candidate-prep/scripts/prepare.py b/.agents/skills/release-candidate-prep/scripts/prepare.py index 29ef65eeea..fb6d014164 100755 --- a/.agents/skills/release-candidate-prep/scripts/prepare.py +++ b/.agents/skills/release-candidate-prep/scripts/prepare.py @@ -26,6 +26,7 @@ PROJECT_VERSION_PATTERN = re.compile(r'(?m)^version\s*=\s*"[^"]+"') RELEASE_PATHS = frozenset( { + ".release-please-manifest.json", "pyproject.toml", "tests/fixtures/released_api_contract.json", "uv.lock", @@ -168,12 +169,17 @@ def replace_project_version_text(text: str, version: str) -> str: def replace_project_version(repo: Path, version: str) -> None: - """Update pyproject.toml while preserving all unrelated text.""" + """Update package and release manifest versions without changing dependencies.""" path = repo / "pyproject.toml" text = path.read_text(encoding="utf-8") path.write_text(replace_project_version_text(text, version), encoding="utf-8") + manifest_path = repo / ".release-please-manifest.json" + manifest = json.loads(manifest_path.read_text(encoding="utf-8")) + manifest["."] = version + manifest_path.write_text(json.dumps(manifest, indent=2) + "\n", encoding="utf-8") + def _current_branch(repo: Path) -> str: result = git(repo, "symbolic-ref", "--quiet", "--short", "HEAD", check=False) @@ -357,6 +363,9 @@ def _validate_prepared_files(repo: Path, version: str, base_commit: str) -> tupl raise ReleasePreparationError("pyproject.toml does not contain the requested version.") if _locked_project_version(repo) != version: raise ReleasePreparationError("uv.lock does not contain the requested project version.") + manifest = json.loads((repo / ".release-please-manifest.json").read_text(encoding="utf-8")) + if manifest.get(".") != version: + raise ReleasePreparationError("The Release Please manifest does not match the version.") contract = json.loads( (repo / "tests/fixtures/released_api_contract.json").read_text(encoding="utf-8") @@ -434,7 +443,7 @@ def materialize( expected_source_head: str, worktree: Path, ) -> PreparedCandidate: - """Create the three-file candidate in the reviewed isolated worktree.""" + """Create the four-file candidate in the reviewed isolated worktree.""" expected_base = validate_commit(expected_base) expected_source_head = validate_commit(expected_source_head) @@ -592,7 +601,7 @@ def main() -> int: print("Changed paths:") for path in candidate.changed_paths: print(f"- {path}") - print("Review the diff before staging the three release-owned files.") + print("Review the diff before staging the four release-owned files.") return 0 diff --git a/.agents/skills/release-candidate-prep/scripts/test_prepare.py b/.agents/skills/release-candidate-prep/scripts/test_prepare.py index 192aa7dd39..e7842d8adc 100755 --- a/.agents/skills/release-candidate-prep/scripts/test_prepare.py +++ b/.agents/skills/release-candidate-prep/scripts/test_prepare.py @@ -57,6 +57,9 @@ def advance_origin(self) -> str: def _write_fixture_files(self) -> None: (self.repo / "tests/fixtures").mkdir(parents=True) + (self.repo / ".release-please-manifest.json").write_text( + json.dumps({".": "0.19.4"}, indent=2) + "\n", encoding="utf-8" + ) (self.repo / "pyproject.toml").write_text( '[project]\nname = "openai-agents"\nversion = "0.19.4"\n', encoding="utf-8", @@ -249,6 +252,19 @@ def test_materialize_creates_branch_and_exact_uncommitted_manifest(self) -> None self.assertEqual(candidate.base_commit, fixture.base_commit) self.assertEqual(candidate.branch, "release/v0.20.0") self.assertEqual(set(candidate.changed_paths), prepare.RELEASE_PATHS) + self.assertEqual( + set(candidate.changed_paths), + { + ".release-please-manifest.json", + "pyproject.toml", + "uv.lock", + "tests/fixtures/released_api_contract.json", + }, + ) + self.assertEqual( + json.loads((candidate.worktree / ".release-please-manifest.json").read_text()), + {".": "0.20.0"}, + ) self.assertEqual( run(release_input.worktree, "git", "branch", "--show-current").stdout.strip(), "release/v0.20.0", diff --git a/.github/RELEASING.md b/.github/RELEASING.md index 802a225958..3efb184b0a 100644 --- a/.github/RELEASING.md +++ b/.github/RELEASING.md @@ -20,16 +20,16 @@ Release Please does not regenerate the public API snapshot. Before marking the r make check-released-api-contract VERSION="$RELEASE_VERSION" ``` - The generator records the checked-out source commit and freezes the API surface for the proposed version. Review the generated `tests/fixtures/released_api_contract.json` diff, then commit and push it to the release PR branch. Do not merely replace its version string: new exports and signatures must be captured too. If the bot or another maintainer updates the candidate's source or version, regenerate and review the snapshot again before merging. + The generator records the checked-out source commit and freezes the API surface for the proposed version. Review the generated `tests/fixtures/released_api_contract.json` diff, then commit and push it to the release PR branch using the maintainer’s own GitHub credentials. This push triggers the repository’s normal pull-request CI for the completed candidate. Do not merely replace its version string: new exports and signatures must be captured too. If the bot or another maintainer updates the candidate's source or version, regenerate and review the snapshot again before merging. 3. Run the required verification and wait for CI on the final candidate. The initial bot PR may fail the snapshot-version test until step 2 is complete. Mark the PR ready for review only after the snapshot and metadata agree. -The standalone `$release-candidate-prep` skill still prepares a manual three-file candidate; it does not complete a bot PR. For this automated PR route, follow the steps above. If releasing through the manual route, also synchronize `.release-please-manifest.json` and the changelog in the reviewed release PR so the next automated proposal starts from the version actually released. +The standalone `$release-candidate-prep` skill prepares a manual four-file candidate: `pyproject.toml`, `uv.lock`, `.release-please-manifest.json`, and `tests/fixtures/released_api_contract.json`. The helper synchronizes the manifest with the requested version so the next automated proposal starts from the version actually released. The manual route uses maintainer-written GitHub Release notes and does not generate a changelog entry. It does not complete a bot PR; for that route, follow the steps above. ### Temporary GitHub Actions authentication The workflow uses the repository's `GITHUB_TOKEN`, appearing as `github-actions[bot]`, with Contents, Issues, and Pull requests write permissions only on the release PR job. It runs only for `openai/openai-agents-python` on `main` and does not check out or execute release PR code. -An administrator must allow GitHub Actions to create pull requests under Settings > Actions > General. Existing organization rules may additionally restrict bot branch writes; verify that the job can open and update a release PR without weakening repository protections. Under GitHub's current behavior, pull-request workflows created by `GITHUB_TOKEN` require a user with write access to select **Approve workflows to run**. Approve checks when prompted and require all checks on the final PR revision before merging. See [GitHub's workflow-trigger documentation](https://docs.github.com/en/actions/how-tos/write-workflows/choose-when-workflows-run/trigger-a-workflow). +An administrator must allow GitHub Actions to create pull requests under Settings > Actions > General. Existing organization rules may additionally restrict bot branch writes; verify that the job can open and update a release PR without weakening repository protections. Under GitHub's current behavior, pull-request workflows created by `GITHUB_TOKEN` require a user with write access to select **Approve workflows to run**. Approve checks when prompted. If no approval prompt or checks appear, the maintainer-authenticated snapshot push in step 2 starts normal PR checks; a maintainer can also close and reopen the PR to trigger them for the current revision. Require all checks on the final PR revision before merging. See [GitHub's workflow-trigger documentation](https://docs.github.com/en/actions/how-tos/write-workflows/choose-when-workflows-run/trigger-a-workflow). `skip-github-release: true` is intentional: a GitHub Release created with `GITHUB_TOKEN` would not trigger `publish.yml`. An authorized maintainer must create the tag and publish the GitHub Release using the procedure below. diff --git a/AGENTS.md b/AGENTS.md index 377f61f367..7e39ed87d0 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -31,7 +31,7 @@ Repository skills are stored under `.agents/skills/`. References below authorize - **`$code-change-verification`:** Run the final SDK stack for changes to `src/agents/`, `tests/`, `examples/`, shared runtime utilities, or SDK build/test configuration such as `pyproject.toml`, `Makefile`, `mkdocs.yml`, `docs/scripts/`, and CI workflows. Docs-only and repo-meta changes can skip it unless they affect those build/test paths or the user requests the full stack. Lightweight review does not waive eligible SDK checks. The skill owns command order, sandbox execution, host-capacity checks, and retry rules. - **`$openai-knowledge`:** Use when OpenAI API/platform behavior needs authoritative external evidence. Inspect local code for SDK-owned behavior; do not repeat unchanged external research for purely local implementation details. - **`$pr-draft-summary`:** After applicable review and verification, generate the local PR draft for runtime, tests, examples, build/test changes, or behavior-impacting docs, including uncommitted work. Skip repo-meta/editorial-only work, an explicit user opt-out, or the release-specific handoff below. A draft never authorizes a branch, commit, push, or PR creation. -- **`$release-candidate-prep`:** Use only when explicitly invoked with a version. Follow its dedicated-worktree workflow and `$final-release-review` gate; its complete final-candidate report replaces the general PR draft. All runtime/docs changes must already be on `main`. The release commit contains only `pyproject.toml`, `uv.lock`, and `tests/fixtures/released_api_contract.json`. See [.github/RELEASING.md](.github/RELEASING.md) for maintainer release operations. +- **`$release-candidate-prep`:** Use only when explicitly invoked with a version. Follow its dedicated-worktree workflow and `$final-release-review` gate; its complete final-candidate report replaces the general PR draft. All runtime/docs changes must already be on `main`. The release commit contains only `pyproject.toml`, `uv.lock`, `.release-please-manifest.json`, and `tests/fixtures/released_api_contract.json`. See [.github/RELEASING.md](.github/RELEASING.md) for maintainer release operations. Continue authorized local work through fixes, applicable review, verification, and handoff. Stop for a concrete unresolved contract or scope decision, missing authority, or an external blocker. When a skill causes a stop, identify the exact instruction and explain the missing decision; do not ask for a generic continuation prompt. Never push, open a PR, or otherwise mutate GitHub. From b8b3c19b5718560719bfa1a129b0461a0b52d55f Mon Sep 17 00:00:00 2001 From: Justin Beckwith Date: Mon, 28 Sep 2026 09:48:27 -0700 Subject: [PATCH 3/5] docs: coordinate manual releases with release-please --- .agents/skills/release-candidate-prep/SKILL.md | 2 ++ .github/RELEASING.md | 6 ++++++ 2 files changed, 8 insertions(+) diff --git a/.agents/skills/release-candidate-prep/SKILL.md b/.agents/skills/release-candidate-prep/SKILL.md index 4b1c04663a..11583c8a3a 100644 --- a/.agents/skills/release-candidate-prep/SKILL.md +++ b/.agents/skills/release-candidate-prep/SKILL.md @@ -166,6 +166,8 @@ Apply the repository's GitHub paste-readiness rules to the report. Use native `# Also report the dedicated worktree path, local branch, commit SHA, parent `origin/main` commit, and the exact four-file manifest outside the copy-ready block. State explicitly that the source checkout was left unchanged, nothing was pushed, and no pull request was created. Leave the worktree in place for the user's handoff. +Include the [standalone manual release procedure](../../../.github/RELEASING.md#standalone-manual-release) in the handoff: before merging, an authorized maintainer pauses Release Please, waits for its queued/running jobs, and closes any superseded bot release PR. Release Please resumes only after the manual candidate's tag and GitHub Release exist. This skill does not perform those GitHub operations. + If `release/v` already exists on `origin`, inspect its exact current commit with credential-free `git ls-remote --heads origin release/v` immediately before handoff and record it as ``. State explicitly that the local branch has replaced the old candidate and now contains exact current `origin/main` plus only the new `release: ` commit. Because this skill never mutates GitHub, provide the user with the exact `git push --force-with-lease=refs/heads/release/v: origin release/v` command to replace the remote branch themselves; never run it. A normal push or an unspecified lease is insufficient for this replacement case. If the remote branch changes after inspection, the explicit lease must reject the push instead of overwriting unseen work. ## Failure behavior diff --git a/.github/RELEASING.md b/.github/RELEASING.md index 3efb184b0a..da02a5fc5e 100644 --- a/.github/RELEASING.md +++ b/.github/RELEASING.md @@ -23,8 +23,14 @@ Release Please does not regenerate the public API snapshot. Before marking the r The generator records the checked-out source commit and freezes the API surface for the proposed version. Review the generated `tests/fixtures/released_api_contract.json` diff, then commit and push it to the release PR branch using the maintainer’s own GitHub credentials. This push triggers the repository’s normal pull-request CI for the completed candidate. Do not merely replace its version string: new exports and signatures must be captured too. If the bot or another maintainer updates the candidate's source or version, regenerate and review the snapshot again before merging. 3. Run the required verification and wait for CI on the final candidate. The initial bot PR may fail the snapshot-version test until step 2 is complete. Mark the PR ready for review only after the snapshot and metadata agree. +### Standalone manual release + The standalone `$release-candidate-prep` skill prepares a manual four-file candidate: `pyproject.toml`, `uv.lock`, `.release-please-manifest.json`, and `tests/fixtures/released_api_contract.json`. The helper synchronizes the manifest with the requested version so the next automated proposal starts from the version actually released. The manual route uses maintainer-written GitHub Release notes and does not generate a changelog entry. It does not complete a bot PR; for that route, follow the steps above. +Before merging a standalone manual release PR, an authorized maintainer must [disable the **Release Please** workflow](https://docs.github.com/en/actions/how-tos/manage-workflow-runs/disable-and-enable-workflows) (`gh workflow disable release-please.yml --repo openai/openai-agents-python`). Wait for every already queued or running Release Please run to finish, then close any open bot release PR superseded by the manual candidate. Keep release PR CI, required review, and publishing workflows enabled. + +Keep Release Please disabled until the manual candidate has merged and its matching tag and GitHub Release exist. Advancing the manifest without that tag can cause Release Please to propose another version using already-released commits; a manual PR does not have the bot's pending-release guard. If release publication is delayed, leave Release Please disabled until that boundary is complete. Then re-enable it (`gh workflow enable release-please.yml --repo openai/openai-agents-python`); the next push to `main` or manual workflow dispatch can propose subsequent changes. Do not apply `autorelease` labels to a manual PR as a substitute for this procedure. + ### Temporary GitHub Actions authentication The workflow uses the repository's `GITHUB_TOKEN`, appearing as `github-actions[bot]`, with Contents, Issues, and Pull requests write permissions only on the release PR job. It runs only for `openai/openai-agents-python` on `main` and does not check out or execute release PR code. From a8a42bd95a349fda547eb3aa30d00b1991aa7cd0 Mon Sep 17 00:00:00 2001 From: Justin Beckwith Date: Mon, 28 Sep 2026 10:02:20 -0700 Subject: [PATCH 4/5] chore: limit release notes to features and fixes --- release-please-config.json | 10 +--------- 1 file changed, 1 insertion(+), 9 deletions(-) diff --git a/release-please-config.json b/release-please-config.json index efd0a3ac11..9565c31207 100644 --- a/release-please-config.json +++ b/release-please-config.json @@ -20,14 +20,6 @@ ], "changelog-sections": [ { "type": "feat", "section": "Features" }, - { "type": "fix", "section": "Bug Fixes" }, - { "type": "perf", "section": "Performance Improvements" }, - { "type": "revert", "section": "Reverts" }, - { "type": "chore", "section": "Chores" }, - { "type": "docs", "section": "Documentation" }, - { "type": "refactor", "section": "Refactors" }, - { "type": "build", "section": "Build System" }, - { "type": "test", "section": "Tests", "hidden": true }, - { "type": "ci", "section": "Continuous Integration", "hidden": true } + { "type": "fix", "section": "Bug Fixes" } ] } From 0198f7049d26fea8035678feefc4bbe1e51ded2e Mon Sep 17 00:00:00 2001 From: Justin Beckwith Date: Mon, 28 Sep 2026 10:25:53 -0700 Subject: [PATCH 5/5] fix: synchronize source version in manual release candidates --- .agents/skills/final-release-review/SKILL.md | 2 +- .../references/review-checklist.md | 2 +- .../skills/release-candidate-prep/SKILL.md | 22 ++++++++--------- .../release-candidate-prep/scripts/prepare.py | 24 ++++++++++++++++--- .../scripts/test_prepare.py | 20 ++++++++++++++++ .github/RELEASING.md | 2 +- AGENTS.md | 2 +- 7 files changed, 56 insertions(+), 18 deletions(-) diff --git a/.agents/skills/final-release-review/SKILL.md b/.agents/skills/final-release-review/SKILL.md index 9340a15029..85841be81a 100644 --- a/.agents/skills/final-release-review/SKILL.md +++ b/.agents/skills/final-release-review/SKILL.md @@ -105,7 +105,7 @@ In final-candidate mode, when the caller provides a dedicated checkout or worktr - Resolve and record the checkout root, current branch, `HEAD`, and clean status before auditing. Do not switch to a different checkout that happens to share the same Git object database. - Require `TARGET=HEAD` to resolve to the checked-out commit. Treat detached HEAD, a mismatched release branch, uncommitted release-owned files, or unrelated changed paths as candidate inconsistency. -- Read `pyproject.toml`, `uv.lock`, `.release-please-manifest.json`, and `tests/fixtures/released_api_contract.json` from that checkout. Verify the intended version, editable `openai-agents` lock entry, root Release Please manifest version, contract baseline, and contract `baseline_commit` against the release branch and commit parent. +- Read `pyproject.toml`, `uv.lock`, `.release-please-manifest.json`, `src/agents/version.py`, and `tests/fixtures/released_api_contract.json` from that checkout. Verify the intended version, editable `openai-agents` lock entry, root Release Please manifest version, literal source fallback, contract baseline, and contract `baseline_commit` against the release branch and commit parent. - Inspect the exact commit diff and confirm that the materialized release commit owns only its expected release manifest when the invoking workflow defines one. - Keep the checkout path as local evidence for the caller, but do not put local paths into copy-ready release text. diff --git a/.agents/skills/final-release-review/references/review-checklist.md b/.agents/skills/final-release-review/references/review-checklist.md index b9a64d4365..9d28b36605 100644 --- a/.agents/skills/final-release-review/references/review-checklist.md +++ b/.agents/skills/final-release-review/references/review-checklist.md @@ -19,7 +19,7 @@ Compare the diff with the released BASE contract. Use `minor` as the minimum for - a breaking change to a non-beta public API, protocol, configuration, environment, or durable serialized boundary; - a major user-facing feature addition that warrants a minor release under repository policy. -Use `patch` otherwise. For a final candidate, verify the intended version against the branch name, `pyproject.toml`, `uv.lock`, `.release-please-manifest.json`, and built package metadata when relevant. For planning mode, do not interpret unchanged version metadata as a declared patch candidate. +Use `patch` otherwise. For a final candidate, verify the intended version against the branch name, `pyproject.toml`, `uv.lock`, `.release-please-manifest.json`, the literal fallback in `src/agents/version.py`, and built package metadata when relevant. For planning mode, do not interpret unchanged version metadata as a declared patch candidate. Capture: diff --git a/.agents/skills/release-candidate-prep/SKILL.md b/.agents/skills/release-candidate-prep/SKILL.md index 11583c8a3a..cd39552f75 100644 --- a/.agents/skills/release-candidate-prep/SKILL.md +++ b/.agents/skills/release-candidate-prep/SKILL.md @@ -9,10 +9,10 @@ Use this skill only when the user explicitly invokes `$release-candidate-prep` a ## Non-negotiable boundaries -- Treat explicit invocation as authorization to fetch `origin/main`, create one dedicated detached release worktree, run branch-free release-readiness gates there, create or replace the local `release/v` in that worktree only after those gates pass, update the four release-owned files, and create one local commit. If the branch already exists locally or remotely, the required final local state is still exact current `origin/main` plus only the new release commit; an existing local branch may be replaced only when it is not checked out in another worktree. +- Treat explicit invocation as authorization to fetch `origin/main`, create one dedicated detached release worktree, run branch-free release-readiness gates there, create or replace the local `release/v` in that worktree only after those gates pass, update the five release-owned files, and create one local commit. If the branch already exists locally or remotely, the required final local state is still exact current `origin/main` plus only the new release commit; an existing local branch may be replaced only when it is not checked out in another worktree. - Keep the user's source checkout on its existing clean `main` commit. Do not fast-forward it, switch its branch, or materialize release files there. Leave the dedicated release worktree in place for green handoff, blocked review, or recoverable failure. - Never push, open or edit a pull request, add labels or milestones, create a release, or otherwise mutate GitHub. Never run `gh`. -- Own exactly `pyproject.toml`, `uv.lock`, `.release-please-manifest.json`, and `tests/fixtures/released_api_contract.json`. Runtime, documentation, workflow, or other repository changes must land on `main` before release preparation. +- Own exactly `pyproject.toml`, `uv.lock`, `.release-please-manifest.json`, `src/agents/version.py`, and `tests/fixtures/released_api_contract.json`. Runtime, documentation, workflow, or other repository changes must land on `main` before release preparation. - Do not stash, delete, overwrite or remove an existing worktree, or work around unrelated local changes. Fail before branch creation when the initial checkout is dirty or is not on `main`, the dedicated worktree is not clean and detached at refreshed `origin/main`, an existing local release branch is checked out in another worktree, the prospective packaged-contract gate fails after the allowed dependency-bootstrap recovery, the planning review blocks, or `origin/main` advances after those gates run. - Treat `$final-release-review` as the controlling release checker, not only as a report generator. Its planning gate must be green before branch creation, and its final-candidate gate must inspect the materialized worktree and be green before PR-ready handoff. Any candidate content, commit, or base change invalidates the previous green result. - Remove inherited `OPENAI_API_KEY` from every child command. Release preparation does not require a live OpenAI API request. @@ -89,10 +89,10 @@ The helper must complete all of these operations or fail with an actionable erro 1. Repeat the source-root, clean `main`, version, registered-worktree, detached-HEAD, and release-branch replaceability checks. 2. Refresh `origin/main` again without moving the source checkout. 3. Require refreshed `origin/main` and `` HEAD to equal ``. If `origin/main` advanced, retain the old detached worktree and rerun preflight plus both readiness gates in a new exact-base worktree. -4. Keep the worktree detached while updating the single project version declaration in `pyproject.toml` and the root version in `.release-please-manifest.json`. +4. Keep the worktree detached while updating the single project version declaration in `pyproject.toml`, the root version in `.release-please-manifest.json`, and the literal `__version__` fallback in `src/agents/version.py`. 5. Run `make sync` with `UV_DEFAULT_INDEX=https://pypi.org/simple`. 6. Run `make update-released-api-contract VERSION=` and then `make check-released-api-contract VERSION=`. -7. Require exactly the four release-owned paths to be modified in ``, leave them unstaged and uncommitted, and confirm that the source checkout remains unchanged. +7. Require exactly the five release-owned paths to be modified in ``, leave them unstaged and uncommitted, and confirm that the source checkout remains unchanged. 8. Only after those candidate checks pass, create or reset the local `release/v` inside `` to exact `` while preserving the validated unstaged manifest. Do not retain commits or content from an older local or remote candidate. This delayed replacement must leave an existing local branch unchanged when candidate generation fails. If the helper fails after branch creation, preserve its local branch, dedicated worktree, and working-tree evidence. Report the failing command and state rather than guessing whether a partial run is safe to resume. Never remove the worktree as automatic cleanup. @@ -104,21 +104,21 @@ Run the remaining commands from ``. Inspect all release-owned ```bash git status --short git diff --check -git diff -- pyproject.toml uv.lock .release-please-manifest.json tests/fixtures/released_api_contract.json +git diff -- pyproject.toml uv.lock .release-please-manifest.json src/agents/version.py tests/fixtures/released_api_contract.json ``` Confirm all of the following: -- `pyproject.toml`, the editable `openai-agents` entry in `uv.lock`, and the root entry in `.release-please-manifest.json` declare the requested version. +- `pyproject.toml`, the editable `openai-agents` entry in `uv.lock`, the root entry in `.release-please-manifest.json`, and the source fallback in `src/agents/version.py` declare the requested version. - The API contract baseline is `v` and its `baseline_commit` is the exact `origin/main` source commit on which the release branch is based. - The generated contract preserves the previous release and freezes intended new exports and signatures. - Any intended `public_properties`, `canonical_imports`, or `public_modules` policy additions have been reviewed explicitly; the updater deliberately does not infer them. -- No path outside the four-file release manifest is changed, staged, or untracked. +- No path outside the five-file release manifest is changed, staged, or untracked. Stage only the manifest and create exactly one local commit: ```bash -git add pyproject.toml uv.lock .release-please-manifest.json tests/fixtures/released_api_contract.json +git add pyproject.toml uv.lock .release-please-manifest.json src/agents/version.py tests/fixtures/released_api_contract.json git commit -m "release: " ``` @@ -126,7 +126,7 @@ Do not amend unrelated content into the commit. ## 6. Run the final-candidate release review -Invoke `$final-release-review` from `` in final-candidate mode with the release commit as `TARGET=HEAD`. This invocation is a release checker: it must inspect the complete candidate diff and the actual checked-out `release/v` contents, including `pyproject.toml`, the editable `openai-agents` entry in `uv.lock`, `.release-please-manifest.json`, and `tests/fixtures/released_api_contract.json`. The branch, package metadata, lockfile, Release Please manifest, contract baseline, contract `baseline_commit`, and intended version must agree. +Invoke `$final-release-review` from `` in final-candidate mode with the release commit as `TARGET=HEAD`. This invocation is a release checker: it must inspect the complete candidate diff and the actual checked-out `release/v` contents, including `pyproject.toml`, the editable `openai-agents` entry in `uv.lock`, `.release-please-manifest.json`, `src/agents/version.py`, and `tests/fixtures/released_api_contract.json`. The branch, package metadata, lockfile, Release Please manifest, source fallback, contract baseline, contract `baseline_commit`, and intended version must agree. If the review is blocked, stop. Return its unblock checklist, retain the local branch, commit, and worktree for follow-up, and do not present the candidate as PR-ready. A report body does not authorize continuation when the release call is blocked. After any fix, regenerate the API contract when the public surface may have changed, restore a single release commit, and rerun the complete final-candidate review. @@ -134,7 +134,7 @@ The earlier planning review proves that the source commit was ready before branc ## 7. Recheck main freshness -After a green review, fetch `origin main` again without credentials from `` and compare it with the release commit's parent. If they differ, the candidate is stale. First verify that the branch is clean, has exactly one local commit, and that the commit changes only the four-file release manifest. Rebase that commit onto the new `origin/main` so Git detects any conflicting release metadata. After a clean rebase, move the local release branch back to `origin/main` with a mixed reset, which preserves the rebased release tree as unstaged task-owned changes. Restore all four release-owned files (`pyproject.toml`, `uv.lock`, `.release-please-manifest.json`, and `tests/fixtures/released_api_contract.json`) from `origin/main`, run `make sync`, and require the worktree to be clean at the new base. Run `make check-prospective-released-api-contract` only in that internally consistent base state, where the installed project version and frozen contract baseline agree. Then update `pyproject.toml` and the root entry in `.release-please-manifest.json` to ``, run `make sync`, run `make update-released-api-contract VERSION=` and `make check-released-api-contract VERSION=`, review the exact manifest again, and recreate the single `release: ` commit. The base and candidate content changed, so the previous green check is invalid: rerun `$final-release-review` from the worktree and require a new green release call. Repeat until the reviewed local branch is exactly one commit ahead of current `origin/main` and that commit changes only the four-file release manifest. +After a green review, fetch `origin main` again without credentials from `` and compare it with the release commit's parent. If they differ, the candidate is stale. First verify that the branch is clean, has exactly one local commit, and that the commit changes only the five-file release manifest. Rebase that commit onto the new `origin/main` so Git detects any conflicting release metadata. After a clean rebase, move the local release branch back to `origin/main` with a mixed reset, which preserves the rebased release tree as unstaged task-owned changes. Restore all five release-owned files (`pyproject.toml`, `uv.lock`, `.release-please-manifest.json`, `src/agents/version.py`, and `tests/fixtures/released_api_contract.json`) from `origin/main`, run `make sync`, and require the worktree to be clean at the new base. Run `make check-prospective-released-api-contract` only in that internally consistent base state, where the installed project version and frozen contract baseline agree. Then update `pyproject.toml`, the root entry in `.release-please-manifest.json`, and the literal `__version__` fallback in `src/agents/version.py` to ``, run `make sync`, run `make update-released-api-contract VERSION=` and `make check-released-api-contract VERSION=`, review the exact manifest again, and recreate the single `release: ` commit. The base and candidate content changed, so the previous green check is invalid: rerun `$final-release-review` from the worktree and require a new green release call. Repeat until the reviewed local branch is exactly one commit ahead of current `origin/main` and that commit changes only the five-file release manifest. If replay conflicts or another path changes, stop with recoverable evidence. Do not force a resolution that expands the release commit beyond its manifest. @@ -164,7 +164,7 @@ Release Apply the repository's GitHub paste-readiness rules to the report. Use native `#123` references for this repository and `owner/repo#123` for another repository. Keep the required compare URL. Do not include local paths, Codex citations, operational diagnostics, or app directives inside the copy-ready description. -Also report the dedicated worktree path, local branch, commit SHA, parent `origin/main` commit, and the exact four-file manifest outside the copy-ready block. State explicitly that the source checkout was left unchanged, nothing was pushed, and no pull request was created. Leave the worktree in place for the user's handoff. +Also report the dedicated worktree path, local branch, commit SHA, parent `origin/main` commit, and the exact five-file manifest outside the copy-ready block. State explicitly that the source checkout was left unchanged, nothing was pushed, and no pull request was created. Leave the worktree in place for the user's handoff. Include the [standalone manual release procedure](../../../.github/RELEASING.md#standalone-manual-release) in the handoff: before merging, an authorized maintainer pauses Release Please, waits for its queued/running jobs, and closes any superseded bot release PR. Release Please resumes only after the manual candidate's tag and GitHub Release exist. This skill does not perform those GitHub operations. diff --git a/.agents/skills/release-candidate-prep/scripts/prepare.py b/.agents/skills/release-candidate-prep/scripts/prepare.py index fb6d014164..2a7613a0e6 100755 --- a/.agents/skills/release-candidate-prep/scripts/prepare.py +++ b/.agents/skills/release-candidate-prep/scripts/prepare.py @@ -24,10 +24,12 @@ VERSION_PATTERN = re.compile(r"\d+\.\d+(?:\.\d+)*(?:[A-Za-z0-9.-]+)?\Z") COMMIT_PATTERN = re.compile(r"[0-9a-f]{40}\Z") PROJECT_VERSION_PATTERN = re.compile(r'(?m)^version\s*=\s*"[^"]+"') +SOURCE_VERSION_PATTERN = re.compile(r'(?m)^([ \t]*__version__[ \t]*=[ \t]*)"([^"]+)"[ \t]*$') RELEASE_PATHS = frozenset( { ".release-please-manifest.json", "pyproject.toml", + "src/agents/version.py", "tests/fixtures/released_api_contract.json", "uv.lock", } @@ -169,7 +171,7 @@ def replace_project_version_text(text: str, version: str) -> str: def replace_project_version(repo: Path, version: str) -> None: - """Update package and release manifest versions without changing dependencies.""" + """Update package, source fallback, and manifest versions without changing dependencies.""" path = repo / "pyproject.toml" text = path.read_text(encoding="utf-8") @@ -180,6 +182,17 @@ def replace_project_version(repo: Path, version: str) -> None: manifest["."] = version manifest_path.write_text(json.dumps(manifest, indent=2) + "\n", encoding="utf-8") + source_path = repo / "src/agents/version.py" + source_text = source_path.read_text(encoding="utf-8") + updated, count = SOURCE_VERSION_PATTERN.subn( + lambda match: f'{match[1]}"{version}"', source_text + ) + if count != 1: + raise ReleasePreparationError( + "Expected exactly one literal __version__ fallback in src/agents/version.py." + ) + source_path.write_text(updated, encoding="utf-8") + def _current_branch(repo: Path) -> str: result = git(repo, "symbolic-ref", "--quiet", "--short", "HEAD", check=False) @@ -366,6 +379,11 @@ def _validate_prepared_files(repo: Path, version: str, base_commit: str) -> tupl manifest = json.loads((repo / ".release-please-manifest.json").read_text(encoding="utf-8")) if manifest.get(".") != version: raise ReleasePreparationError("The Release Please manifest does not match the version.") + source_versions = SOURCE_VERSION_PATTERN.findall( + (repo / "src/agents/version.py").read_text(encoding="utf-8") + ) + if len(source_versions) != 1 or source_versions[0][1] != version: + raise ReleasePreparationError("The source version fallback does not match the version.") contract = json.loads( (repo / "tests/fixtures/released_api_contract.json").read_text(encoding="utf-8") @@ -443,7 +461,7 @@ def materialize( expected_source_head: str, worktree: Path, ) -> PreparedCandidate: - """Create the four-file candidate in the reviewed isolated worktree.""" + """Create the five-file candidate in the reviewed isolated worktree.""" expected_base = validate_commit(expected_base) expected_source_head = validate_commit(expected_source_head) @@ -601,7 +619,7 @@ def main() -> int: print("Changed paths:") for path in candidate.changed_paths: print(f"- {path}") - print("Review the diff before staging the four release-owned files.") + print("Review the diff before staging the five release-owned files.") return 0 diff --git a/.agents/skills/release-candidate-prep/scripts/test_prepare.py b/.agents/skills/release-candidate-prep/scripts/test_prepare.py index e7842d8adc..da577c29c8 100755 --- a/.agents/skills/release-candidate-prep/scripts/test_prepare.py +++ b/.agents/skills/release-candidate-prep/scripts/test_prepare.py @@ -3,8 +3,10 @@ from __future__ import annotations +import importlib.metadata import json import os +import runpy import subprocess import tempfile import unittest @@ -57,6 +59,15 @@ def advance_origin(self) -> str: def _write_fixture_files(self) -> None: (self.repo / "tests/fixtures").mkdir(parents=True) + (self.repo / "src/agents").mkdir(parents=True) + (self.repo / "src/agents/version.py").write_text( + "import importlib.metadata\n\n" + "try:\n" + ' __version__ = importlib.metadata.version("openai-agents")\n' + "except importlib.metadata.PackageNotFoundError:\n" + ' __version__ = "0.19.4"\n', + encoding="utf-8", + ) (self.repo / ".release-please-manifest.json").write_text( json.dumps({".": "0.19.4"}, indent=2) + "\n", encoding="utf-8" ) @@ -251,12 +262,21 @@ def test_materialize_creates_branch_and_exact_uncommitted_manifest(self) -> None self.assertEqual(candidate.base_commit, fixture.base_commit) self.assertEqual(candidate.branch, "release/v0.20.0") + version_module = candidate.worktree / "src/agents/version.py" + with mock.patch( + "importlib.metadata.version", + side_effect=importlib.metadata.PackageNotFoundError("openai-agents"), + ): + self.assertEqual(runpy.run_path(str(version_module))["__version__"], "0.20.0") + with mock.patch("importlib.metadata.version", return_value="0.21.0"): + self.assertEqual(runpy.run_path(str(version_module))["__version__"], "0.21.0") self.assertEqual(set(candidate.changed_paths), prepare.RELEASE_PATHS) self.assertEqual( set(candidate.changed_paths), { ".release-please-manifest.json", "pyproject.toml", + "src/agents/version.py", "uv.lock", "tests/fixtures/released_api_contract.json", }, diff --git a/.github/RELEASING.md b/.github/RELEASING.md index da02a5fc5e..12f11dd9e6 100644 --- a/.github/RELEASING.md +++ b/.github/RELEASING.md @@ -25,7 +25,7 @@ Release Please does not regenerate the public API snapshot. Before marking the r ### Standalone manual release -The standalone `$release-candidate-prep` skill prepares a manual four-file candidate: `pyproject.toml`, `uv.lock`, `.release-please-manifest.json`, and `tests/fixtures/released_api_contract.json`. The helper synchronizes the manifest with the requested version so the next automated proposal starts from the version actually released. The manual route uses maintainer-written GitHub Release notes and does not generate a changelog entry. It does not complete a bot PR; for that route, follow the steps above. +The standalone `$release-candidate-prep` skill prepares a manual five-file candidate: `pyproject.toml`, `uv.lock`, `.release-please-manifest.json`, `src/agents/version.py`, and `tests/fixtures/released_api_contract.json`. The helper synchronizes the manifest and source-checkout fallback with the requested version so the next automated proposal starts from the version actually released. The manual route uses maintainer-written GitHub Release notes and does not generate a changelog entry. It does not complete a bot PR; for that route, follow the steps above. Before merging a standalone manual release PR, an authorized maintainer must [disable the **Release Please** workflow](https://docs.github.com/en/actions/how-tos/manage-workflow-runs/disable-and-enable-workflows) (`gh workflow disable release-please.yml --repo openai/openai-agents-python`). Wait for every already queued or running Release Please run to finish, then close any open bot release PR superseded by the manual candidate. Keep release PR CI, required review, and publishing workflows enabled. diff --git a/AGENTS.md b/AGENTS.md index 7e39ed87d0..6cf28ce7e0 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -31,7 +31,7 @@ Repository skills are stored under `.agents/skills/`. References below authorize - **`$code-change-verification`:** Run the final SDK stack for changes to `src/agents/`, `tests/`, `examples/`, shared runtime utilities, or SDK build/test configuration such as `pyproject.toml`, `Makefile`, `mkdocs.yml`, `docs/scripts/`, and CI workflows. Docs-only and repo-meta changes can skip it unless they affect those build/test paths or the user requests the full stack. Lightweight review does not waive eligible SDK checks. The skill owns command order, sandbox execution, host-capacity checks, and retry rules. - **`$openai-knowledge`:** Use when OpenAI API/platform behavior needs authoritative external evidence. Inspect local code for SDK-owned behavior; do not repeat unchanged external research for purely local implementation details. - **`$pr-draft-summary`:** After applicable review and verification, generate the local PR draft for runtime, tests, examples, build/test changes, or behavior-impacting docs, including uncommitted work. Skip repo-meta/editorial-only work, an explicit user opt-out, or the release-specific handoff below. A draft never authorizes a branch, commit, push, or PR creation. -- **`$release-candidate-prep`:** Use only when explicitly invoked with a version. Follow its dedicated-worktree workflow and `$final-release-review` gate; its complete final-candidate report replaces the general PR draft. All runtime/docs changes must already be on `main`. The release commit contains only `pyproject.toml`, `uv.lock`, `.release-please-manifest.json`, and `tests/fixtures/released_api_contract.json`. See [.github/RELEASING.md](.github/RELEASING.md) for maintainer release operations. +- **`$release-candidate-prep`:** Use only when explicitly invoked with a version. Follow its dedicated-worktree workflow and `$final-release-review` gate; its complete final-candidate report replaces the general PR draft. All runtime/docs changes must already be on `main`. The release commit contains only `pyproject.toml`, `uv.lock`, `.release-please-manifest.json`, `src/agents/version.py`, and `tests/fixtures/released_api_contract.json`. See [.github/RELEASING.md](.github/RELEASING.md) for maintainer release operations. Continue authorized local work through fixes, applicable review, verification, and handoff. Stop for a concrete unresolved contract or scope decision, missing authority, or an external blocker. When a skill causes a stop, identify the exact instruction and explain the missing decision; do not ask for a generic continuation prompt. Never push, open a PR, or otherwise mutate GitHub.