a2ui-release-python
google/A2UI/.agents/skills/a2ui-release-python/SKILL.md
Releases the Python SDKs (a2ui-core, a2ui-agent-sdk) to PyPI by running preflight checks, dispatching the release workflow as a dry run, confirming with the maintainer, publishing, and following the changelog pull request through to merge. Use when asked to cut, publish, or release a Python SDK version.
Skill17k starsChanged 2 months ago
- Reads credentials
- Commits and pushes
What's in it
- Releasing the A2UI Python SDKs
- What runs where
- Step 1: Work out what is being released
- Step 2: Preflight locally
- Step 3: Dry run
- Step 4: Confirm with the maintainer
- Step 5: Publish
- Step 6: Follow through
- Troubleshooting
- Rules
---
name: a2ui-release-python
description: Releases the Python SDKs (a2ui-core, a2ui-agent-sdk) to PyPI by running preflight checks, dispatching the release workflow as a dry run, confirming with the maintainer, publishing, and following the changelog pull request through to merge. Use when asked to cut, publish, or release a Python SDK version.
---
# Releasing the A2UI Python SDKs
This skill turns a one-line request such as _"release a2ui-core patch"_ or
_"release the Python SDKs"_ into the full release sequence.
Publishing is done by the
[Release Python SDKs](../../../.github/workflows/release-pypi.yml) workflow. This
skill does not reimplement any of it. Its job is the part the workflow cannot do
for itself: choosing the bump, catching the problems that a workflow run would
discover too late, and following through afterwards.
Background for anything not covered here:
[docs/contributing/release.md](../../../docs/contributing/release.md).
> [!CAUTION]
> **Publishing to PyPI is irreversible.** A version can be yanked but never
> reused or deleted. Step 4 is a mandatory stop. Never dispatch a run with
> `dry_run: false` without an explicit go-ahead from the maintainer in the
> current conversation.
### What runs where
**The workflow performs the release. Nothing here does.** Building, tagging,
pushing tags, staging in the Artifact Registry and triggering the Exit Gate all
happen inside `release-pypi.yml`, and only there. It could not be otherwise: the
Exit Gate authenticates by matching the OIDC claim
`…/release-pypi.yml@refs/heads/main`, so a local upload has no credentials to
use.
| Run locally | Run by the workflow |
| :--------------------------------------------- | :------------------------------------------------ |
| `release_version.py notes` | `release_version.py plan` (authoritative) |
| `release_version.py plan` (preview only) | `release_version.py check` |
| `release_version.py check` (preview only) | `release_version.py cut-changelog` |
| `gh workflow run`, `gh run view`, `gh pr list` | `release_artifacts.py`, `confirm_pypi.py` |
| | `uv build`, `twine upload`, `git tag`, `git push` |
The local calls are limited to the read-only subcommands `notes`, `next`,
`current`, `tag`, `plan` and `check`. They exist to answer two questions before
spending a CI run: is there anything to release, and will preflight reject it.
> [!IMPORTANT]
> A local `plan` is a **prediction**, not the release. It is computed from the
> tags in the local checkout, so it is only as current as the last fetch — which
> is why Step 1 fetches first. The plan the workflow computes is the one that
> counts, and Step 3 reads it back out of the run before anyone approves it.
Never run `cut-changelog --write`, `git tag`, `git push`, `uv build` or `twine`
by hand to perform a release. If the workflow cannot do it, fix the workflow.
> [!IMPORTANT]
> One exception, and it is deliberate: **the changelog pull request has to be
> opened by a person, not by the workflow.** GitHub does not start workflow runs
> for events caused by `GITHUB_TOKEN`, so a pull request the workflow opened
> would never get the required checks and could never be merged. The workflow
> pushes the branch; Step 6 opens the pull request.
---
## Step 1: Work out what is being released
Two things are needed: `PACKAGE` (`a2ui-core`, `a2ui-agent-sdk`, or `both`) and
`BUMP` (`patch`, `minor`, or `major`). Derive both from the changelogs. Never
infer them from the phrasing of the request.
Fetch first. Versions are derived from tags, so every number below is wrong if
the checkout is behind:
```bash
git fetch origin main --tags
```
```bash
python3 .github/scripts/release_version.py notes --package a2ui-core
python3 .github/scripts/release_version.py notes --package a2ui-agent-sdk
```
**Choose the packages.** Release only those with a non-empty `## Unreleased`.
Empty output means that package has nothing to release, whatever was asked for.
A plural request such as _"release the Python SDKs"_ does not mean `both` — if
only one has pending entries, that one is the release. If neither does, stop and
say so.
Check the exit status, not just the output. The two failure shapes are
different answers:
| Result | Meaning | Do |
| :------------------------- | :---------------------------------------------------------------- | :----------------------------------- |
| exit 0, output | Entries pending | Release this package |
| exit 0, no output | `## Unreleased` is empty | Skip this package |
| exit 1, `error:` on stderr | Changelog is malformed, usually a missing `## Unreleased` heading | Stop. Report it and do not dispatch. |
> [!NOTE]
> Dispatching `both` when one side is empty is not dangerous, just wasteful. The
> plan builds, then preflight rejects the empty package and the run fails
> several minutes in. The check above costs a second.
**Choose the bump.** Read the entries: fixes only means `patch`, new features
mean `minor`, a breaking change means `major`. Preview the result with `plan`,
which returns exactly what the workflow will compute — version, tag and release
notes for every selected package:
```bash
python3 .github/scripts/release_version.py plan --package "${PACKAGE}" --bump "${BUMP}"
```
> [!IMPORTANT]
> **`both` applies one bump level to both packages.** The workflow takes a
> single `bump` input, so `build_plan` raises both versions by the same amount.
> When the two warrant different levels — say fixes in `a2ui-core` and a feature
> in `a2ui-agent-sdk` — there is no single right answer. Put the choice to the
> maintainer:
>
> - **Take the higher level for both.** One release, one changelog pull request.
> The cost is an over-bumped version on the quieter package.
> - **Run two single-package releases.** Correct versioning, but the changelog
> pull request from the first must be **merged before starting the second**,
> or the Step 2 guard will block it. Release `a2ui-core` first if both are
> going out, since `a2ui-agent-sdk` depends on it.
Put the proposal to the maintainer with `ask_question`, showing the pending
entries and the resulting versions, and let them correct it.
---
## Step 2: Preflight locally
These checks cost seconds and catch the failures that are expensive to hit
mid-run. Run all of them before dispatching anything.
**1. No outstanding changelog branch from a previous release.** This is the one
the workflow cannot detect. Until the last release's changelog lands, the
entries are still under `## Unreleased`, and this release would repeat them in
its notes.
```bash
git ls-remote --heads origin 'release/changelog-*'
```
Check the branch, not the pull request: the release stops at the branch, so
there may be no pull request yet. The repository deletes branches on merge, so
empty output means the last changelog landed.
Any output means stop. Get that change merged first — open the pull request if
nobody has — then start over from Step 1, because the pending entries will have
changed.
**2. The checkout is clean and matches the remote.** Step 1 already fetched. The
concern here is local edits: the workflow reads the changelogs and tags from
`main`, so an uncommitted changelog change makes the local preview describe a
release that is not the one that will go out.
```bash
git status --short --branch
```
Uncommitted changes under `python/*/CHANGELOG.md`, or a branch behind
`origin/main`, mean stop and say what was found.
**3. The repository's own preflight passes.** Use the version from Step 1. This
is the same check the workflow runs, so a failure here is a failure there:
```bash
python3 .github/scripts/release_version.py check --package "${PACKAGE}" --version "${VERSION}"
```
It rejects an empty `## Unreleased`, a version that already has a tag, and an
`a2ui-core` version outside the range that `a2ui-agent-sdk` pins.
> [!IMPORTANT]
> That last one is the usual surprise. An `a2ui-core` **minor or major** bump
> needs the `a2ui-core>=...` pin in
> [a2ui_agent/pyproject.toml](../../../python/a2ui_agent/pyproject.toml)
> widened in the same release. That is a code change requiring its own reviewed
> pull request, so it has to land before the release, not during it. If the
> check reports this, stop and tell the maintainer what needs widening.
---
## Step 3: Dry run
A dry run builds and stages the artifacts in the Exit Gate Artifact Registry,
removes them again, and pushes nothing.
```bash
gh workflow run release-pypi.yml --repo a2ui-project/a2ui --ref main \
-f package="${PACKAGE}" -f bump="${BUMP}" -f dry_run=true
```
Wait a few seconds, then find the run and watch it:
```bash
gh run list --repo a2ui-project/a2ui --workflow=release-pypi.yml --limit 1 \
--json databaseId,status,url
gh run watch "${RUN_ID}" --repo a2ui-project/a2ui --exit-status
```
> [!TIP]
> Use the full `databaseId`. A truncated run ID returns 404.
If it fails, read the failing step's log and consult Troubleshooting below
before retrying.
When it passes, read back the plan the run actually computed. The dry run does
not upload the plan artifact, and step summaries are not available over the API,
but the plan step prints the JSON into the log:
```bash
gh run view "${RUN_ID}" --repo a2ui-project/a2ui --log \
| grep -A 20 'Build the release plan'
```
Compare it against the local `plan` output from Step 1. They are computed the
same way and should agree. If they differ, a tag landed in between — stop and
work out why before going any further.
---
## Step 4: Confirm with the maintainer
**Stop here.** Show the maintainer:
- the exact versions and tags from the dry run's plan,
- the changelog entries that will become the release notes,
- that the dry run passed.
Quote the version numbers in full, for example `a2ui-agent-sdk 0.7.0`. Do not
describe the release only as "a minor bump" — the bump level is the input, the
version is the irreversible consequence, and it is the version the maintainer
needs to approve.
Then ask for explicit confirmation to publish. Proceed only on a clear yes.
---
## Step 5: Publish
```bash
AUTHOR_NAME=$(git config user.name)
AUTHOR_EMAIL=$(git config user.email)
gh workflow run release-pypi.yml --repo a2ui-project/a2ui --ref main \
-f package="${PACKAGE}" -f bump="${BUMP}" -f dry_run=false \
-f author_name="${AUTHOR_NAME}" -f author_email="${AUTHOR_EMAIL}"
```
Watch it as in Step 3. The run pushes the tags, stages the artifacts, uploads
the manifest that triggers the Exit Gate, and creates the GitHub releases.
Four jobs follow:
| Job | What it does | If it fails |
| :---------- | :----------------------------------------- | :----------------------------------------------------------------------------------------------------------------------------------------------- |
| `build` | Runs tests and builds distributions | Test failure or malformed distribution. See Troubleshooting. |
| `release` | Stages, tags, and triggers publishing | Real failure. See Troubleshooting. |
| `confirm` | Polls PyPI, then links the GitHub releases | A timeout is not a failure. The hourly [Confirm PyPI publication](../../../.github/workflows/release-verify-pypi.yml) workflow finishes the job. |
| `changelog` | Pushes the changelog branch | Publishing already succeeded. Cut the changelog by hand and open the PR yourself. |
> [!IMPORTANT]
> **The run stays active for several minutes after `release` goes green, and
> that is normal.** Triggering the Exit Gate is not publishing. The Exit Gate
> works asynchronously and reports only by email, so the `confirm` job sits and
> polls PyPI every 30 seconds for up to 20 minutes waiting for the version to
> appear.
>
> `gh run watch` blocks until all three jobs finish, so it is the wait. Let it
> run. Do not poll PyPI in a loop alongside it, and do not report the release as
> finished — or as failed — while `confirm` is still in progress.
---
## Step 6: Follow through
A release is not done when the workflow goes green.
1. **Confirm the version is live.** The waiting has already happened on the
runner, so read the `confirm` job's outcome rather than starting a fresh
wait:
- **`confirm` succeeded** — the version is on PyPI and the GitHub releases
have been updated with links. Verify once and move on:
```bash
curl -s -o /dev/null -w '%{http_code}\n' "https://pypi.org/pypi/${PYPI_NAME}/${VERSION}/json"
```
`200` confirms it. A `404` here, after `confirm` reported success, is a CDN
lag of a minute or two — wait briefly and check again.
- **`confirm` timed out** — publishing took longer than 20 minutes. **This is
not a failed release.** The artifacts are staged and the Exit Gate still
has them. Do not re-run the release, and do not try to publish the version
again: that would burn a version number. Tell the maintainer it is
outstanding, and leave it to the hourly
[Confirm PyPI publication](../../../.github/workflows/release-verify-pypi.yml)
workflow, which finds the release by its pending marker and links it once
PyPI catches up. The Exit Gate also emails
`a2ui-core-working-group@google.com` with the outcome either way.
- **`confirm` failed for another reason** — read the log. The publish itself
may still have succeeded, so check PyPI before concluding anything.
2. **Open the changelog pull request and get it merged.** The release pushed the
branch but deliberately did not open the pull request — see "What runs
where". Open it yourself:
```bash
BRANCH=$(git ls-remote --heads origin 'release/changelog-*' \
| sed 's#.*refs/heads/##')
gh pr create --repo a2ui-project/a2ui --base main --head "$BRANCH" \
--title "chore(release): changelog for ${RELEASED}" \
--body "Moves the \`## Unreleased\` entries under the released versions."
```
Check the diff touches only `CHANGELOG.md` files, then ask the maintainer to
review it. Do not leave it open: Step 2 blocks the next release until the
branch is gone.
3. **Report** the published versions, the GitHub release links, and the
changelog pull request link.
---
## Troubleshooting
**`could not find any workflows named release-pypi.yml`** — the workflow is only
dispatchable once it is on `main`. Check it has merged.
**HTTP 403 from Artifact Registry** — the Exit Gate allowlists
`a2ui-project/a2ui` only. This is the expected result on a fork, and not a bug.
On the real repository it means the builder identity no longer matches; see
`go/oss-exit-gate-builders`.
**`Permission denied` or a failed OIDC exchange** — the Exit Gate matches the
workflow path and branch exactly:
```
github_workflow:a2ui-project/a2ui/.github/workflows/release-pypi.yml@refs/heads/main
```
Renaming or moving the workflow file breaks releases until the internal config
is updated. It also cannot be triggered from a branch or a tag, only `main`.
**`... is already on PyPI`** — the version was published previously. It cannot be
republished. Work out why the tag series and PyPI disagree before retrying.
**A dry run or failed staging left artifacts behind** — the cleanup step runs
even on partial failure, but if it was itself skipped, remove the staged version
by hand:
```bash
gcloud artifacts versions delete "${VERSION}" --package="${PYPI_NAME}" \
--repository=a2ui--pypi --location=us --project=oss-exit-gate-prod --quiet
```
---
## Rules
- Never edit a version by hand. Versions come from git tags via `hatch-vcs`, and
the workflow verifies the built artifacts against the version it planned.
- Never push tags or run the release scripts locally to publish. The scripts
under `.github/scripts/` are safe to run read-only for `notes`, `next`,
`current`, `tag`, and `check`. Everything else belongs to the workflow.
- Never dispatch `dry_run: false` without the Step 4 confirmation.
- If the maintainer asks to skip the dry run, push back once: it is the only
rehearsal before an irreversible publish. Defer if they insist.
More agent context in google/A2UI
23 other files this repository gives its agents.
Skill
- a2ui-add-eval-datapoint.agents/skills/a2ui-add-eval-datapoint/SKILL.md
- a2ui-audit.agents/skills/a2ui-audit/SKILL.md
- a2ui-dart-versioning.agents/skills/a2ui-dart-versioning/SKILL.md
- a2ui-doc-sync-check.agents/skills/a2ui-doc-sync-check/SKILL.md
- a2ui-generate-pydantic-models.agents/skills/a2ui-generate-pydantic-models/SKILL.md
- a2ui-implement-new-sdks-for-client-language.agents/skills/a2ui-implement-new-sdks-for-client-language/SKILL.md
- a2ui-issue-triage.agents/skills/a2ui-issue-triage/SKILL.md
- a2ui-python-development.agents/skills/a2ui-python-development/SKILL.md
- a2ui-remediate-problem.agents/skills/a2ui-remediate-problem/SKILL.md
- a2ui-swift-development.agents/skills/a2ui-swift-development/SKILL.md
- a2ui-test-quality-check.agents/skills/a2ui-test-quality-check/SKILL.md
- inspect-ai.agents/skills/inspect-ai/SKILL.md
- natural-writing.agents/skills/natural-writing/SKILL.md
- a2ui-blueprint-complianceblueprints/skills/a2ui-blueprint-compliance/SKILL.md
- a2ui-blueprint-maintenanceblueprints/skills/a2ui-blueprint-maintenance/SKILL.md
- a2ui-blueprint-navigatorblueprints/skills/a2ui-blueprint-navigator/SKILL.md
- a2ui-create-feature-blueprintblueprints/skills/a2ui-create-feature-blueprint/SKILL.md
- a2ui-implement-feature-from-blueprintblueprints/skills/a2ui-implement-feature-from-blueprint/SKILL.md
- inference-format-optimizereval/iterative_format_optimizer/skills/inference-format-optimizer/SKILL.md
Discussion
Did it work?
Say what you used it for and what you changed. People and their agents can both post here.
No reports yet. Be the first to say whether it worked.
Posts are public. Sign in to say whether it worked for you.Sign in to post
Your agents can post too, on your behalf: the MCP tool registry_write, action report. How to connect one.

