Normative guide for humans and coding agents working in this repository. Other documents
(README.md, CONTRIBUTING.md, python/README.md) are short and
link here. Design source: the
ai-integrations design doc.
- Do not create
__init__.pyinpython/<plugin>/src/temporalio/orsrc/temporalio/contrib/. The SDK wheel owns those packages;scripts/ci/check_wheel.pyfails the build if they appear. - Do not install with plain
uv syncor run tests with a bareuv run. Usemake sync,make test, and the other targets; they exportUV_NO_EDITABLE=1. - Do not add a workflow file, job, or secret for one plugin. Plugin variation lives in
plugin.toml,pyproject.toml, and the shared make targets. - Do not add
exclude-newerwithoutexclude-newer-package = { temporalio = false }, and do not replace--lockedwith--frozen. - Do not add
[tool.uv.sources]path or workspace entries between plugins. Cross-plugin dependencies use published registry coordinates only. - Do not hand-edit the committed
0.0.0development version. Protected release tags are the published version source; never move or delete a tag. A failed pre-release gets the nextrcN. - Do not write
CHANGELOG.mdfiles. Release notes are generated from commit messages. - Do not put Python source or stub files in
python/_shared/. Python test support is duplicated into each plugin (and its source template) so every file resolves against that plugin's environment. Non-Python make logic and tool configuration may be shared there when the format supports composition.
AI plugins for Temporal SDKs. Each plugin is
independently installable, dependency-isolated, tested, versioned and released. Layout is
<language>/<integration>/; directories under a language root that start with _ are shared
resources (python/_shared/, python/_template/) and are ignored by CI discovery.
| Folder | Coordinate | First version here | Maturity | Root API |
|---|---|---|---|---|
python/mcp |
temporalio-mcp |
0.1.0 | Pre-release | temporalio.mcp |
python/deepagents |
temporalio-deepagents |
0.0.1 | Pre-release | temporalio.deepagents |
python/google_adk |
temporalio-google-adk |
0.0.1 | Pre-release | temporalio.google_adk |
python/google_genai |
temporalio-google-genai |
0.1.0 | Public Preview | temporalio.google_genai |
python/langgraph |
temporalio-langgraph |
0.1.0 | Public Preview | temporalio.langgraph |
python/langsmith |
temporalio-langsmith |
0.1.0 | Public Preview | temporalio.langsmith |
python/openai_agents |
temporalio-openai-agents |
1.0.0 | Generally Available | temporalio.openai_agents |
python/strands_agents |
temporalio-strands-agents |
0.1.0 | Public Preview | temporalio.strands_agents |
python/typesafe |
temporalio-typesafe |
0.0.1 | Pre-release | temporalio.typesafe |
typescript/vercel-ai-sdk |
@temporalio/vercel-ai-sdk |
1.0.0 | Generally Available | @temporalio/vercel-ai-sdk |
typescript/google-adk |
@temporalio/google-adk |
0.1.0 | Public Preview | @temporalio/google-adk |
typescript/langsmith |
@temporalio/langsmith |
continues (1.24.0 next) | Public Preview | @temporalio/langsmith |
typescript/openai-agents |
@temporalio/openai-agents |
continues (1.24.0 next) | Generally Available | @temporalio/openai-agents |
typescript/strands-agents |
@temporalio/strands-agents |
continues (1.24.0 next) | Pre-release | @temporalio/strands-agents |
java/spring-ai |
io.temporal:spring-ai |
0.1.0 | Public Preview | io.temporal.springai |
go/googleadk |
go.temporal.io/googleadk |
0.1.0 (new coordinate) | Public Preview | googleadk |
"First version here" values are informational; the registry is the source of truth for the
version policy (below). Go plugins use go.temporal.io/<name> module paths and live under
go/<name>/. Before publishing, the vanity site must route each module to this repository's
subdirectory. The Go module reference documents the fourth
go-import field for subdirectories, supported since Go 1.25. Google ADK requires Go 1.26.6,
so its mapping can be:
<meta name="go-import" content="go.temporal.io/googleadk git https://github.com/temporalio/ai-integrations go/googleadk">Its tags are go/googleadk/v<version>. go/googleadk is now owned here under the new module
path. UUID and span random-stream
identifiers retain their original strings for replay compatibility. The shared Go release
workflow verifies vanity routing before accepting a published module.
Naming derivation, enforced by scripts/ci/check_conventions.py: folder name = plugin.toml
name; Python coordinate = temporalio- + name with _ replaced by -; Python root API =
temporalio.<name>; release tag = <language>/<name>/v<version>. Folders never end in -plugin
or _plugin.
Maturity mapping (plugin.toml maturity and the Python classifier must agree):
pre-release = Development Status :: 3 - Alpha; public-preview =
Development Status :: 4 - Beta; generally-available =
Development Status :: 5 - Production/Stable.
Use each plugin's public Temporal documentation for its release stage. In READMEs and
this table's Maturity column, use the three release-stage labels: Pre-release maps to
pre-release, Public Preview to public-preview, and Generally Available to
generally-available. Feature-specific stages do not change a plugin's
overall maturity (for example, OpenAI Agents is Generally Available with preview or experimental features).
- Each plugin owns its manifest and lockfile (
pyproject.toml+uv.lock); no lockfile at a language root, no root Python project.scripts/is a separate tooling project, not a root project. - Each plugin carries
plugin.toml(metadata only: name, language, coordinate, registry, root API, maturity, docs,[ci] runtime-versions,[smoke] imports). It holds no secrets and no owners;.github/CODEOWNERSis* @temporalio/ai-sdk. - The root
LICENSEis the source of truth and every plugin directory carries a committed regular-file copy of it, because each wheel and sdist must ship the license text. The conventions check fails unless the plugin copy is committed and byte-identical to the root file (cp LICENSE python/<name>/LICENSEto refresh);pyproject.tomldeclareslicense = "MIT"andlicense-files = ["LICENSE"];check_wheel.pyverifies the packaged text equals the root file. - Python manifests and lockfiles carry the static
0.0.0development placeholder. A protected release tag is the published version source; the release matrix runsuv version <tag-version>before sync, test and build, and only those tested artifacts are published. Ordinary CI and local development keep0.0.0. - No changelog files.
scripts/release/release_tool.py release-notesderives notes from commit subjects touching the plugin directory since its previous tag. - Every GitHub Action is pinned to a full commit SHA with a
# vNcomment (org opengrep rule).
- Layout:
python/<name>/{pyproject.toml, uv.lock, plugin.toml, Makefile, README.md, LICENSE, src/temporalio/<name>/, tests/}. - Build backend
uv_buildwithmodule-name = "temporalio.<name>".py.typedships in the leaf package (redundant with the SDK's marker, kept on purpose). - Installs are non-editable.
temporaliois a regular package owned by the SDK wheel, so an editable install of a plugin can resolve incorrectly unless the SDK extends its package path.python/_shared/python.mkexportsUV_NO_EDITABLE=1;[tool.uv] cache-keysincludessrc/**/*so edits trigger a rebuild;link-mode = "copy"keeps overwrites deterministic. - Provenance guard (
tests/helpers/provenance.py, mirrored byscripts/ci/smoke.py) runs at every pytest session start and fails loudly if the install is editable, any file differs from the distribution's RECORD, files under the package directory are not owned by the distribution, or another distribution ships the same paths.tests/test_installed_matches_source.pyadditionally byte-compares the installed package withsrc/. - Dependency cooldown:
exclude-newer = "2 weeks"(org supply-chain policy) withexclude-newer-package = { temporalio = false }so a new SDK release is adoptable the day it ships; a plugin that depends on another plugin adds that coordinate too (temporalio-mcp = falseinopenai_agents), otherwise a fresh first-party release is invisible touv lockfor two weeks. Declare exactly what the package imports at module level;openinferenceand similar lazy imports go in an extra. - Tooling: ruff, pyright, basedpyright, mypy (
mypy_path = "src",explicit_package_bases), pydocstyle (google), pytest + xdist (-n auto --dist=worksteal; theos._exit(0)hook is xdist-aware).python/_shared/owns the make recipes and composable Pyright/Ruff defaults; mypy and pydocstyle settings stay in eachpyproject.tomlbecause those tools cannot extend another config while preserving plugin overrides. All tools are invoked throughmaketargets; seemake help. - Tests self-provision the Temporal dev server (
WorkflowEnvironment.start_local, version pinned intests/__init__.py) with its default configuration; add a--dynamic-config-valueflag in a plugin's conftest only when one of its tests needs a server feature that is off by default. Bind those options in a startup callback passed totests/helpers/environment.py'sstart_local_with_retry. This plugin-agnostic policy allows three startup attempts with a one-second delay for the SDK's explicit startup-deadline message; other errors and cancellation propagate immediately. - Test scaffolding: each plugin owns its provenance guard, local dev-server fixtures and pytest hooks. New plugins copy the standard implementations from
python/_template/tests/; the conventions check keepsenvironment.py,plugin_meta.py,provenance.py,test_env.py, andtest_installed_matches_source.pybyte-identical to their canonical templates. Plugin-specific fixtures stay in that plugin's conftest. Tests must not require real provider credentials; use deterministic local models, mock transports, or in-process servers to exercise provider behavior in CI.
- Each Java plugin owns a Gradle wrapper with distribution checksum,
build.gradle,settings.gradle,plugin.toml, and per-Spring-Boot dependency locks. Shared build conventions live injava/_shared/java.gradle. - Every Java plugin starts
settings.gradlewith apluginManagementblock applying../_shared/settings.gradle. The shared script checks Maven Central before the Gradle Plugin Portal; the conventions check enforces this entry point for every plugin. - Java development versions are
0.0.0; a release tag supplies-PreleaseVersionbefore tests and builds. Do not publish the placeholder or add snapshot automation. - Java packages and publication metadata use the root MIT license.
- CI uses metadata to test Ubuntu on Java 17/25 and macOS/Windows on Java 25. All compatibility variants run on PRs. The primary Ubuntu/max variant alone produces the tested Maven distributions and runs a clean consumer smoke test.
./gradlew spotlessCheck test stageDistchecks without rewriting source. Updating dependency locks is intentional:./gradlew resolveAndLockAll --write-locks.- Nightly and
latest-deps=trueruns use-PdependencyMode=latest, selecting stable releases within the plugin's declared dependency families and each configured Spring Boot major series. RunresolveAndLockAll --write-locks --refresh-dependenciesfirst; subsequent test and build commands reuse a separate ignored latest lock. Ordinary CI and releases default to-PdependencyMode=lockedand committed locks.
- Each
typescript/<name>/is an independent pnpm project withpackage.json,pnpm-lock.yaml,pnpm-workspace.yaml,plugin.toml,tsconfig.json,Makefile,README.mdand a regular-file copy of the root MITLICENSE. No language-level workspace or lockfile. Shared make, TypeScript and ESLint configuration lives intypescript/_shared/. - Coordinates and root APIs are
@temporalio/<name>. The development version is0.0.0. Node 22.12.0 is the minimum (provider packages require Node 22 and unflagged CommonJS loading of ES modules); CI tests the latest Node 22.x and 24.x using major selectors withcheck-latest: true. Use pnpm 10.27.0 andmake sync,make lint,make test,make build. - SDK and Workflow Streams dependencies use published npm coordinates. The
unpublished SDK test helpers are copied into each consuming plugin's
tests/helpers/, with a private development-only link. Never use a local SDK checkout or another plugin's checkout as a dependency. - Local server startup uses
startLocalWithRetry, with three attempts and a one-second delay only for the SDK's explicit startup-deadline error. Bind each plugin's server configuration in the startup callback. Copy the helper and its regression tests fromtypescript/_template/src/__tests__/; the conventions check keeps each plugin's copies byte-identical to the templates. make syncpreserves the committed lock.make sync-latestupdates compatible ranges under the two-week pnpm release-age policy. Preserve first-party and exact AI SDK dependency-family cooldown exceptions inherited from the SDK.- Activity names remain stable for workflow replay compatibility.
- All five TypeScript integrations are enabled for release
through the shared
release-typescript.ymlworkflow. New coordinates require one interactive publication of the tested tarball before npm permits configuring their trusted publisher, as described below. No per-plugin workflows or secrets. CI checks tarball contents and a clean consumer import.
One entry workflow, one reusable workflow per language, plugin as a parameter, no secrets.
.github/workflows/ci.yml(pull_request,merge_group, push tomain, nightly, dispatch). Jobchangesrunsscripts/ci/detect_changes.py: plugins are discovered from<language>/*/<manifest>(ignoring_*); a changed file under a plugin selects that plugin; a non-plugin file under a language root selects every plugin of that language;.github/**andscripts/ci/**select everything;scripts/release/**select only the script tests; push tomain, nightly and dispatch select everything. Jobconventionschecks repository invariants and runs the script tests. Jobspython,typescript,java, andgocall their per-language reusable workflows once per selected plugin. Jobci-statusfans in and is the only required check (skipped upstream jobs count as success)..github/workflows/_typescript-plugin.yml: uses the shared runtime/platform matrix and independent pnpm lockfile for each plugin. Every cell runs sync, lint and tests; ubuntu/max also packs, verifies contents and smoke-installs the tarball in a clean consumer. Nightly selects compatible latest dependencies. TypeScript nightly issues usetypescript/<plugin>keys to avoid collisions with Python..github/workflows/_python-plugin.yml: jobmatrixreadsplugin.tomlruntime-versionsand emits the same matrix for every run, pull requests included (ubuntu at the min and max versions, macOS and Windows at max); jobtestrunsmake sync(orsync-lateston nightly runs),make lint,make test, then, on the ubuntu/max cell only, thepython-build-checkcomposite action (make build,check_wheel.py, isolatedsmoke.pyon wheel and sdist). Windows runners install GNU make with choco..github/workflows/_go-plugin.yml: uses the same matrix generator and platform layout with each plugin's Goruntime-versions. Every cell runsmake sync(orsync-latest),make lint,make test, andmake build; the ubuntu/max cell enables-race.GOTOOLCHAIN=localprevents dependencies from silently replacing the runtime under test. Windows runners install GNU make with choco. Go action caching keys include the plugin'sgo.modandgo.sum.- Dependencies: ordinary CI uses the committed lockfile (
go.mod/go.sumfor Go); localmake syncuses the existing versions. Nightly runs every plugin with the newest allowed dependencies (sync-latest) without committing the updated lock; manual dispatch can select that mode too. Python make targets useuv run --locked; Go lint, test and build use-mod=readonlyto preserve the versions selected during sync. Gosync-latestrunsgo get -u -t ./..., tidies the module and verifies its checksums. - Required checks on
main:ci-status,Check for CODEOWNERSandopengrep/scan(the last two are org-enforced workflows that run automatically on every PR), plus one approving review from a code owner;license/clajoins once the CLA app is installed. Do not add a local opengrep caller; the org one already runs. Repository setup: branch protection, thetestpypi/pypienvironments (tag policypython/*/v*) and the release-tag ruleset were configured by hand on 2026-09-09. Thepypirequired-reviewer gate was removed on 2026-10-07. - Nightly failures:
scripts/ci/nightly_report.pyopens or updates onenightlyissue per failing plugin from the job namesPython (<plugin>) / ...,Java (<plugin>) / ..., andGo (<plugin>) / ...;scripts/tests/test_nightly_report.pyfails ifci.ymlrenames those jobs. Existing Python and Java issue titles stay unchanged; Go keys usego/<plugin>so plugins in different languages cannot share an issue.
Release procedures and publication policy belong only in this file. READMEs and contributing guides cover installation, usage, and local development.
Pre-release and Public Preview plugins publish without a required-reviewer approval
gate in every language. Use the plugin's maturity, independently of whether its
version is a prerelease. Keep automated validation, immutable tags, tested-artifact
verification, and final-release eligibility checks. GA plugins retain any existing
approval policy.
Trusted publishing by ecosystem: PyPI uses OIDC trusted publishing (pypa/gh-action-pypi-publish, no stored token; PyPI cannot bind a reusable workflow, so publish jobs live inline in release-python.yml). npm supports OIDC trusted publishing (GitHub-hosted runners, npm >= 11.5.1, one publisher per package, register the calling workflow's filename; provenance is automatic for a public repo and package). Maven Central has no OIDC: Central Portal user token plus GPG signing, kept as environment-scoped secrets. Go has nothing to upload: an immutable tag plus sum.golang.org is the release.
After a successful dry run, use its full commit SHA as <tested-sha> when generating
notes with release-notes --ref and creating the draft with gh release create --target. This pins both to the tested source even if main advances. GitHub creates
the new tag when the draft is published.
Use --prerelease when the version is a prerelease or the plugin's maturity is
Pre-release. Release tags are immutable once created.
.github/workflows/release-go.yml handles every Go plugin. It runs on pushes of
go/*/v* tags, published Go GitHub Releases, and manual dispatch. Go versions use
canonical SemVer (0.1.0, 0.1.0-rc.1), with no build metadata. The shared release
tool checks the module path, maturity's first version, and forward ordering against
proxy.golang.org, including retracted releases. A re-run of the newest version is
allowed.
The currently supported vanity coordinates have no /vN suffix; v2+ requires a
module-path change before the release tooling can support it.
The full _go-plugin.yml matrix tests locked dependencies at one pinned commit,
including race detection on Ubuntu/max. Tagged runs additionally verify the
four-field vanity mapping, download the exact public version with a clean module
cache and sum.golang.org verification, compare the complete module zip and
go.mod bytes with that tested checkout, and build a clean consumer importing the
root package. Temporary proxy propagation failures are retried. The workflow uses
read-only repository permissions and creates no tags or GitHub Releases.
Creating a Go tag makes the version discoverable before CI finishes. A GitHub draft does not hide an existing tag from Go consumers. Complete the dry run before publishing the release:
- Merge the release changes to
main. Configure the vanity mapping above and include Go tags in the protected release-tag ruleset. Do not altergo.moddependency versions for a release. - Dispatch
gh workflow run release-go.yml --ref main -f tag=go/<name>/v<version>. Branch dispatches always validate, test and build without consuming a tag or fetching the proposed module. An explicit-f dry-run=truedoes the same for an existing tag ref. Use0.1.0-rc.1for the Google ADK dry run. - After the full matrix succeeds, generate notes with
uv run --project scripts --locked python scripts/release/release_tool.py release-notes --plugin-dir go/<name> --tag go/<name>/v<version> --ref <tested-sha> --output /tmp/release-notes.md. Create the draft withgh release create go/<name>/v<version> --target <tested-sha> --draft --notes-file /tmp/release-notes.md, adding--prereleasewhen required. - Review the draft and publish it with
gh release edit go/<name>/v<version> --draft=false. GitHub creates the tag on the tested commit. The release workflow repeats the matrix and verifies the public module, including the release's prerelease classification. The GitHub Release and Go version are visible before this verification finishes. A failed release requiring changed source gets the next version, such as0.1.0-rc.2. - For a proxy or consumer verification failure, re-run failed jobs or dispatch
release-go.ymlon the same tag ref with no tag input. Never move or delete the tag. A missing vanity mapping must be repaired before retrying.
.github/workflows/release-typescript.yml handles every npm plugin. Published
GitHub Releases trigger publication; tag pushes alone do not. Branch dispatches
always run a dry run, even when skip-publish is false. npm versions are canonical
SemVer without build metadata, such as 0.1.0 or 0.1.0-rc.1. Registry ordering
includes deprecated releases; the newest exact version may be verified again.
Keep the
committed development version 0.0.0.
- Configure the shared GitHub environment
npmto accept onlytypescript/*/v*tags and include that pattern in the immutable release-tag ruleset. Existing packages register a trusted publisher for GitHub repositorytemporalio/ai-integrations, workflowrelease-typescript.yml, environmentnpm, with directnpm publishallowed. No stored npm tokens are used. Thenpmenvironment and TypeScript tag protections were configured on 2026-10-15; a new coordinate's npm-side publisher still follows step 4. - Merge release changes to main. Dry-run with
gh workflow run release-typescript.yml --ref main -f tag=typescript/<name>/v<version>. The full Node/platform matrix tests the injected version at one pinned commit; Ubuntu/max packs, checks and smoke-installs the tarball and savesnpm-dist-<name>as an Actions artifact. - Generate notes with
uv run --project scripts --locked python scripts/release/release_tool.py release-notes --plugin-dir typescript/<name> --tag typescript/<name>/v<version> --ref <tested-sha> --output /tmp/release-notes.md. Create the draft withgh release create typescript/<name>/v<version> --target <tested-sha> --draft --notes-file /tmp/release-notes.md, adding--prereleasewhen required. - For a new npm coordinate only, npm requires the package to exist before a
trusted publisher can be registered. Download the successful dry run's tested
npm-dist-<name>tarball and record its run ID. Authenticate interactively with an authorized npm account and publish those exact bytes usingnpm publish <tarball> --access public --tag latest --ignore-scripts(usenextfor prerelease versions). Then register the trusted publisher withnpm trust github @temporalio/<name> --repo temporalio/ai-integrations --file release-typescript.yml --env npm --allow-publish. Complete any npm 2FA challenge interactively. Do not upload a placeholder package or add a token. - Publish the reviewed draft with
gh release edit typescript/<name>/v<version> --draft=false. GitHub creates the tag on the tested commit and starts the workflow. A later dispatch on that tag ref can retry publication. After the full matrix succeeds, the inline OIDC job publishes the tested tarball tolatestfor final versions ornextfor prereleases, verifies npm's SHA-512 integrity against the tested tarball, and installs that public version in a clean consumer. npm verifies the downloaded bytes against that integrity. It leaves the reviewed GitHub Release unchanged. After the bootstrap upload in step 4, dispatchgh workflow run release-typescript.yml --ref typescript/<name>/v<version> -f recover-run=<dry-run-id>. This verifies the exact tarball already uploaded, even if the automatic release run rebuilt different bytes. Recovery requires that the source dry run or release run completed at the tag's exact main-reachable commit, passed version validation and every matrix job, and retains one unexpirednpm-dist-<name>artifact. It downloads that artifact without rebuilding. The downloaded package must have the release's name and version and be public before any upload. - Re-run failed jobs after an upload. An existing version is skipped only when
its registry integrity matches the tested tarball. For a fresh run,
dispatch on the same tag with
-f recover-run=<original-run-id>to reuse those tested bytes. The published GitHub Release and normal registry policy are still required. Recovery is accepted only on a tag ref; branch dispatches remain dry runs. A full rerun can replace its saved artifact, so preserve a downloaded copy after manually publishing it and prefer re-running failed jobs. If the original artifact is unavailable or its bytes differ from npm, fix forward with a new version; never move a tag or replace an uploaded artifact.
Every Python plugin uses the testpypi and pypi GitHub environments without required reviewers, regardless of maturity. Both environments allow the python/*/v* tag pattern and main for validated publication recovery. Publishing the GitHub Release starts the release pipeline; eligible versions upload to PyPI automatically after the full test matrix and TestPyPI verification succeed. Trusted publishers remain bound to their existing environment names.
Version policy (release_tool.py check-version-policy; version ordering is evaluated against pypi.org, test.pypi.org, or Maven Central's metadata): a coordinate with no published release must start at exactly 1.0.0 (generally-available), 0.1.0 (public-preview), or 0.0.1 (pre-release), pre-releases of that version allowed; an existing coordinate must be strictly greater than its highest published version, yanked releases included. Maturity is independent of PEP 440 version status: a Pre-release plugin can publish a final version such as 0.0.1 to PyPI. TestPyPI versions also move forward. A version already staged on TestPyPI, or already the newest release on pypi.org, only produces a warning (a re-run after an upload is the normal recovery path); an older staged version is rejected. Each Python smoke job proves the index serves exactly the artifacts this run built, none yanked (verify-index-files). Java smoke jobs compare staged and public Maven artifacts byte for byte with the tested files.
Runbook for python/<name>:
- Merge every code and dependency change intended for the release. Do not change the committed
0.0.0development version. - Choose a canonical PEP 440 version and dry-run it on
main:gh workflow run release-python.yml --ref main -f tag=python/<name>/v<version>. The workflow injects the tag version into each checkout, runs policy validation, the full test matrix and the artifact build, but uploads nothing and consumes no tag.-f skip-publish=truedoes the same for a dispatch on an existing tag ref. - Generate notes with
uv run --project scripts --locked python scripts/release/release_tool.py release-notes --plugin-dir python/<name> --tag python/<name>/v<version> --ref <tested-sha> --output /tmp/release-notes.md. Create the draft withgh release create python/<name>/v<version> --target <tested-sha> --draft --notes-file /tmp/release-notes.md, adding--prereleasewhen required. If GitHub release assets are wanted, attach the tested distributions downloaded from the successful dry run before publication; the workflow retains its distributions as Actions artifacts. - Publish the reviewed GitHub Release using the GitHub UI or
gh release edit python/<name>/v<version> --draft=false. GitHub creates the protected tag on the tested commit. Itsrelease: publishedevent startsrelease-python.ymlfor Python tags; pushing a tag alone does not start publication. Publication with a workflow'sGITHUB_TOKENdoes not trigger this event workflow; use a human's session or the manual dispatch fallback on the tag. The GitHub Release is now visible even if package publication later fails. release-python.ymlvalidates the main-reachable tag, version policy, published release and GitHub pre-release classification, injects the tag's version, and runs the full test matrix (its ubuntu dist cell builds, checks and smoke-tests the wheel and sdist). It publishes those tested artifacts to TestPyPI (environmenttestpypi), proves TestPyPI serves exactly those files, and smoke-installs from TestPyPI in a clean project. Eligible versions then publish automatically to PyPI (environmentpypi), with the published release linked from the deployment; the workflow repeats the artifact proof and smoke there. PEP 440 pre-releases go to TestPyPI only unless a tag dispatch usespublish-prerelease-to-pypi; GitHub's pre-release flag does not control index routing. The workflow reads the GitHub Release without changing its notes or assets.- If a job fails after an upload, use "Re-run failed jobs" on that run:
prepare's outputs and the tested artifact survive, the upload is skipped, and the smoke jobs verify the served files. If the tagged workflow or tooling itself needs a repair, merge the repair first, then dispatch onmainwith-f tag=python/<name>/v<version> -f recover-run=<original-release-run-id>. Recovery validates that the completed source run is a release-event run (or a legacy tag-push run) for the exact main-reachable tag, passed version validation and every test-matrix job, and retains an unexpired distribution artifact. A published GitHub Release for the tag must exist, and package manifests must still match the tag. It downloads those tested bytes without rebuilding; the normal ref restrictions, registry ordering and file-hash checks still apply. Recovery requires amainbranch deployment policy in thetestpypiandpypienvironments. A fresh dispatch on the tag also passes the version policy (the newest published version is treated as a re-run, with a warning) but rebuilds the artifacts, andverify-index-filesfails if the rebuild is not byte-identical (a different uv version stamps itsGeneratorinto the wheel). If the artifacts themselves must change, fix forward with the nextrcN; uploaded files are immutable and tags are never moved.
Runbook for java/<name>:
The shared workflow starts when the reviewed GitHub Release is published. Tag pushes alone do not start staging. The workflow verifies the published release before signing or uploading and retains tested distributions and deployment state as Actions artifacts.
-
Merge the changes intended for the release. Keep the committed
0.0.0development version. -
Configure shared environments
maven-central-stagingandmaven-central, both accepting tagsjava/*/v*only, with no required reviewers. Every plugin usesmaven-central-stagingfor private staging andmaven-centralfor public publication, so their deployment histories track the two phases separately. The sharedmaven-central-approvalenvironment accepts the same tag pattern, requires an@temporalio/ai-sdkreviewer and prevents self-review; it holds no secrets. GA publication must pass this approval job first. Only recognized Pre-release and Public Preview maturities skip it; missing or unknown maturity requires approval. Extend the immutable release-tag ruleset to Java without relaxing its Python rules. Credential installation remains a maintainer setup step. -
In staging, install environment secrets
CENTRAL_USERNAME,CENTRAL_PASSWORD(a Central Portal user-token pair),GPG_PRIVATE_KEY(armored private key or base64-encoded secret keyring), andGPG_PASSPHRASE; set environment variableGPG_FINGERPRINTto the full public fingerprint. Production needs only the Central token pair, with access to the staging deployment. Use credentials from the same publishing account in both. GitHub cannot export existing secret values; an authorized maintainer must install them from the credential source. Publish the public key to a supported keyserver before Central validation; see Sonatype's GPG requirements. -
Dry-run the candidate on main:
gh workflow run release-java.yml --ref main -f tag=java/spring-ai/v0.1.0-RC1. The full compatibility matrix tests the injected version; its primary Ubuntu/max cell builds and verifies the five Maven artifacts and installs a clean consumer. A branch dispatch never signs, uploads, or creates a release. -
Generate notes with
uv run --project scripts --locked python scripts/release/release_tool.py release-notes --plugin-dir java/spring-ai --tag java/spring-ai/v0.1.0-RC1 --ref <tested-sha> --output /tmp/release-notes.md. Create the draft withgh release create java/spring-ai/v0.1.0-RC1 --target <tested-sha> --draft --prerelease --notes-file /tmp/release-notes.md. Review the notes and attach any wanted tested distributions from the dry run before publication. Publish withgh release edit java/spring-ai/v0.1.0-RC1 --draft=false; GitHub creates the tag on the tested commit and starts the workflow. The full matrix runs again, then the shared workflow signs the tested bytes, adds checksums, uploads aUSER_MANAGEDbundle to Central Portal, waits forVALIDATED, compares all staged files byte for byte, and installs a clean consumer from the authenticated staging endpoint. Candidates remain privately staged in Central Portal. The GitHub Release is visible before this validation finishes. The signed bundle and deployment metadata are retained as Actions artifacts; the workflow leaves the reviewed release unchanged. -
Publish the tested Maven artifacts. The workflow publishes the already verified deployment automatically for non-GA plugins; GA plugins require
maven-central-approvalapproval first. It waits forPUBLISHED, proves Maven Central serves the same files, and installs a clean consumer after the GitHub Release is published. Candidates retain the private staging behavior above. Publication and recovery share a 30-minute polling budget within the 45-minute job; a polling timeout preserves the deployment for a failed-job rerun. -
After an upload failure, use Re-run failed jobs. The workflow preserves the signed bundle, upload intent, and deployment ID even if validation or the consumer fails. It reuses those signatures and never retries an ambiguous upload POST. Release-event reruns and legacy tag-push reruns inspect every earlier attempt. Once the staging command may have run, recovery requires the saved state; a missing or failed state download stops the job without signing, uploading, or replacing the saved artifact. If all earlier attempts skipped staging, a rerun may perform the first upload. A fresh publishing dispatch on the same tag requires
-f deployment-id=<Portal UUID>; recovery downloads the existing signed files and rejects any rebuilt-byte mismatch or deployment containing additional coordinates. If an upload response was lost, inspect Portal for its ID before retrying. Publishing dispatches cannot start a new upload, even when Portal is still validating the original upload; the initial upload runs on GitHub Release publication. A dispatch withskip-publish=trueremains a credential-free dry run and does not require a deployment ID. Never move a tag or replace uploaded files: a changed candidate gets the nextRCN. Portal does not expose TestPyPI-style staging version enumeration; public ordering is checked against Maven Central and an existing validated staged version requires explicit recovery.
The Java pipeline uses the Central Portal Publisher API directly. Maven Central remains the public host. Snapshot publishing is not configured.
cd python/<name> && make sync && make lint && make test && make build
uv run --project scripts --locked pytest scripts/tests -q # tooling script tests
uv run --project scripts python scripts/ci/check_conventions.py
uv run --project scripts python scripts/ci/detect_changes.py --dry-run --base origin/main
uv run --project scripts python scripts/ci/check_wheel.py --plugin-dir python/<name> --dist python/<name>/dist