From b55d3c3b90e064666d03f26a916ed159864b1692 Mon Sep 17 00:00:00 2001 From: Mike Goldsmith Date: Fri, 15 May 2026 13:31:56 +0100 Subject: [PATCH 1/9] docs: add RELEASING.md Documents the per-package independent release model used by this repo, the manual end-to-end release steps with the existing build scripts, and the towncrier-based changelog flow. Calls out missing pieces explicitly: PyPI names are still TBD per package, and contrib's automated release workflows haven't been ported yet. Both tracked in the bootstrap issue. Assisted-by: Claude Opus 4.7 (1M context) --- RELEASING.md | 133 +++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 133 insertions(+) create mode 100644 RELEASING.md diff --git a/RELEASING.md b/RELEASING.md new file mode 100644 index 00000000..49f88856 --- /dev/null +++ b/RELEASING.md @@ -0,0 +1,133 @@ +# Release process + +This repo follows an **independent per-package release model**: every +publishable package owns its own version, `CHANGELOG.md`, and release cadence. +There is no coordinated bulk release across packages. The packages listed in +`[exclude_release]` of [`eachdist.ini`](eachdist.ini) are excluded from any +bulk-release tooling carried over from `opentelemetry-python-contrib`. + +> [!IMPORTANT] +> The CI workflows for fully automated releases (`prepare-release`, +> `package-release`, etc.) have **not yet been ported** from +> `opentelemetry-python-contrib`. Until they are, releases are performed +> manually using the steps below. Tracked in #15. + +## Publishable packages + +| Package | PyPI name | +| --- | --- | +| `opentelemetry-instrumentation-anthropic` | _TBD_ | +| `opentelemetry-instrumentation-claude-agent-sdk` | _TBD_ | +| `opentelemetry-instrumentation-google-genai` | _TBD_ | +| `opentelemetry-instrumentation-langchain` | _TBD_ | +| `opentelemetry-instrumentation-openai-agents-v2` | _TBD_ | +| `opentelemetry-instrumentation-openai-v2` | _TBD_ | +| `opentelemetry-instrumentation-weaviate` | _TBD_ | +| `opentelemetry-util-genai` | _TBD_ | + +> [!NOTE] +> The PyPI names will differ from the names used in +> `opentelemetry-python-contrib`. Until each name is chosen and reserved on +> PyPI, packages cannot be published from this repo. Tracked in #15. + +## Releasing a package + +The steps below cover releasing a single package end-to-end. Repeat per +package as needed. + +### 1. Prepare the changelog + +Changelog entries are managed with [towncrier](https://towncrier.readthedocs.io/). +Each PR adds a fragment under `/.changelog/.`; at +release time those fragments are compiled into the package's `CHANGELOG.md`. + +From the package directory, build the new release section into `CHANGELOG.md`: + +```sh +cd +uv run towncrier build --version +``` + +This consumes every fragment under `./.changelog/` and inserts a new +`## Version ()` block. Commit the resulting changes. + +> [!NOTE] +> Several `CHANGELOG.md` files have pre-existing entries under the static +> `## Unreleased` section that pre-date towncrier. These entries are **not** +> picked up by `towncrier build`. Fold them by hand into the new release +> block before committing. + +### 2. Bump the version + +Each package's version lives in +`/src/.../version.py` (path depends on the package). Bump the +version (e.g. drop the `.dev` suffix for the release, then bump it back +afterwards for the next development cycle). + +### 3. Tag and build + +Tags follow the format `==` (matches the contrib +convention, used by [`scripts/build_a_package.sh`](scripts/build_a_package.sh)). + +```sh +git tag == +git push --tags + +PACKAGE_NAME= VERSION= ./scripts/build_a_package.sh +``` + +`build_a_package.sh` writes the `.tar.gz` and `.whl` into `dist/`. + +### 4. Publish to PyPI + +```sh +twine upload --skip-existing dist/* +``` + +> [!IMPORTANT] +> Publishing requires PyPI maintainer rights on the package. The token / API +> credential setup is not yet automated. Maintainers should publish from a +> machine that already has `~/.pypirc` configured, or via a dedicated CI +> workflow once one exists. + +### 5. Create a GitHub release + +```sh +gh release create == \ + --title " " \ + --notes-file <(awk '/^## Version /,/^## /{print}' /CHANGELOG.md | sed '$d') +``` + +Or use the GitHub UI: navigate to the new tag and click "Draft release", +pasting in the relevant section from the package's `CHANGELOG.md`. + +### 6. Bump to the next development version + +Open a follow-up PR bumping the package's `version.py` to the next +`X.Yb.dev` (or `X.Y..dev` for stable components), so subsequent +fragments accumulate against the next release. + +## Releasing a dev version to claim the PyPI namespace + +When introducing a new package, release the current development version +under the `opentelemetry` PyPI org to prevent name-squatting. Do this +shortly after the introductory PR lands on `main`. + +## Version numbering + +- **Unstable components** (everything currently in this repo): versions look + like `0.Yb0.dev` on `main`, `0.Yb0` at release time, then bump to + `0.{Y+1}b0.dev`. +- **Stable components**: versions look like `X.Y.0.dev` on `main`, `X.Y.0` + at release time, then bump to `X.{Y+1}.0.dev`. None of the current + packages are stable. + +## What's still missing + +Tracked in #15: + +- New PyPI names for each package +- `prepare-release` workflow (cuts the release PR and bumps versions) +- `package-release` workflow (tags, builds, publishes, opens GitHub release) +- `backport` workflow for patch releases on long-term release branches +- Patch release tooling once a package has a long-term release branch From 8809ed6e68b0ef126c07152c5254a660b71d339d Mon Sep 17 00:00:00 2001 From: Mike Goldsmith Date: Fri, 15 May 2026 13:44:55 +0100 Subject: [PATCH 2/9] add note about towncrier dependency Addresses Copilot review feedback: the changelog steps depend on #16 (towncrier setup) which hasn't merged yet. Make the dependency explicit at the top of the doc so it's obvious to any reviewer. Assisted-by: Claude Opus 4.7 (1M context) --- RELEASING.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/RELEASING.md b/RELEASING.md index 49f88856..2b99852a 100644 --- a/RELEASING.md +++ b/RELEASING.md @@ -12,6 +12,11 @@ bulk-release tooling carried over from `opentelemetry-python-contrib`. > `opentelemetry-python-contrib`. Until they are, releases are performed > manually using the steps below. Tracked in #15. +> [!NOTE] +> The changelog steps below assume the towncrier setup introduced in #16. +> That PR adds `towncrier` to the dev dependencies and per-package +> `[tool.towncrier]` config. This document should only land after #16. + ## Publishable packages | Package | PyPI name | From 9c439ade90d5f465cdf4519afc9654da4d8c1b9f Mon Sep 17 00:00:00 2001 From: Mike Goldsmith Date: Fri, 15 May 2026 13:46:56 +0100 Subject: [PATCH 3/9] revert inline towncrier note The note belongs in the PR description as a merge-ordering signal, not in the doc itself where it would have to be removed once #16 lands. Assisted-by: Claude Opus 4.7 (1M context) --- RELEASING.md | 5 ----- 1 file changed, 5 deletions(-) diff --git a/RELEASING.md b/RELEASING.md index 2b99852a..49f88856 100644 --- a/RELEASING.md +++ b/RELEASING.md @@ -12,11 +12,6 @@ bulk-release tooling carried over from `opentelemetry-python-contrib`. > `opentelemetry-python-contrib`. Until they are, releases are performed > manually using the steps below. Tracked in #15. -> [!NOTE] -> The changelog steps below assume the towncrier setup introduced in #16. -> That PR adds `towncrier` to the dev dependencies and per-package -> `[tool.towncrier]` config. This document should only land after #16. - ## Publishable packages | Package | PyPI name | From b487c37930767483f584ad4b2d043f6bb98d93e5 Mon Sep 17 00:00:00 2001 From: Mike Goldsmith Date: Tue, 19 May 2026 14:00:05 +0100 Subject: [PATCH 4/9] port package release workflows from contrib Replaces the manual-release documentation that was originally on this branch with the contrib release workflows for independently released packages. Every publishable package in this repo is independent, so only the per-package workflows apply (no coordinated bulk-release flow). Workflows ported: - [Package] Prepare release: cuts the release branch package-release//v..x; opens PRs to bump version and build the changelog via towncrier on both the release branch and main. - [Package] Prepare patch release: opens a patch-bump PR against an existing package release branch. - [Package] Release: publishes the built wheel to PyPI, creates the GitHub release, opens a back-merge PR copying the changelog to main. Supporting scripts ported: - scripts/generate_release_notes.sh - scripts/merge_changelog_to_main.sh - .github/scripts/use-cla-approved-github-bot.sh Adaptations from contrib: - Workflow package dropdowns list the 8 publishable packages in this repo. - backport.yml not ported; no release branches exist yet, will revisit when one does. - PyPI publishing uses a token (secrets.pypi_password), matching contrib. Tracked in #15 to migrate to PyPI trusted publishing once package names are decided and per-package publishers can be set up. RELEASING.md rewritten as a thin doc pointing at the workflows, listing the otelbot / PyPI / branch protection prerequisites, and documenting the pre-existing static "## Unreleased" entries that need manual folding on the first towncrier release per package. Assisted-by: Claude Opus 4.7 (1M context) --- .../scripts/use-cla-approved-github-bot.sh | 4 + .../package-prepare-patch-release.yml | 129 ++++++++++ .github/workflows/package-prepare-release.yml | 219 +++++++++++++++++ .github/workflows/package-release.yml | 158 +++++++++++++ RELEASING.md | 221 ++++++++---------- scripts/generate_release_notes.sh | 26 +++ scripts/merge_changelog_to_main.sh | 50 ++++ 7 files changed, 682 insertions(+), 125 deletions(-) create mode 100755 .github/scripts/use-cla-approved-github-bot.sh create mode 100644 .github/workflows/package-prepare-patch-release.yml create mode 100644 .github/workflows/package-prepare-release.yml create mode 100644 .github/workflows/package-release.yml create mode 100755 scripts/generate_release_notes.sh create mode 100755 scripts/merge_changelog_to_main.sh diff --git a/.github/scripts/use-cla-approved-github-bot.sh b/.github/scripts/use-cla-approved-github-bot.sh new file mode 100755 index 00000000..fc47865a --- /dev/null +++ b/.github/scripts/use-cla-approved-github-bot.sh @@ -0,0 +1,4 @@ +#!/bin/bash -e + +git config user.name otelbot +git config user.email 197425009+otelbot@users.noreply.github.com \ No newline at end of file diff --git a/.github/workflows/package-prepare-patch-release.yml b/.github/workflows/package-prepare-patch-release.yml new file mode 100644 index 00000000..eebdf9ec --- /dev/null +++ b/.github/workflows/package-prepare-patch-release.yml @@ -0,0 +1,129 @@ +name: "[Package] Prepare patch release" +on: + workflow_dispatch: + inputs: + package: + type: choice + options: + - opentelemetry-instrumentation-anthropic + - opentelemetry-instrumentation-claude-agent-sdk + - opentelemetry-instrumentation-google-genai + - opentelemetry-instrumentation-langchain + - opentelemetry-instrumentation-openai-agents-v2 + - opentelemetry-instrumentation-openai-v2 + - opentelemetry-instrumentation-weaviate + - opentelemetry-util-genai + description: 'Package to be released' + required: true +permissions: + contents: read +run-name: "[Package][${{ inputs.package }}] Prepare patch release" + +jobs: + prepare-patch-release: + permissions: + contents: write # required for pushing branches + pull-requests: write # required for creating pull requests + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Verify prerequisites + run: | + if [[ $GITHUB_REF_NAME != package-release/${{ inputs.package }}/v* ]]; then + echo this workflow should only be run against package-release/${{ inputs.package }}* branches, but is running on $GITHUB_REF_NAME + exit 1 + fi + + path=./$(./scripts/eachdist.py find-package --package ${{ inputs.package }}) + changelog=$path/CHANGELOG.md + + if [ ! -f $changelog ]; then + echo "missing $changelog file" + exit 1 + fi + + version=$(./scripts/eachdist.py version --package ${{ inputs.package }}) + + version_file=$(find $path -type f -path "**/version.py") + file_count=$(echo "$version_file" | wc -l) + + if [ "$file_count" -ne 1 ]; then + echo "Error: expected one version file, found $file_count" + echo "$version_file" + exit 1 + fi + + if [[ $version =~ ^([0-9]+)\.([0-9]+)\.([0-9]+) ]]; then + # 1.2.3 or 1.2.3rc1 + major="${BASH_REMATCH[1]}" + minor="${BASH_REMATCH[2]}" + patch="${BASH_REMATCH[3]}" + next_version="$major.$minor.$((patch + 1))" + release_branch_name="package-release/${{ inputs.package }}/v$major.$minor.x" + elif [[ $version =~ ^([0-9]+)\.([0-9]+)b([0-9]+)$ ]]; then + # 0.1b1 + major="${BASH_REMATCH[1]}" + minor="${BASH_REMATCH[2]}" + patch="${BASH_REMATCH[3]}" + next_version="$major.${minor}b$((patch + 1))" + release_branch_name="package-release/${{ inputs.package }}/v$major.${minor}bx" + else + echo "unexpected version: '$version'" + exit 1 + fi + + if [[ $GITHUB_REF_NAME != $release_branch_name ]]; then + echo this workflow should only be run against $release_branch_name branch, but is running on $GITHUB_REF_NAME + exit 1 + fi + + echo "PACKAGE_NAME=${{ inputs.package }}" >> $GITHUB_ENV + echo "VERSION=$version" >> $GITHUB_ENV + echo "NEXT_VERSION=$next_version" >> $GITHUB_ENV + echo "CHANGELOG=$changelog" >> $GITHUB_ENV + echo "VERSION_FILE=$version_file" >> $GITHUB_ENV + + - name: Update version + run: | + # replace the version in the version file (1.2.3 -> 1.2.4) + sed -i -E "s/__version__\s*=\s*\"${VERSION}\"/__version__ = \"${NEXT_VERSION}\"/g" $VERSION_FILE + + - name: Set up Python 3.10 + uses: actions/setup-python@v5 + with: + python-version: '3.10' + - name: Install tox + run: pip install tox + - name: run tox + run: tox -e generate + + - name: Install towncrier + run: pip install towncrier==25.8.0 + + - name: Generate changelog + run: towncrier build --yes --version "$NEXT_VERSION" --dir "$(dirname $CHANGELOG)" + + - name: Use CLA approved github bot + run: .github/scripts/use-cla-approved-github-bot.sh + + - uses: actions/create-github-app-token@df432ceedc7162793a195dd1713ff69aefc7379e # v2.0.6 + id: otelbot-token + with: + app-id: ${{ vars.OTELBOT_APP_ID }} + private-key: ${{ secrets.OTELBOT_PRIVATE_KEY }} + + - name: Create pull request + env: + # not using secrets.GITHUB_TOKEN since pull requests from that token do not run workflows + GITHUB_TOKEN: ${{ steps.otelbot-token.outputs.token }} + run: | + message="Prepare patch release for ${PACKAGE_NAME} v${NEXT_VERSION}" + branch="otelbot/patch-${PACKAGE_NAME}-version-to-v${NEXT_VERSION}" + + git commit -a -m "$message" + git push origin HEAD:$branch + gh pr create --title "[$GITHUB_REF_NAME] $message" \ + --body "$message." \ + --head $branch \ + --base $GITHUB_REF_NAME diff --git a/.github/workflows/package-prepare-release.yml b/.github/workflows/package-prepare-release.yml new file mode 100644 index 00000000..f1368aac --- /dev/null +++ b/.github/workflows/package-prepare-release.yml @@ -0,0 +1,219 @@ +name: "[Package] Prepare release" +on: + workflow_dispatch: + inputs: + package: + type: choice + options: + - opentelemetry-instrumentation-anthropic + - opentelemetry-instrumentation-claude-agent-sdk + - opentelemetry-instrumentation-google-genai + - opentelemetry-instrumentation-langchain + - opentelemetry-instrumentation-openai-agents-v2 + - opentelemetry-instrumentation-openai-v2 + - opentelemetry-instrumentation-weaviate + - opentelemetry-util-genai + description: 'Package to be released' + required: true + +permissions: + contents: read + +run-name: "[Package][${{ inputs.package }}] Prepare release" +jobs: + prereqs: + runs-on: ubuntu-latest + outputs: + version: ${{ steps.verify.outputs.version }} + changelog: ${{ steps.verify.outputs.changelog }} + version_file: ${{ steps.verify.outputs.version_file }} + release_branch_name: ${{ steps.verify.outputs.release_branch_name }} + next_version: ${{ steps.verify.outputs.next_version }} + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - id: verify + name: Verify prerequisites + run: | + if [[ $GITHUB_REF_NAME != main ]]; then + echo this workflow should only be run against main + exit 1 + fi + + path=./$(./scripts/eachdist.py find-package --package ${{ inputs.package }}) + changelog=$path/CHANGELOG.md + + if [ ! -f $changelog ]; then + echo "missing $changelog file" + exit 1 + fi + + version_dev=$(./scripts/eachdist.py version --package ${{ inputs.package }}) + + if [[ ! $version_dev =~ ^([0-9]+)\.([0-9]+)[\.|b]{1}([0-9]+).*.dev$ ]]; then + echo "unexpected version: $version" + exit 1 + fi + + version=${version_dev%.dev} + + version_file=$(find $path -type f -path "**/version.py") + file_count=$(echo "$version_file" | wc -l) + + if [ "$file_count" -ne 1 ]; then + echo "Error: expected one version file, found $file_count" + echo "$version_file" + exit 1 + fi + + if [[ $version =~ ^([0-9]+)\.([0-9]+)\.([0-9]+) ]]; then + # 1.2.3 or 1.2.3rc1 + major="${BASH_REMATCH[1]}" + minor="${BASH_REMATCH[2]}" + release_branch_name="package-release/${{ inputs.package }}/v$major.$minor.x" + next_version="$major.$((minor + 1)).0" + elif [[ $version =~ ^([0-9]+)\.([0-9]+)b([0-9]+)$ ]]; then + # 0.1b1 + major="${BASH_REMATCH[1]}" + minor="${BASH_REMATCH[2]}" + release_branch_name="package-release/${{ inputs.package }}/v$major.${minor}bx" + next_version="$major.$((minor + 1))b0" + else + echo "unexpected version: '$version'" + exit 1 + fi + + echo "version=$version" >> $GITHUB_OUTPUT + echo "changelog=$changelog" >> $GITHUB_OUTPUT + echo "version_file=$version_file" >> $GITHUB_OUTPUT + echo "release_branch_name=$release_branch_name" >> $GITHUB_OUTPUT + echo "next_version=$next_version" >> $GITHUB_OUTPUT + + create-pull-request-against-release-branch: + runs-on: ubuntu-latest + needs: prereqs + permissions: + contents: write # required for pushing branches + pull-requests: write # required for creating pull requests + steps: + - uses: actions/checkout@v4 + + - name: Set environment variables + run: | + echo "RELEASE_BRANCH_NAME=${{ needs.prereqs.outputs.release_branch_name }}" >> $GITHUB_ENV + echo "VERSION=${{ needs.prereqs.outputs.version }}" >> $GITHUB_ENV + echo "CHANGELOG=${{ needs.prereqs.outputs.changelog }}" >> $GITHUB_ENV + echo "VERSION_FILE=${{ needs.prereqs.outputs.version_file }}" >> $GITHUB_ENV + echo "PACKAGE_NAME=${{ github.event.inputs.package }}" >> $GITHUB_ENV + + - name: Create package release branch + run: | + git push origin HEAD:$RELEASE_BRANCH_NAME + + - name: Update package version + run: | + # replace the version in the version file (1.2.3dev -> 1.2.3) + sed -i -E "s/__version__\s*=\s*\"${VERSION}\.dev\"/__version__ = \"${VERSION}\"/g" $VERSION_FILE + + - name: Set up Python 3.10 + uses: actions/setup-python@v5 + with: + python-version: '3.10' + - name: Install tox + run: pip install tox + - name: run tox + run: tox -e generate + + - name: Install towncrier + run: pip install towncrier==25.8.0 + + - name: Generate changelog + run: towncrier build --yes --version "$VERSION" --dir "$(dirname $CHANGELOG)" + + - name: Use CLA approved github bot + run: .github/scripts/use-cla-approved-github-bot.sh + + - uses: actions/create-github-app-token@df432ceedc7162793a195dd1713ff69aefc7379e # v2.0.6 + id: otelbot-token + with: + app-id: ${{ vars.OTELBOT_APP_ID }} + private-key: ${{ secrets.OTELBOT_PRIVATE_KEY }} + + - name: Create pull request against the release branch + env: + # not using secrets.GITHUB_TOKEN since pull requests from that token do not run workflows + GITHUB_TOKEN: ${{ steps.otelbot-token.outputs.token }} + run: | + message="Prepare release for ${PACKAGE_NAME} v${VERSION}" + branch="otelbot/prepare-${RELEASE_BRANCH_NAME}" + + git commit -a -m "$message" + git push origin HEAD:$branch + gh pr create --title "[$RELEASE_BRANCH_NAME] $message" \ + --body "$message." \ + --head $branch \ + --base $RELEASE_BRANCH_NAME + + create-pull-request-against-main: + runs-on: ubuntu-latest + needs: prereqs + permissions: + contents: write # required for pushing branches + pull-requests: write # required for creating pull requests + steps: + - uses: actions/checkout@v4 + + - name: Set environment variables + run: | + echo "PACKAGE_NAME=${{ github.event.inputs.package }}" >> $GITHUB_ENV + echo "VERSION=${{ needs.prereqs.outputs.version }}" >> $GITHUB_ENV + echo "VERSION_FILE=${{ needs.prereqs.outputs.version_file }}" >> $GITHUB_ENV + echo "NEXT_VERSION=${{ needs.prereqs.outputs.next_version }}" >> $GITHUB_ENV + echo "CHANGELOG=${{ needs.prereqs.outputs.changelog }}" >> $GITHUB_ENV + + - name: Update version + run: | + # replace the version in the version file (1.2.3dev -> 1.3.3.dev) + sed -i -E "s/__version__\s*=\s*\"${VERSION}\.dev\"/__version__ = \"${NEXT_VERSION}.dev\"/g" $VERSION_FILE + + - name: Set up Python 3.10 + uses: actions/setup-python@v5 + with: + python-version: '3.10' + - name: Install tox + run: pip install tox + - name: run tox + run: tox -e generate + + - name: Install towncrier + run: pip install towncrier==25.8.0 + + - name: Generate changelog + run: towncrier build --yes --version "$VERSION" --dir "$(dirname $CHANGELOG)" + + - name: Use CLA approved github bot + run: .github/scripts/use-cla-approved-github-bot.sh + + - uses: actions/create-github-app-token@df432ceedc7162793a195dd1713ff69aefc7379e # v2.0.6 + id: otelbot-token-main + with: + app-id: ${{ vars.OTELBOT_APP_ID }} + private-key: ${{ secrets.OTELBOT_PRIVATE_KEY }} + + - name: Create pull request against main + env: + # not using secrets.GITHUB_TOKEN since pull requests from that token do not run workflows + GITHUB_TOKEN: ${{ steps.otelbot-token-main.outputs.token }} + run: | + message="Update ${PACKAGE_NAME} version to v${NEXT_VERSION}" + body="Update \`${PACKAGE_NAME}\` version to v\`${NEXT_VERSION}\`." + branch="otelbot/update-${PACKAGE_NAME}-version-to-v${NEXT_VERSION}" + + git commit -a -m "$message" + git push origin HEAD:$branch + gh pr create --title "$message" \ + --body "$body" \ + --head $branch \ + --base main diff --git a/.github/workflows/package-release.yml b/.github/workflows/package-release.yml new file mode 100644 index 00000000..18c24fb8 --- /dev/null +++ b/.github/workflows/package-release.yml @@ -0,0 +1,158 @@ +name: "[Package] Release" +on: + workflow_dispatch: + inputs: + package: + type: choice + options: + - opentelemetry-instrumentation-anthropic + - opentelemetry-instrumentation-claude-agent-sdk + - opentelemetry-instrumentation-google-genai + - opentelemetry-instrumentation-langchain + - opentelemetry-instrumentation-openai-agents-v2 + - opentelemetry-instrumentation-openai-v2 + - opentelemetry-instrumentation-weaviate + - opentelemetry-util-genai + description: 'Package to be released' + required: true +permissions: + contents: read +run-name: "[Package][${{ inputs.package }}] Release" +jobs: + release: + permissions: + contents: write # required for creating releases + pull-requests: write # required for creating pull requests + runs-on: ubuntu-latest + steps: + - run: | + if [[ $GITHUB_REF_NAME != package-release/${{ inputs.package }}* ]]; then + echo this workflow should only be run against package-release/${{ inputs.package }}* branches + exit 1 + fi + + - uses: actions/checkout@v4 + + - name: Set environment variables + run: | + version=$(./scripts/eachdist.py version --package ${{ inputs.package }}) + if [[ $version =~ ^([0-9]+)\.([0-9]+)\.([0-9]+) ]]; then + major="${BASH_REMATCH[1]}" + minor="${BASH_REMATCH[2]}" + patch="${BASH_REMATCH[3]}" + if [[ $patch != 0 ]]; then + prior_version_when_patch="${major}.${minor}.$((patch - 1))" + fi + elif [[ $version =~ ^([0-9]+)\.([0-9]+)b([0-9]+)$ ]]; then + major="${BASH_REMATCH[1]}" + minor="${BASH_REMATCH[2]}" + patch="${BASH_REMATCH[3]}" + + if [[ $patch != 0 ]]; then + prior_version_when_patch="${major}.${minor}b$((patch - 1))" + fi + else + echo "unexpected version: $version" + exit 1 + fi + + path=$(./scripts/eachdist.py find-package --package ${{ inputs.package }}) + echo "CHANGELOG=./$path/CHANGELOG.md" >> $GITHUB_ENV + echo "PACKAGE_NAME=${{ inputs.package }}" >> $GITHUB_ENV + echo "VERSION=$version" >> $GITHUB_ENV + echo "RELEASE_TAG=${{ inputs.package }}==$version" >> $GITHUB_ENV + echo "PRIOR_VERSION_WHEN_PATCH=$prior_version_when_patch" >> $GITHUB_ENV + + # check out main branch to verify there won't be problems with merging the change log + # at the end of this workflow + - uses: actions/checkout@v4 + with: + ref: main + + - name: Check that change log update was merged to main + run: | + if [[ -z $PRIOR_VERSION_WHEN_PATCH ]]; then + # not making a patch release + if ! grep --quiet "^## Version ${VERSION}" ${CHANGELOG}; then + echo the pull request generated by prepare-release-branch.yml needs to be merged first + exit 1 + fi + fi + + # back to the release branch + - uses: actions/checkout@v4 + + # next few steps publish to pypi + - uses: actions/setup-python@v5 + with: + python-version: '3.10' + + - name: Build wheels + run: ./scripts/build_a_package.sh + + - name: Install twine + run: | + pip install twine + + - name: Publish to PyPI + env: + TWINE_USERNAME: '__token__' + TWINE_PASSWORD: ${{ secrets.pypi_password }} + run: | + twine upload --skip-existing --verbose dist/* + + - name: Generate release notes + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: ./scripts/generate_release_notes.sh + + - name: Create GitHub release + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + gh release create --target $GITHUB_REF_NAME \ + --title "${PACKAGE_NAME} ${VERSION}" \ + --notes-file /tmp/release-notes.txt \ + --discussion-category announcements \ + $RELEASE_TAG + + - uses: actions/checkout@v4 + with: + ref: main + + - name: Copy change log updates to main + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: ./scripts/merge_changelog_to_main.sh + + - name: Use CLA approved github bot + run: .github/scripts/use-cla-approved-github-bot.sh + + - uses: actions/create-github-app-token@df432ceedc7162793a195dd1713ff69aefc7379e # v2.0.6 + id: otelbot-token + with: + app-id: ${{ vars.OTELBOT_APP_ID }} + private-key: ${{ secrets.OTELBOT_PRIVATE_KEY }} + + - name: Create pull request against main + env: + # not using secrets.GITHUB_TOKEN since pull requests from that token do not run workflows + GITHUB_TOKEN: ${{ steps.otelbot-token.outputs.token }} + run: | + message="Copy changelog updates from $GITHUB_REF_NAME" + body="Copy changelog updates from \`$GITHUB_REF_NAME\`." + branch="otelbot/changelog-${GITHUB_REF_NAME//\//-}" + + if [[ -z $PRIOR_VERSION_WHEN_PATCH ]]; then + if git diff --quiet; then + echo there are no updates needed to the change log on main, not creating pull request + exit 0 # success + fi + fi + + git commit -a -m "$message" + git push origin HEAD:$branch + gh pr create --title "$message" \ + --body "$body" \ + --head $branch \ + --base main diff --git a/RELEASING.md b/RELEASING.md index 49f88856..16d87990 100644 --- a/RELEASING.md +++ b/RELEASING.md @@ -1,133 +1,104 @@ # Release process -This repo follows an **independent per-package release model**: every -publishable package owns its own version, `CHANGELOG.md`, and release cadence. -There is no coordinated bulk release across packages. The packages listed in -`[exclude_release]` of [`eachdist.ini`](eachdist.ini) are excluded from any -bulk-release tooling carried over from `opentelemetry-python-contrib`. - -> [!IMPORTANT] -> The CI workflows for fully automated releases (`prepare-release`, -> `package-release`, etc.) have **not yet been ported** from -> `opentelemetry-python-contrib`. Until they are, releases are performed -> manually using the steps below. Tracked in #15. - -## Publishable packages - -| Package | PyPI name | -| --- | --- | -| `opentelemetry-instrumentation-anthropic` | _TBD_ | -| `opentelemetry-instrumentation-claude-agent-sdk` | _TBD_ | -| `opentelemetry-instrumentation-google-genai` | _TBD_ | -| `opentelemetry-instrumentation-langchain` | _TBD_ | -| `opentelemetry-instrumentation-openai-agents-v2` | _TBD_ | -| `opentelemetry-instrumentation-openai-v2` | _TBD_ | -| `opentelemetry-instrumentation-weaviate` | _TBD_ | -| `opentelemetry-util-genai` | _TBD_ | - -> [!NOTE] -> The PyPI names will differ from the names used in -> `opentelemetry-python-contrib`. Until each name is chosen and reserved on -> PyPI, packages cannot be published from this repo. Tracked in #15. - -## Releasing a package - -The steps below cover releasing a single package end-to-end. Repeat per -package as needed. - -### 1. Prepare the changelog - -Changelog entries are managed with [towncrier](https://towncrier.readthedocs.io/). -Each PR adds a fragment under `/.changelog/.`; at -release time those fragments are compiled into the package's `CHANGELOG.md`. - -From the package directory, build the new release section into `CHANGELOG.md`: - -```sh -cd -uv run towncrier build --version -``` - -This consumes every fragment under `./.changelog/` and inserts a new -`## Version ()` block. Commit the resulting changes. - -> [!NOTE] -> Several `CHANGELOG.md` files have pre-existing entries under the static -> `## Unreleased` section that pre-date towncrier. These entries are **not** -> picked up by `towncrier build`. Fold them by hand into the new release -> block before committing. - -### 2. Bump the version - -Each package's version lives in -`/src/.../version.py` (path depends on the package). Bump the -version (e.g. drop the `.dev` suffix for the release, then bump it back -afterwards for the next development cycle). - -### 3. Tag and build - -Tags follow the format `==` (matches the contrib -convention, used by [`scripts/build_a_package.sh`](scripts/build_a_package.sh)). +Every package in this repo releases independently. There is no coordinated +bulk release across packages. + +Releases are driven by GitHub Actions workflows ported from +`opentelemetry-python-contrib`. They handle version bumps, changelog +generation (via [towncrier](https://towncrier.readthedocs.io/)), tagging, +PyPI publishing, GitHub releases, and back-merging the changelog to `main`. + +## Prerequisites (one-time, repo-level) + +These must be configured by maintainers before the workflows can run: + +- **`OTELBOT_APP_ID`** (repo or org variable) and **`OTELBOT_PRIVATE_KEY`** + (repo or org secret) — credentials for the `otelbot` GitHub App. The + workflows commit and open PRs through this App so the resulting PRs + trigger CI (a plain `GITHUB_TOKEN` doesn't). +- **`pypi_password`** (repo secret) — a PyPI API token with publish rights + on the relevant packages. Tracked in #15: migrating to PyPI trusted + publishing once package names are decided. +- **Branch protection** on `package-release/*/v*` branches so changes only + land via reviewed PRs. + +## Minor/major release flow + +For releasing `` (e.g. `opentelemetry-instrumentation-anthropic`): + +1. Run the + [`[Package] Prepare release`](./.github/workflows/package-prepare-release.yml) + workflow against `main`. Select the package from the dropdown. + - Creates a long-term release branch + `package-release//v..x` (or `v.bx` for unstable). + - Opens **two PRs**: + - PR against the release branch: drops the `.dev` suffix from + `version.py`, builds the changelog via `towncrier build`. + - PR against `main`: bumps `version.py` to the next `.dev` version, + builds the changelog via `towncrier build` (so fragments don't + carry over to the next release cycle). +2. Review and merge **both** PRs. +3. Run the + [`[Package] Release`](./.github/workflows/package-release.yml) + workflow against the `package-release//v*` branch. + - Verifies the changelog PR was merged to `main`. + - Builds the wheel via `scripts/build_a_package.sh` and publishes to + PyPI via `twine`. + - Creates a GitHub release tagged `==`. + - Opens a back-merge PR against `main` copying the resolved changelog + section (in case any edits landed on the release branch). +4. Review and merge the back-merge PR if one was created. + +## Patch release flow + +1. Check out the package's existing release branch + `package-release//v..x`. +2. Land any patch PRs against this branch (cherry-pick or direct). +3. Run the + [`[Package] Prepare patch release`](./.github/workflows/package-prepare-patch-release.yml) + workflow with the release branch selected. + - Opens a PR against the release branch bumping the patch version and + running `towncrier build` against the patch fragments. +4. Review and merge the PR. +5. Run the + [`[Package] Release`](./.github/workflows/package-release.yml) + workflow against the release branch. Same effect as for a + minor/major release. + +## Pre-existing static `## Unreleased` entries + +Several packages carry CHANGELOG entries that pre-date towncrier (added +before the towncrier marker was inserted). `towncrier build` does **not** +fold them into the generated release section. Before the first towncrier +release of a given package, fold those entries by hand into the new +release section produced by `towncrier build` (or convert them into +fragments first). The do-not-edit comment in each `CHANGELOG.md` flags +this. + +## Claiming a PyPI namespace for a new package + +When a new package is introduced, release the current `.dev` version under +the `opentelemetry` PyPI org to prevent name-squatting. Do this shortly +after the introductory PR lands on `main`. + +## Troubleshooting + +### PyPI publish failed mid-workflow + +Switch to the release branch locally and re-run the publish step manually: ```sh -git tag == -git push --tags - -PACKAGE_NAME= VERSION= ./scripts/build_a_package.sh -``` - -`build_a_package.sh` writes the `.tar.gz` and `.whl` into `dist/`. - -### 4. Publish to PyPI - -```sh -twine upload --skip-existing dist/* -``` - -> [!IMPORTANT] -> Publishing requires PyPI maintainer rights on the package. The token / API -> credential setup is not yet automated. Maintainers should publish from a -> machine that already has `~/.pypirc` configured, or via a dedicated CI -> workflow once one exists. - -### 5. Create a GitHub release - -```sh -gh release create == \ - --title " " \ - --notes-file <(awk '/^## Version /,/^## /{print}' /CHANGELOG.md | sed '$d') +git checkout package-release//v..x +./scripts/build_a_package.sh +twine upload --skip-existing --verbose dist/* ``` -Or use the GitHub UI: navigate to the new tag and click "Draft release", -pasting in the relevant section from the package's `CHANGELOG.md`. - -### 6. Bump to the next development version - -Open a follow-up PR bumping the package's `version.py` to the next -`X.Yb.dev` (or `X.Y..dev` for stable components), so subsequent -fragments accumulate against the next release. - -## Releasing a dev version to claim the PyPI namespace - -When introducing a new package, release the current development version -under the `opentelemetry` PyPI org to prevent name-squatting. Do this -shortly after the introductory PR lands on `main`. - -## Version numbering - -- **Unstable components** (everything currently in this repo): versions look - like `0.Yb0.dev` on `main`, `0.Yb0` at release time, then bump to - `0.{Y+1}b0.dev`. -- **Stable components**: versions look like `X.Y.0.dev` on `main`, `X.Y.0` - at release time, then bump to `X.{Y+1}.0.dev`. None of the current - packages are stable. - -## What's still missing +Then re-run the `[Package] Release` workflow to pick up the remaining +steps (GitHub release + back-merge PR). -Tracked in #15: +## Out of scope -- New PyPI names for each package -- `prepare-release` workflow (cuts the release PR and bumps versions) -- `package-release` workflow (tags, builds, publishes, opens GitHub release) -- `backport` workflow for patch releases on long-term release branches -- Patch release tooling once a package has a long-term release branch +- A `backport` workflow (none yet — add when there's a real long-term + release branch to backport into). +- Coordinated cross-package releases (every package here is independent; + `eachdist.ini` lists all publishable packages under `[exclude_release]`). diff --git a/scripts/generate_release_notes.sh b/scripts/generate_release_notes.sh new file mode 100755 index 00000000..193e6241 --- /dev/null +++ b/scripts/generate_release_notes.sh @@ -0,0 +1,26 @@ +#!/bin/bash + +# This script copies release notes for the current version from CHANGELOG.md file +# and stores them in /tmp/release-notes.txt. +# It also stores them in a /tmp/CHANGELOG_SECTION.md which is used later to copy them over to the +# CHANGELOG in main branch which is done by merge_changelog_to_main script. + +# This script is called from the release workflows (package-release.yml and release.yml). + +set -ev + +# conditional block not indented because of the heredoc +if [[ ! -z $PRIOR_VERSION_WHEN_PATCH ]]; then +cat > /tmp/release-notes.txt << EOF +This is a patch release on the previous $PRIOR_VERSION_WHEN_PATCH release, fixing the issue(s) below. + +EOF +fi + +# CHANGELOG_SECTION.md is also used at the end of the release workflow +# for copying the change log updates to main +sed -n "0,/^## Version ${VERSION}/d;/^## Version /q;p" $CHANGELOG > /tmp/CHANGELOG_SECTION.md + +# the complex perl regex is needed because markdown docs render newlines as soft wraps +# while release notes render them as line breaks +perl -0pe 's/(?> /tmp/release-notes.txt diff --git a/scripts/merge_changelog_to_main.sh b/scripts/merge_changelog_to_main.sh new file mode 100755 index 00000000..ee7d027c --- /dev/null +++ b/scripts/merge_changelog_to_main.sh @@ -0,0 +1,50 @@ +#!/bin/bash + +# This script copies release notes for the current version from CHANGELOG.md file +# and stores them in a temporary file for later use in the release workflow + +# This script is called from the release workflows (package-release.yml and release.yml). + +set -ev + +if [[ -z $PRIOR_VERSION_WHEN_PATCH ]]; then + # this was not a patch release, so the version exists already in the CHANGELOG.md + + # update the release date + date=$(gh release view $RELEASE_TAG --json publishedAt --jq .publishedAt | sed 's/T.*//') + sed -Ei "s/## Version ${VERSION}.*/## Version ${VERSION} ($date)/" ${CHANGELOG} + + # the entries are copied over from the release branch to support workflows + # where change log entries may be updated after preparing the release branch + + # copy the portion above the release, up to and including the heading + sed -n "0,/^## Version ${VERSION} ($date)/p" ${CHANGELOG} > /tmp/CHANGELOG.md + + # copy the release notes + cat /tmp/CHANGELOG_SECTION.md >> /tmp/CHANGELOG.md + + # copy the portion below the release + sed -n "0,/^## Version ${VERSION} /d;0,/^## Version /{/^## Version/!d};p" ${CHANGELOG} \ + >> /tmp/CHANGELOG.md + + # update the real CHANGELOG.md + cp /tmp/CHANGELOG.md ${CHANGELOG} +else + # this was a patch release, so the version does not exist already in the CHANGELOG.md + + # copy the portion above the top-most release, not including the heading + sed -n "0,/^## Version /{ /^## Version /!p }" ${CHANGELOG} > /tmp/CHANGELOG.md + + # add the heading + date=$(gh release view $RELEASE_TAG --json publishedAt --jq .publishedAt | sed 's/T.*//') + echo "## Version ${VERSION} ($date)" >> /tmp/CHANGELOG.md + + # copy the release notes + cat /tmp/CHANGELOG_SECTION.md >> /tmp/CHANGELOG.md + + # copy the portion starting from the top-most release + sed -n "/^## Version /,\$p" ${CHANGELOG} >> /tmp/CHANGELOG.md + + # update the real CHANGELOG.md + cp /tmp/CHANGELOG.md ${CHANGELOG} +fi \ No newline at end of file From ad2bec4a926ce8bab53939abab4aa5b7df91f274 Mon Sep 17 00:00:00 2001 From: Mike Goldsmith Date: Thu, 28 May 2026 11:35:50 +0100 Subject: [PATCH 5/9] drop --discussion-category from gh release create Discussions are not enabled on the opentelemetry-python-genai repo, and there is no `announcements` category, so the flag would cause `gh release create` to fail. Drop it for now; can be re-added in a follow-up if/when Discussions is enabled and an announcements category is created. Picked up from JWinermaSplunk's review on PR #20. Assisted-by: Claude Opus 4.7 (1M context) --- .github/workflows/package-release.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/.github/workflows/package-release.yml b/.github/workflows/package-release.yml index 18c24fb8..80075e94 100644 --- a/.github/workflows/package-release.yml +++ b/.github/workflows/package-release.yml @@ -113,7 +113,6 @@ jobs: gh release create --target $GITHUB_REF_NAME \ --title "${PACKAGE_NAME} ${VERSION}" \ --notes-file /tmp/release-notes.txt \ - --discussion-category announcements \ $RELEASE_TAG - uses: actions/checkout@v4 From 529d6d3de02041e0c9e5ac33678267b9270f01fd Mon Sep 17 00:00:00 2001 From: Mike Goldsmith Date: Tue, 9 Jun 2026 13:05:08 +0100 Subject: [PATCH 6/9] restore setup-uv step to package release workflow build_a_package.sh uses `uv run` and `uv build`, so the runner needs uv installed. Removed by mistake earlier. Assisted-by: Claude Opus 4.7 (1M context) --- .github/workflows/package-release.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.github/workflows/package-release.yml b/.github/workflows/package-release.yml index 80075e94..6c7c89d7 100644 --- a/.github/workflows/package-release.yml +++ b/.github/workflows/package-release.yml @@ -87,6 +87,9 @@ jobs: with: python-version: '3.10' + - name: Install uv + uses: astral-sh/setup-uv@08807647e7069bb48b6ef5acd8ec9567f424441b # v8.1.0 + - name: Build wheels run: ./scripts/build_a_package.sh From e0697972d913d17ff3d5ce0089b4a9056be87792 Mon Sep 17 00:00:00 2001 From: Mike Goldsmith Date: Fri, 12 Jun 2026 14:09:13 +0100 Subject: [PATCH 7/9] add bulk release workflows with tag-from-main and PyPI trusted publishing Replace contrib's always-on release branches with prep and publish on main, lazy backport branches, release-all orchestration, and OIDC publishing via the pypi GitHub environment. --- .../workflows/_prepare-release-package.yml | 85 +++++++ .github/workflows/_release-package.yml | 218 +++++++++++++++++ .../package-prepare-patch-release.yml | 128 ++++------ .github/workflows/package-prepare-release.yml | 214 +---------------- .github/workflows/package-release.yml | 158 ++----------- .github/workflows/release-all-prepare.yml | 92 ++++++++ .github/workflows/release-all.yml | 142 +++++++++++ RELEASING.md | 220 +++++++++++++----- scripts/build_a_package.sh | 4 +- scripts/bump_package_dev_version.sh | 37 +++ scripts/eachdist.py | 49 ++++ scripts/prepare_package_for_patch_release.sh | 49 ++++ scripts/prepare_package_for_release.sh | 41 ++++ 13 files changed, 948 insertions(+), 489 deletions(-) create mode 100644 .github/workflows/_prepare-release-package.yml create mode 100644 .github/workflows/_release-package.yml create mode 100644 .github/workflows/release-all-prepare.yml create mode 100644 .github/workflows/release-all.yml create mode 100755 scripts/bump_package_dev_version.sh create mode 100755 scripts/prepare_package_for_patch_release.sh create mode 100755 scripts/prepare_package_for_release.sh diff --git a/.github/workflows/_prepare-release-package.yml b/.github/workflows/_prepare-release-package.yml new file mode 100644 index 00000000..c218af26 --- /dev/null +++ b/.github/workflows/_prepare-release-package.yml @@ -0,0 +1,85 @@ +name: _Prepare release package +on: + workflow_call: + inputs: + package: + required: true + type: string + pr_title_prefix: + required: false + type: string + default: "" + add_release_label: + required: false + type: boolean + default: false + +permissions: + contents: read + +jobs: + prepare: + permissions: + contents: write + pull-requests: write + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Verify prerequisites + run: | + if [[ "$GITHUB_REF_NAME" != "main" ]]; then + echo "This workflow should only be run against main" + exit 1 + fi + + version_dev=$(./scripts/eachdist.py version --package ${{ inputs.package }}) + if [[ ! "$version_dev" =~ \.dev$ ]]; then + echo "Expected a .dev version on main, got ${version_dev}" + exit 1 + fi + + echo "VERSION=${version_dev%.dev}" >> $GITHUB_ENV + echo "PACKAGE_NAME=${{ inputs.package }}" >> $GITHUB_ENV + + - name: Set up Python 3.10 + uses: actions/setup-python@v5 + with: + python-version: '3.10' + + - name: Install tox and towncrier + run: | + pip install tox towncrier==25.8.0 + + - name: Prepare package for release + run: ./scripts/prepare_package_for_release.sh "${{ inputs.package }}" + + - name: Use CLA approved github bot + run: .github/scripts/use-cla-approved-github-bot.sh + + - uses: actions/create-github-app-token@df432ceedc7162793a195dd1713ff69aefc7379e # v2.0.6 + id: otelbot-token + with: + app-id: ${{ vars.OTELBOT_APP_ID }} + private-key: ${{ secrets.OTELBOT_PRIVATE_KEY }} + + - name: Create pull request against main + env: + GITHUB_TOKEN: ${{ steps.otelbot-token.outputs.token }} + run: | + prefix="${{ inputs.pr_title_prefix }}" + message="${prefix}Prepare release for ${PACKAGE_NAME} v${VERSION}" + body="Prepare \`${PACKAGE_NAME}\` v\`${VERSION}\` for release." + branch="otelbot/prepare-${PACKAGE_NAME}-v${VERSION}" + + git commit -a -m "$message" + git push origin HEAD:"$branch" + pr_url=$(gh pr create --title "$message" \ + --body "$body" \ + --head "$branch" \ + --base main \ + --json url --jq .url) + + if [[ "${{ inputs.add_release_label }}" == "true" ]]; then + gh pr edit "$pr_url" --add-label release + fi diff --git a/.github/workflows/_release-package.yml b/.github/workflows/_release-package.yml new file mode 100644 index 00000000..d370f4a4 --- /dev/null +++ b/.github/workflows/_release-package.yml @@ -0,0 +1,218 @@ +name: _Release package +on: + workflow_call: + inputs: + package: + required: true + type: string + bump_after_release: + required: false + type: boolean + default: false + ref: + required: false + type: string + default: "" + on_main: + required: false + type: boolean + default: false + +permissions: + contents: read + +jobs: + release: + environment: pypi + permissions: + contents: write + pull-requests: write + id-token: write + runs-on: ubuntu-latest + steps: + - name: Resolve release target + id: target + run: | + if [[ "${{ inputs.on_main }}" == "true" || "$GITHUB_REF_NAME" == "main" ]]; then + echo "on_main=true" >> "$GITHUB_OUTPUT" + elif [[ "$GITHUB_REF_NAME" == package-release/${{ inputs.package }}/v* ]]; then + echo "on_main=false" >> "$GITHUB_OUTPUT" + else + echo "Run on main or package-release/${{ inputs.package }}/v*" + exit 1 + fi + + - uses: actions/checkout@v4 + with: + ref: ${{ inputs.ref != '' && inputs.ref || github.ref }} + + - name: Set environment variables + run: | + version=$(./scripts/eachdist.py version --package ${{ inputs.package }}) + if [[ $version =~ ^([0-9]+)\.([0-9]+)\.([0-9]+) ]]; then + major="${BASH_REMATCH[1]}" + minor="${BASH_REMATCH[2]}" + patch="${BASH_REMATCH[3]}" + if [[ $patch != 0 ]]; then + prior_version_when_patch="${major}.${minor}.$((patch - 1))" + fi + elif [[ $version =~ ^([0-9]+)\.([0-9]+)b([0-9]+)$ ]]; then + major="${BASH_REMATCH[1]}" + minor="${BASH_REMATCH[2]}" + patch="${BASH_REMATCH[3]}" + if [[ $patch != 0 ]]; then + prior_version_when_patch="${major}.${minor}b$((patch - 1))" + fi + else + echo "unexpected version: $version" + exit 1 + fi + + if [[ "${{ steps.target.outputs.on_main }}" == "true" && "$version" == *.dev ]]; then + echo "version on main still has a .dev suffix; merge the prepare PR first" + exit 1 + fi + + path=$(./scripts/eachdist.py find-package --package ${{ inputs.package }}) + echo "CHANGELOG=./$path/CHANGELOG.md" >> $GITHUB_ENV + echo "PACKAGE_NAME=${{ inputs.package }}" >> $GITHUB_ENV + echo "VERSION=$version" >> $GITHUB_ENV + echo "RELEASE_TAG=${{ inputs.package }}==$version" >> $GITHUB_ENV + echo "PRIOR_VERSION_WHEN_PATCH=$prior_version_when_patch" >> $GITHUB_ENV + if [[ "${{ steps.target.outputs.on_main }}" == "true" ]]; then + echo "RELEASE_TARGET=main" >> $GITHUB_ENV + else + echo "RELEASE_TARGET=$GITHUB_REF_NAME" >> $GITHUB_ENV + fi + + - name: Check that changelog update was merged to main + if: steps.target.outputs.on_main == 'true' + run: | + if [[ -z $PRIOR_VERSION_WHEN_PATCH ]]; then + if ! grep --quiet "^## Version ${VERSION}" ${CHANGELOG}; then + echo "The prepare-release PR needs to be merged first" + exit 1 + fi + fi + + - uses: actions/checkout@v4 + if: steps.target.outputs.on_main != 'true' + with: + ref: main + + - name: Check that changelog update was merged to main (backport branch) + if: steps.target.outputs.on_main != 'true' + run: | + if [[ -z $PRIOR_VERSION_WHEN_PATCH ]]; then + if ! grep --quiet "^## Version ${VERSION}" ${CHANGELOG}; then + echo "The prepare-release PR needs to be merged first" + exit 1 + fi + fi + + - uses: actions/checkout@v4 + if: steps.target.outputs.on_main != 'true' + with: + ref: ${{ inputs.ref != '' && inputs.ref || github.ref }} + + - uses: actions/setup-python@v5 + with: + python-version: '3.10' + + - name: Install uv + uses: astral-sh/setup-uv@08807647e7069bb48b6ef5acd8ec9567f424441b # v8.1.0 + + - name: Build wheels + run: ./scripts/build_a_package.sh + + - name: Publish to PyPI + uses: pypa/gh-action-pypi-publish@release/v1 + with: + packages-dir: dist/ + skip-existing: true + + - name: Generate release notes + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: ./scripts/generate_release_notes.sh + + - name: Create GitHub release + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + gh release create --target "$RELEASE_TARGET" \ + --title "${PACKAGE_NAME} ${VERSION}" \ + --notes-file /tmp/release-notes.txt \ + "$RELEASE_TAG" + + - uses: actions/checkout@v4 + with: + ref: main + + - name: Copy changelog updates to main + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: ./scripts/merge_changelog_to_main.sh + + - name: Use CLA approved github bot + run: .github/scripts/use-cla-approved-github-bot.sh + + - uses: actions/create-github-app-token@df432ceedc7162793a195dd1713ff69aefc7379e # v2.0.6 + id: otelbot-token + with: + app-id: ${{ vars.OTELBOT_APP_ID }} + private-key: ${{ secrets.OTELBOT_PRIVATE_KEY }} + + - name: Open pull request for changelog updates on main + env: + GITHUB_TOKEN: ${{ steps.otelbot-token.outputs.token }} + run: | + message="Copy changelog updates for ${PACKAGE_NAME} v${VERSION}" + body="Copy changelog updates for \`${PACKAGE_NAME}\` v\`${VERSION}\`." + branch="otelbot/changelog-${PACKAGE_NAME}-${VERSION}" + + changes=0 + if [[ -z $PRIOR_VERSION_WHEN_PATCH ]]; then + if ! git diff --quiet; then + changes=1 + fi + else + changes=1 + fi + + if [[ $changes == 0 ]]; then + echo "No changelog updates needed on main" + exit 0 + fi + + git commit -a -m "$message" + git push origin HEAD:"$branch" + gh pr create --title "$message" \ + --body "$body" \ + --head "$branch" \ + --base main + + - name: Bump package version on main after release + if: inputs.bump_after_release && steps.target.outputs.on_main == 'true' + env: + GITHUB_TOKEN: ${{ steps.otelbot-token.outputs.token }} + run: | + git checkout main + git pull origin main + + ./scripts/bump_package_dev_version.sh "${PACKAGE_NAME}" + + if git diff --quiet; then + echo "No version bump needed" + exit 0 + fi + + message="Bump ${PACKAGE_NAME} to next dev version after v${VERSION}" + branch="otelbot/bump-${PACKAGE_NAME}-after-v${VERSION}" + + git commit -a -m "$message" + git push origin HEAD:"$branch" + gh pr create --title "$message" \ + --body "Bump \`${PACKAGE_NAME}\` to the next \`.dev\` version after releasing v\`${VERSION}\`." \ + --head "$branch" \ + --base main diff --git a/.github/workflows/package-prepare-patch-release.yml b/.github/workflows/package-prepare-patch-release.yml index eebdf9ec..0a5b7baa 100644 --- a/.github/workflows/package-prepare-patch-release.yml +++ b/.github/workflows/package-prepare-patch-release.yml @@ -5,13 +5,13 @@ on: package: type: choice options: - - opentelemetry-instrumentation-anthropic - - opentelemetry-instrumentation-claude-agent-sdk + - opentelemetry-instrumentation-genai-anthropic + - opentelemetry-instrumentation-genai-claude-agent-sdk - opentelemetry-instrumentation-google-genai - - opentelemetry-instrumentation-langchain - - opentelemetry-instrumentation-openai-agents-v2 - - opentelemetry-instrumentation-openai-v2 - - opentelemetry-instrumentation-weaviate + - opentelemetry-instrumentation-genai-langchain + - opentelemetry-instrumentation-genai-openai-agents + - opentelemetry-instrumentation-genai-openai + - opentelemetry-instrumentation-genai-weaviate-client - opentelemetry-util-genai description: 'Package to be released' required: true @@ -22,87 +22,54 @@ run-name: "[Package][${{ inputs.package }}] Prepare patch release" jobs: prepare-patch-release: permissions: - contents: write # required for pushing branches - pull-requests: write # required for creating pull requests + contents: write + pull-requests: write runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - name: Verify prerequisites + id: verify run: | - if [[ $GITHUB_REF_NAME != package-release/${{ inputs.package }}/v* ]]; then - echo this workflow should only be run against package-release/${{ inputs.package }}* branches, but is running on $GITHUB_REF_NAME - exit 1 - fi - - path=./$(./scripts/eachdist.py find-package --package ${{ inputs.package }}) - changelog=$path/CHANGELOG.md - - if [ ! -f $changelog ]; then - echo "missing $changelog file" - exit 1 - fi - - version=$(./scripts/eachdist.py version --package ${{ inputs.package }}) - - version_file=$(find $path -type f -path "**/version.py") - file_count=$(echo "$version_file" | wc -l) - - if [ "$file_count" -ne 1 ]; then - echo "Error: expected one version file, found $file_count" - echo "$version_file" - exit 1 - fi - - if [[ $version =~ ^([0-9]+)\.([0-9]+)\.([0-9]+) ]]; then - # 1.2.3 or 1.2.3rc1 - major="${BASH_REMATCH[1]}" - minor="${BASH_REMATCH[2]}" - patch="${BASH_REMATCH[3]}" - next_version="$major.$minor.$((patch + 1))" - release_branch_name="package-release/${{ inputs.package }}/v$major.$minor.x" - elif [[ $version =~ ^([0-9]+)\.([0-9]+)b([0-9]+)$ ]]; then - # 0.1b1 - major="${BASH_REMATCH[1]}" - minor="${BASH_REMATCH[2]}" - patch="${BASH_REMATCH[3]}" - next_version="$major.${minor}b$((patch + 1))" - release_branch_name="package-release/${{ inputs.package }}/v$major.${minor}bx" - else - echo "unexpected version: '$version'" - exit 1 - fi - - if [[ $GITHUB_REF_NAME != $release_branch_name ]]; then - echo this workflow should only be run against $release_branch_name branch, but is running on $GITHUB_REF_NAME - exit 1 - fi - - echo "PACKAGE_NAME=${{ inputs.package }}" >> $GITHUB_ENV - echo "VERSION=$version" >> $GITHUB_ENV - echo "NEXT_VERSION=$next_version" >> $GITHUB_ENV - echo "CHANGELOG=$changelog" >> $GITHUB_ENV - echo "VERSION_FILE=$version_file" >> $GITHUB_ENV - - - name: Update version - run: | - # replace the version in the version file (1.2.3 -> 1.2.4) - sed -i -E "s/__version__\s*=\s*\"${VERSION}\"/__version__ = \"${NEXT_VERSION}\"/g" $VERSION_FILE + on_main=false + on_backport=false + case "$GITHUB_REF_NAME" in + main) + on_main=true + ;; + package-release/${{ inputs.package }}/v*) + on_backport=true + ;; + *) + echo "Run on main (current minor patch) or package-release/${{ inputs.package }}/v* (backport)" + exit 1 + ;; + esac + + echo "on_main=${on_main}" >> "$GITHUB_OUTPUT" + echo "on_backport=${on_backport}" >> "$GITHUB_OUTPUT" + echo "PACKAGE_NAME=${{ inputs.package }}" >> "$GITHUB_ENV" - name: Set up Python 3.10 uses: actions/setup-python@v5 with: python-version: '3.10' - - name: Install tox - run: pip install tox - - name: run tox - run: tox -e generate - - name: Install towncrier - run: pip install towncrier==25.8.0 + - name: Install tox and towncrier + run: pip install tox towncrier==25.8.0 - - name: Generate changelog - run: towncrier build --yes --version "$NEXT_VERSION" --dir "$(dirname $CHANGELOG)" + - name: Prepare patch release on main + if: steps.verify.outputs.on_main == 'true' + run: ./scripts/prepare_package_for_release.sh "${{ inputs.package }}" + + - name: Prepare patch release on backport branch + if: steps.verify.outputs.on_backport == 'true' + run: ./scripts/prepare_package_for_patch_release.sh "${{ inputs.package }}" + + - name: Set pull request title version + run: | + version=$(./scripts/eachdist.py version --package "${{ inputs.package }}") + echo "VERSION=${version}" >> "$GITHUB_ENV" - name: Use CLA approved github bot run: .github/scripts/use-cla-approved-github-bot.sh @@ -115,15 +82,14 @@ jobs: - name: Create pull request env: - # not using secrets.GITHUB_TOKEN since pull requests from that token do not run workflows GITHUB_TOKEN: ${{ steps.otelbot-token.outputs.token }} run: | - message="Prepare patch release for ${PACKAGE_NAME} v${NEXT_VERSION}" - branch="otelbot/patch-${PACKAGE_NAME}-version-to-v${NEXT_VERSION}" + message="Prepare patch release for ${PACKAGE_NAME} v${VERSION}" + branch="otelbot/patch-${PACKAGE_NAME}-v${VERSION}" git commit -a -m "$message" - git push origin HEAD:$branch - gh pr create --title "[$GITHUB_REF_NAME] $message" \ + git push origin HEAD:"$branch" + gh pr create --title "$message" \ --body "$message." \ - --head $branch \ - --base $GITHUB_REF_NAME + --head "$branch" \ + --base "$GITHUB_REF_NAME" diff --git a/.github/workflows/package-prepare-release.yml b/.github/workflows/package-prepare-release.yml index f1368aac..5b1de096 100644 --- a/.github/workflows/package-prepare-release.yml +++ b/.github/workflows/package-prepare-release.yml @@ -5,13 +5,13 @@ on: package: type: choice options: - - opentelemetry-instrumentation-anthropic - - opentelemetry-instrumentation-claude-agent-sdk + - opentelemetry-instrumentation-genai-anthropic + - opentelemetry-instrumentation-genai-claude-agent-sdk - opentelemetry-instrumentation-google-genai - - opentelemetry-instrumentation-langchain - - opentelemetry-instrumentation-openai-agents-v2 - - opentelemetry-instrumentation-openai-v2 - - opentelemetry-instrumentation-weaviate + - opentelemetry-instrumentation-genai-langchain + - opentelemetry-instrumentation-genai-openai-agents + - opentelemetry-instrumentation-genai-openai + - opentelemetry-instrumentation-genai-weaviate-client - opentelemetry-util-genai description: 'Package to be released' required: true @@ -20,200 +20,10 @@ permissions: contents: read run-name: "[Package][${{ inputs.package }}] Prepare release" -jobs: - prereqs: - runs-on: ubuntu-latest - outputs: - version: ${{ steps.verify.outputs.version }} - changelog: ${{ steps.verify.outputs.changelog }} - version_file: ${{ steps.verify.outputs.version_file }} - release_branch_name: ${{ steps.verify.outputs.release_branch_name }} - next_version: ${{ steps.verify.outputs.next_version }} - steps: - - uses: actions/checkout@v4 - with: - fetch-depth: 0 - - - id: verify - name: Verify prerequisites - run: | - if [[ $GITHUB_REF_NAME != main ]]; then - echo this workflow should only be run against main - exit 1 - fi - - path=./$(./scripts/eachdist.py find-package --package ${{ inputs.package }}) - changelog=$path/CHANGELOG.md - - if [ ! -f $changelog ]; then - echo "missing $changelog file" - exit 1 - fi - - version_dev=$(./scripts/eachdist.py version --package ${{ inputs.package }}) - - if [[ ! $version_dev =~ ^([0-9]+)\.([0-9]+)[\.|b]{1}([0-9]+).*.dev$ ]]; then - echo "unexpected version: $version" - exit 1 - fi - - version=${version_dev%.dev} - - version_file=$(find $path -type f -path "**/version.py") - file_count=$(echo "$version_file" | wc -l) - - if [ "$file_count" -ne 1 ]; then - echo "Error: expected one version file, found $file_count" - echo "$version_file" - exit 1 - fi - - if [[ $version =~ ^([0-9]+)\.([0-9]+)\.([0-9]+) ]]; then - # 1.2.3 or 1.2.3rc1 - major="${BASH_REMATCH[1]}" - minor="${BASH_REMATCH[2]}" - release_branch_name="package-release/${{ inputs.package }}/v$major.$minor.x" - next_version="$major.$((minor + 1)).0" - elif [[ $version =~ ^([0-9]+)\.([0-9]+)b([0-9]+)$ ]]; then - # 0.1b1 - major="${BASH_REMATCH[1]}" - minor="${BASH_REMATCH[2]}" - release_branch_name="package-release/${{ inputs.package }}/v$major.${minor}bx" - next_version="$major.$((minor + 1))b0" - else - echo "unexpected version: '$version'" - exit 1 - fi - - echo "version=$version" >> $GITHUB_OUTPUT - echo "changelog=$changelog" >> $GITHUB_OUTPUT - echo "version_file=$version_file" >> $GITHUB_OUTPUT - echo "release_branch_name=$release_branch_name" >> $GITHUB_OUTPUT - echo "next_version=$next_version" >> $GITHUB_OUTPUT - - create-pull-request-against-release-branch: - runs-on: ubuntu-latest - needs: prereqs - permissions: - contents: write # required for pushing branches - pull-requests: write # required for creating pull requests - steps: - - uses: actions/checkout@v4 - - - name: Set environment variables - run: | - echo "RELEASE_BRANCH_NAME=${{ needs.prereqs.outputs.release_branch_name }}" >> $GITHUB_ENV - echo "VERSION=${{ needs.prereqs.outputs.version }}" >> $GITHUB_ENV - echo "CHANGELOG=${{ needs.prereqs.outputs.changelog }}" >> $GITHUB_ENV - echo "VERSION_FILE=${{ needs.prereqs.outputs.version_file }}" >> $GITHUB_ENV - echo "PACKAGE_NAME=${{ github.event.inputs.package }}" >> $GITHUB_ENV - - - name: Create package release branch - run: | - git push origin HEAD:$RELEASE_BRANCH_NAME - - - name: Update package version - run: | - # replace the version in the version file (1.2.3dev -> 1.2.3) - sed -i -E "s/__version__\s*=\s*\"${VERSION}\.dev\"/__version__ = \"${VERSION}\"/g" $VERSION_FILE - - - name: Set up Python 3.10 - uses: actions/setup-python@v5 - with: - python-version: '3.10' - - name: Install tox - run: pip install tox - - name: run tox - run: tox -e generate - - name: Install towncrier - run: pip install towncrier==25.8.0 - - - name: Generate changelog - run: towncrier build --yes --version "$VERSION" --dir "$(dirname $CHANGELOG)" - - - name: Use CLA approved github bot - run: .github/scripts/use-cla-approved-github-bot.sh - - - uses: actions/create-github-app-token@df432ceedc7162793a195dd1713ff69aefc7379e # v2.0.6 - id: otelbot-token - with: - app-id: ${{ vars.OTELBOT_APP_ID }} - private-key: ${{ secrets.OTELBOT_PRIVATE_KEY }} - - - name: Create pull request against the release branch - env: - # not using secrets.GITHUB_TOKEN since pull requests from that token do not run workflows - GITHUB_TOKEN: ${{ steps.otelbot-token.outputs.token }} - run: | - message="Prepare release for ${PACKAGE_NAME} v${VERSION}" - branch="otelbot/prepare-${RELEASE_BRANCH_NAME}" - - git commit -a -m "$message" - git push origin HEAD:$branch - gh pr create --title "[$RELEASE_BRANCH_NAME] $message" \ - --body "$message." \ - --head $branch \ - --base $RELEASE_BRANCH_NAME - - create-pull-request-against-main: - runs-on: ubuntu-latest - needs: prereqs - permissions: - contents: write # required for pushing branches - pull-requests: write # required for creating pull requests - steps: - - uses: actions/checkout@v4 - - - name: Set environment variables - run: | - echo "PACKAGE_NAME=${{ github.event.inputs.package }}" >> $GITHUB_ENV - echo "VERSION=${{ needs.prereqs.outputs.version }}" >> $GITHUB_ENV - echo "VERSION_FILE=${{ needs.prereqs.outputs.version_file }}" >> $GITHUB_ENV - echo "NEXT_VERSION=${{ needs.prereqs.outputs.next_version }}" >> $GITHUB_ENV - echo "CHANGELOG=${{ needs.prereqs.outputs.changelog }}" >> $GITHUB_ENV - - - name: Update version - run: | - # replace the version in the version file (1.2.3dev -> 1.3.3.dev) - sed -i -E "s/__version__\s*=\s*\"${VERSION}\.dev\"/__version__ = \"${NEXT_VERSION}.dev\"/g" $VERSION_FILE - - - name: Set up Python 3.10 - uses: actions/setup-python@v5 - with: - python-version: '3.10' - - name: Install tox - run: pip install tox - - name: run tox - run: tox -e generate - - - name: Install towncrier - run: pip install towncrier==25.8.0 - - - name: Generate changelog - run: towncrier build --yes --version "$VERSION" --dir "$(dirname $CHANGELOG)" - - - name: Use CLA approved github bot - run: .github/scripts/use-cla-approved-github-bot.sh - - - uses: actions/create-github-app-token@df432ceedc7162793a195dd1713ff69aefc7379e # v2.0.6 - id: otelbot-token-main - with: - app-id: ${{ vars.OTELBOT_APP_ID }} - private-key: ${{ secrets.OTELBOT_PRIVATE_KEY }} - - - name: Create pull request against main - env: - # not using secrets.GITHUB_TOKEN since pull requests from that token do not run workflows - GITHUB_TOKEN: ${{ steps.otelbot-token-main.outputs.token }} - run: | - message="Update ${PACKAGE_NAME} version to v${NEXT_VERSION}" - body="Update \`${PACKAGE_NAME}\` version to v\`${NEXT_VERSION}\`." - branch="otelbot/update-${PACKAGE_NAME}-version-to-v${NEXT_VERSION}" - - git commit -a -m "$message" - git push origin HEAD:$branch - gh pr create --title "$message" \ - --body "$body" \ - --head $branch \ - --base main +jobs: + prepare: + uses: ./.github/workflows/_prepare-release-package.yml + with: + package: ${{ inputs.package }} + secrets: inherit diff --git a/.github/workflows/package-release.yml b/.github/workflows/package-release.yml index 6c7c89d7..ab76d9de 100644 --- a/.github/workflows/package-release.yml +++ b/.github/workflows/package-release.yml @@ -5,156 +5,26 @@ on: package: type: choice options: - - opentelemetry-instrumentation-anthropic - - opentelemetry-instrumentation-claude-agent-sdk + - opentelemetry-instrumentation-genai-anthropic + - opentelemetry-instrumentation-genai-claude-agent-sdk - opentelemetry-instrumentation-google-genai - - opentelemetry-instrumentation-langchain - - opentelemetry-instrumentation-openai-agents-v2 - - opentelemetry-instrumentation-openai-v2 - - opentelemetry-instrumentation-weaviate + - opentelemetry-instrumentation-genai-langchain + - opentelemetry-instrumentation-genai-openai-agents + - opentelemetry-instrumentation-genai-openai + - opentelemetry-instrumentation-genai-weaviate-client - opentelemetry-util-genai description: 'Package to be released' required: true + permissions: contents: read + run-name: "[Package][${{ inputs.package }}] Release" + jobs: release: - permissions: - contents: write # required for creating releases - pull-requests: write # required for creating pull requests - runs-on: ubuntu-latest - steps: - - run: | - if [[ $GITHUB_REF_NAME != package-release/${{ inputs.package }}* ]]; then - echo this workflow should only be run against package-release/${{ inputs.package }}* branches - exit 1 - fi - - - uses: actions/checkout@v4 - - - name: Set environment variables - run: | - version=$(./scripts/eachdist.py version --package ${{ inputs.package }}) - if [[ $version =~ ^([0-9]+)\.([0-9]+)\.([0-9]+) ]]; then - major="${BASH_REMATCH[1]}" - minor="${BASH_REMATCH[2]}" - patch="${BASH_REMATCH[3]}" - if [[ $patch != 0 ]]; then - prior_version_when_patch="${major}.${minor}.$((patch - 1))" - fi - elif [[ $version =~ ^([0-9]+)\.([0-9]+)b([0-9]+)$ ]]; then - major="${BASH_REMATCH[1]}" - minor="${BASH_REMATCH[2]}" - patch="${BASH_REMATCH[3]}" - - if [[ $patch != 0 ]]; then - prior_version_when_patch="${major}.${minor}b$((patch - 1))" - fi - else - echo "unexpected version: $version" - exit 1 - fi - - path=$(./scripts/eachdist.py find-package --package ${{ inputs.package }}) - echo "CHANGELOG=./$path/CHANGELOG.md" >> $GITHUB_ENV - echo "PACKAGE_NAME=${{ inputs.package }}" >> $GITHUB_ENV - echo "VERSION=$version" >> $GITHUB_ENV - echo "RELEASE_TAG=${{ inputs.package }}==$version" >> $GITHUB_ENV - echo "PRIOR_VERSION_WHEN_PATCH=$prior_version_when_patch" >> $GITHUB_ENV - - # check out main branch to verify there won't be problems with merging the change log - # at the end of this workflow - - uses: actions/checkout@v4 - with: - ref: main - - - name: Check that change log update was merged to main - run: | - if [[ -z $PRIOR_VERSION_WHEN_PATCH ]]; then - # not making a patch release - if ! grep --quiet "^## Version ${VERSION}" ${CHANGELOG}; then - echo the pull request generated by prepare-release-branch.yml needs to be merged first - exit 1 - fi - fi - - # back to the release branch - - uses: actions/checkout@v4 - - # next few steps publish to pypi - - uses: actions/setup-python@v5 - with: - python-version: '3.10' - - - name: Install uv - uses: astral-sh/setup-uv@08807647e7069bb48b6ef5acd8ec9567f424441b # v8.1.0 - - - name: Build wheels - run: ./scripts/build_a_package.sh - - - name: Install twine - run: | - pip install twine - - - name: Publish to PyPI - env: - TWINE_USERNAME: '__token__' - TWINE_PASSWORD: ${{ secrets.pypi_password }} - run: | - twine upload --skip-existing --verbose dist/* - - - name: Generate release notes - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - run: ./scripts/generate_release_notes.sh - - - name: Create GitHub release - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - run: | - gh release create --target $GITHUB_REF_NAME \ - --title "${PACKAGE_NAME} ${VERSION}" \ - --notes-file /tmp/release-notes.txt \ - $RELEASE_TAG - - - uses: actions/checkout@v4 - with: - ref: main - - - name: Copy change log updates to main - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - run: ./scripts/merge_changelog_to_main.sh - - - name: Use CLA approved github bot - run: .github/scripts/use-cla-approved-github-bot.sh - - - uses: actions/create-github-app-token@df432ceedc7162793a195dd1713ff69aefc7379e # v2.0.6 - id: otelbot-token - with: - app-id: ${{ vars.OTELBOT_APP_ID }} - private-key: ${{ secrets.OTELBOT_PRIVATE_KEY }} - - - name: Create pull request against main - env: - # not using secrets.GITHUB_TOKEN since pull requests from that token do not run workflows - GITHUB_TOKEN: ${{ steps.otelbot-token.outputs.token }} - run: | - message="Copy changelog updates from $GITHUB_REF_NAME" - body="Copy changelog updates from \`$GITHUB_REF_NAME\`." - branch="otelbot/changelog-${GITHUB_REF_NAME//\//-}" - - if [[ -z $PRIOR_VERSION_WHEN_PATCH ]]; then - if git diff --quiet; then - echo there are no updates needed to the change log on main, not creating pull request - exit 0 # success - fi - fi - - git commit -a -m "$message" - git push origin HEAD:$branch - gh pr create --title "$message" \ - --body "$body" \ - --head $branch \ - --base main + uses: ./.github/workflows/_release-package.yml + with: + package: ${{ inputs.package }} + bump_after_release: true + secrets: inherit diff --git a/.github/workflows/release-all-prepare.yml b/.github/workflows/release-all-prepare.yml new file mode 100644 index 00000000..4832413c --- /dev/null +++ b/.github/workflows/release-all-prepare.yml @@ -0,0 +1,92 @@ +name: "[All] Prepare release" +on: + workflow_dispatch: + inputs: + bump_type: + type: choice + options: + - minor + - none + description: > + Reserved for future per-package bump granularity. Prepare always strips + .dev and builds changelogs for packages with towncrier fragments. + default: minor + +permissions: + contents: read + +run-name: "[All] Prepare release" + +jobs: + prepare: + permissions: + contents: write + pull-requests: write + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Verify branch + run: | + if [[ "$GITHUB_REF_NAME" != "main" ]]; then + echo "This workflow should only be run against main" + exit 1 + fi + + - name: Find packages with changelog fragments + id: packages + run: | + set -e + mapfile -t packages < <(./scripts/eachdist.py packages-with-changelog-fragments) + if [[ ${#packages[@]} -eq 0 ]]; then + echo "No packages with changelog fragments found" + exit 1 + fi + printf '%s\n' "${packages[@]}" + { + echo "packages<> "$GITHUB_OUTPUT" + + - name: Set up Python 3.10 + uses: actions/setup-python@v5 + with: + python-version: '3.10' + + - name: Install tox and towncrier + run: pip install tox towncrier==25.8.0 + + - name: Prepare packages for release + run: | + while IFS= read -r package; do + [[ -z "$package" ]] && continue + echo "Preparing ${package}" + ./scripts/prepare_package_for_release.sh "$package" + done <<< "${{ steps.packages.outputs.packages }}" + + - name: Use CLA approved github bot + run: .github/scripts/use-cla-approved-github-bot.sh + + - uses: actions/create-github-app-token@df432ceedc7162793a195dd1713ff69aefc7379e # v2.0.6 + id: otelbot-token + with: + app-id: ${{ vars.OTELBOT_APP_ID }} + private-key: ${{ secrets.OTELBOT_PRIVATE_KEY }} + + - name: Create combined prepare pull request + env: + GITHUB_TOKEN: ${{ steps.otelbot-token.outputs.token }} + run: | + message="Prepare release for all packages with changelog fragments" + body="Prepare all packages that have towncrier changelog fragments for release." + branch="otelbot/prepare-release-all-$(date +%Y%m%d)" + + git commit -a -m "$message" + git push origin HEAD:"$branch" + pr_url=$(gh pr create --title "$message" \ + --body "$body" \ + --head "$branch" \ + --base main \ + --json url --jq .url) + gh pr edit "$pr_url" --add-label release diff --git a/.github/workflows/release-all.yml b/.github/workflows/release-all.yml new file mode 100644 index 00000000..b4acb7da --- /dev/null +++ b/.github/workflows/release-all.yml @@ -0,0 +1,142 @@ +name: "[All] Release" +on: + workflow_dispatch: + pull_request: + types: [closed] + branches: [main] + +permissions: + contents: read + +run-name: "[All] Release" + +jobs: + enumerate: + if: | + github.event_name == 'workflow_dispatch' || + (github.event.pull_request.merged == true && + contains(github.event.pull_request.labels.*.name, 'release')) + runs-on: ubuntu-latest + outputs: + packages: ${{ steps.enumerate.outputs.packages }} + ref: ${{ steps.release_ref.outputs.ref }} + steps: + - name: Set release ref + id: release_ref + run: echo "ref=${{ github.event.pull_request.merge_commit_sha || github.sha }}" >> "$GITHUB_OUTPUT" + + - uses: actions/checkout@v4 + with: + ref: ${{ steps.release_ref.outputs.ref }} + + - name: Verify manual dispatch runs on main + if: github.event_name == 'workflow_dispatch' + run: | + if [[ "$GITHUB_REF_NAME" != "main" ]]; then + echo "This workflow should only be run against main" + exit 1 + fi + + - name: Enumerate packages ready for release + id: enumerate + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + packages=() + while IFS= read -r package; do + [[ -z "$package" ]] && continue + version=$(./scripts/eachdist.py version --package "$package") + if [[ "$version" == *.dev ]]; then + echo "Skipping ${package}: version still has .dev suffix (${version})" + continue + fi + tag="${package}==${version}" + if gh release view "$tag" >/dev/null 2>&1; then + echo "Skipping ${package}: tag ${tag} already exists" + continue + fi + echo "Ready: ${package} v${version}" + packages+=("$package") + done < <(./scripts/eachdist.py list-release-packages) + + if [[ ${#packages[@]} -eq 0 ]]; then + echo "No packages ready for release" + exit 1 + fi + + json=$(printf '%s\n' "${packages[@]}" | jq -R . | jq -s -c .) + echo "packages=${json}" >> "$GITHUB_OUTPUT" + + publish: + needs: enumerate + strategy: + fail-fast: false + matrix: + package: ${{ fromJson(needs.enumerate.outputs.packages) }} + uses: ./.github/workflows/_release-package.yml + with: + package: ${{ matrix.package }} + bump_after_release: false + ref: ${{ needs.enumerate.outputs.ref }} + on_main: true + secrets: inherit + + bump-main: + needs: [enumerate, publish] + if: always() && needs.enumerate.result == 'success' + permissions: + contents: write + pull-requests: write + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + with: + ref: main + + - name: Use CLA approved github bot + run: .github/scripts/use-cla-approved-github-bot.sh + + - uses: actions/create-github-app-token@df432ceedc7162793a195dd1713ff69aefc7379e # v2.0.6 + id: otelbot-token + with: + app-id: ${{ vars.OTELBOT_APP_ID }} + private-key: ${{ secrets.OTELBOT_PRIVATE_KEY }} + + - name: Bump released packages to next dev versions + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + GITHUB_TOKEN: ${{ steps.otelbot-token.outputs.token }} + PACKAGES_JSON: ${{ needs.enumerate.outputs.packages }} + run: | + bumped=() + while IFS= read -r package; do + [[ -z "$package" ]] && continue + version=$(./scripts/eachdist.py version --package "$package") + tag="${package}==${version}" + if ! gh release view "$tag" >/dev/null 2>&1; then + echo "Skipping ${package}: release tag ${tag} not found" + continue + fi + if [[ "$version" == *.dev ]]; then + echo "Skipping ${package}: already bumped to ${version}" + continue + fi + ./scripts/bump_package_dev_version.sh "$package" + bumped+=("$package") + done < <(echo "$PACKAGES_JSON" | jq -r '.[]') + + if [[ ${#bumped[@]} -eq 0 ]]; then + echo "No packages to bump" + exit 0 + fi + + message="Bump packages to next dev versions after release" + body="Bump packages to the next \`.dev\` version after release:\n\n$(printf '%s\n' "${bumped[@]}")" + branch="otelbot/bump-after-release-all-$(date +%Y%m%d)" + + git commit -a -m "$message" + git push origin HEAD:"$branch" + gh pr create --title "$message" \ + --body "$body" \ + --head "$branch" \ + --base main diff --git a/RELEASING.md b/RELEASING.md index 16d87990..fa776eed 100644 --- a/RELEASING.md +++ b/RELEASING.md @@ -1,12 +1,31 @@ # Release process -Every package in this repo releases independently. There is no coordinated -bulk release across packages. +Every package releases independently. The default path is a coordinated +**release-all** workflow; per-package workflows are for urgent or partial +releases. -Releases are driven by GitHub Actions workflows ported from -`opentelemetry-python-contrib`. They handle version bumps, changelog -generation (via [towncrier](https://towncrier.readthedocs.io/)), tagging, -PyPI publishing, GitHub releases, and back-merging the changelog to `main`. +Releases are **tag-from-`main`**: each publish creates a tag +(`==`) pointing at the release commit on `main`. Backport +branches (`package-release//v*`) are created lazily from an old tag only +when patching an older minor line — not for every release. + +Releases are driven by GitHub Actions workflows. They handle version bumps, +changelog generation (via [towncrier](https://towncrier.readthedocs.io/)), +tagging, PyPI publishing, GitHub releases, and changelog updates on `main`. + +## Release model + +Unlike `opentelemetry-python-contrib`, we do not maintain a long-lived release +branch for every minor. Normal releases tag `main` directly; backport branches +are created on demand from an existing tag when patching an older minor. + +| | opentelemetry-python-contrib | This repo | +|---|---|---| +| Normal release | Long-lived `package-release//v*` branch | Tag on `main` | +| Tag target | Commit on the release branch | Commit on `main` | +| Patch in current line | Commits + tags on the release branch | Tags on `main` | +| Backport to older minor | Same branch (already exists) | Branch from old tag (lazy) | +| Branch sprawl | One branch per package per minor | Branches only for backports | ## Prerequisites (one-time, repo-level) @@ -16,54 +35,125 @@ These must be configured by maintainers before the workflows can run: (repo or org secret) — credentials for the `otelbot` GitHub App. The workflows commit and open PRs through this App so the resulting PRs trigger CI (a plain `GITHUB_TOKEN` doesn't). -- **`pypi_password`** (repo secret) — a PyPI API token with publish rights - on the relevant packages. Tracked in #15: migrating to PyPI trusted - publishing once package names are decided. -- **Branch protection** on `package-release/*/v*` branches so changes only - land via reviewed PRs. +- **PyPI Trusted Publishing** — each publishable package needs a trusted + publisher on PyPI (see [PyPI publishing setup](#pypi-publishing-setup)). + No long-lived PyPI API token is stored in GitHub secrets. +- **`pypi` GitHub environment** — release jobs deploy through this + environment so publishing can be restricted (branch rules, required + reviewers). See [GitHub environment setup](#github-environment-setup). +- **Branch protection** on `package-release/*/v*` branches so backport changes + only land via reviewed PRs. + +### PyPI publishing setup + +For each publishable package, add a trusted publisher on PyPI +(*Manage* → *Publishing* → *Add a new pending publisher*): + +| Field | Value | +|-------|-------| +| PyPI project name | e.g. `opentelemetry-util-genai` | +| Owner | `open-telemetry` | +| Repository name | `opentelemetry-python-genai` | +| Workflow name | `_release-package.yml` | +| Environment name | `pypi` | + +All packages share the same workflow and environment. Register one publisher +entry per PyPI project. The first upload from CI activates the publisher. + +Publishable packages (from `eachdist.ini`): + +- `opentelemetry-instrumentation-genai-anthropic` +- `opentelemetry-instrumentation-genai-claude-agent-sdk` +- `opentelemetry-instrumentation-google-genai` +- `opentelemetry-instrumentation-genai-langchain` +- `opentelemetry-instrumentation-genai-openai` +- `opentelemetry-instrumentation-genai-openai-agents` +- `opentelemetry-instrumentation-genai-weaviate-client` +- `opentelemetry-util-genai` + +### GitHub environment setup + +Create a **`pypi`** environment under repository *Settings* → *Environments*. + +Recommended restrictions: -## Minor/major release flow +- **Deployment branches**: limit to `main` and `package-release/**` (backport + releases run from backport branches). +- **Required reviewers** (optional): gate PyPI uploads behind maintainer + approval. Bulk `[All] Release` runs one matrix job per package; each job + waits on the environment, so consider whether reviewers should approve once + per run or per matrix leg. -For releasing `` (e.g. `opentelemetry-instrumentation-anthropic`): +Release jobs in `_release-package.yml` set `environment: pypi` and request +`id-token: write` so GitHub can mint a short-lived OIDC token for PyPI. + +## Bulk release (default) + +For releasing every package that has towncrier changelog fragments: 1. Run the + [`[All] Prepare release`](./.github/workflows/release-all-prepare.yml) + workflow against `main`. + - Finds packages with fragments under `.changelog/`. + - Opens one combined PR on `main` that drops `.dev` suffixes and runs + `towncrier build` for each eligible package. + - Labels the PR `release`. +2. Review and merge the prepare PR. +3. The + [`[All] Release`](./.github/workflows/release-all.yml) + workflow runs automatically when a labelled prepare PR merges (or trigger it + manually against `main`). + - Publishes each ready package to PyPI. + - Creates a GitHub release tag (`==`) on `main` for each. + - Opens a PR bumping released packages back to the next `.dev` version. + +Packages without changelog fragments are skipped during prepare and logged in +the workflow output. + +## Per-package release + +Use when only one package needs to ship, or the rest of the workspace is not +ready for a bulk release. + +1. Run [`[Package] Prepare release`](./.github/workflows/package-prepare-release.yml) - workflow against `main`. Select the package from the dropdown. - - Creates a long-term release branch - `package-release//v..x` (or `v.bx` for unstable). - - Opens **two PRs**: - - PR against the release branch: drops the `.dev` suffix from - `version.py`, builds the changelog via `towncrier build`. - - PR against `main`: bumps `version.py` to the next `.dev` version, - builds the changelog via `towncrier build` (so fragments don't - carry over to the next release cycle). -2. Review and merge **both** PRs. -3. Run the + against `main`. Select the package from the dropdown. + - Opens a PR on `main` that drops the `.dev` suffix and runs + `towncrier build`. +2. Review and merge the prepare PR. +3. Run + [`[Package] Release`](./.github/workflows/package-release.yml) + against `main`. + - Builds the wheel, publishes to PyPI, creates the GitHub release tag, and + opens PRs for any changelog date updates and the next `.dev` bump. + +## Patch release (current minor line) + +1. Land the fix on `main` as a normal PR (with a towncrier fragment). +2. Run + [`[Package] Prepare patch release`](./.github/workflows/package-prepare-patch-release.yml) + against `main`. + - Same mechanics as prepare release: drops `.dev`, runs `towncrier build`. +3. Review and merge the prepare PR. +4. Run [`[Package] Release`](./.github/workflows/package-release.yml) - workflow against the `package-release//v*` branch. - - Verifies the changelog PR was merged to `main`. - - Builds the wheel via `scripts/build_a_package.sh` and publishes to - PyPI via `twine`. - - Creates a GitHub release tagged `==`. - - Opens a back-merge PR against `main` copying the resolved changelog - section (in case any edits landed on the release branch). -4. Review and merge the back-merge PR if one was created. - -## Patch release flow - -1. Check out the package's existing release branch - `package-release//v..x`. -2. Land any patch PRs against this branch (cherry-pick or direct). -3. Run the + against `main`. + +## Backport patch (older minor line) + +1. Create `package-release//v.bx` from the `==.b` + tag if it does not exist yet. +2. Cherry-pick or develop the fix on the branch. +3. Run [`[Package] Prepare patch release`](./.github/workflows/package-prepare-patch-release.yml) - workflow with the release branch selected. - - Opens a PR against the release branch bumping the patch version and - running `towncrier build` against the patch fragments. -4. Review and merge the PR. -5. Run the + against the backport branch. + - Bumps the patch version and runs `towncrier build`. +4. Review and merge the prepare PR into the backport branch. +5. Run [`[Package] Release`](./.github/workflows/package-release.yml) - workflow against the release branch. Same effect as for a - minor/major release. + against the backport branch. + - Tags the backport branch and opens a PR copying changelog updates to + `main`. ## Pre-existing static `## Unreleased` entries @@ -77,28 +167,38 @@ this. ## Claiming a PyPI namespace for a new package -When a new package is introduced, release the current `.dev` version under -the `opentelemetry` PyPI org to prevent name-squatting. Do this shortly +When a new package is introduced, create the PyPI project and register a +trusted publisher (see above) before the first CI release. Optionally upload +the current `.dev` version manually once to prevent name-squatting, shortly after the introductory PR lands on `main`. ## Troubleshooting +### No packages found during `[All] Prepare release` + +At least one publishable package needs a towncrier fragment under +`.changelog/` (any file other than `.gitkeep` / `.gitignore`). + ### PyPI publish failed mid-workflow -Switch to the release branch locally and re-run the publish step manually: +Re-run the release workflow (`[Package] Release` or `[All] Release`). Trusted +Publishing only works from GitHub Actions — there is no repo-stored PyPI token +for manual `twine upload`. + +If the wheel was built but upload failed, fix the underlying issue (PyPI +project missing, trusted publisher misconfigured, environment approval pending) +and re-run. The workflow uses `skip-existing`, so a partial upload is safe to +retry. + +After a successful PyPI upload, re-running picks up remaining steps (GitHub +release tag + follow-up PRs) if those failed. -```sh -git checkout package-release//v..x -./scripts/build_a_package.sh -twine upload --skip-existing --verbose dist/* -``` +### Version still has a `.dev` suffix at release time -Then re-run the `[Package] Release` workflow to pick up the remaining -steps (GitHub release + back-merge PR). +Merge the prepare PR first. Release workflows require a non-`.dev` version in +`version.py`. ## Out of scope -- A `backport` workflow (none yet — add when there's a real long-term - release branch to backport into). -- Coordinated cross-package releases (every package here is independent; - `eachdist.ini` lists all publishable packages under `[exclude_release]`). +- A `backport` workflow (create backport branches manually from release tags + when needed). diff --git a/scripts/build_a_package.sh b/scripts/build_a_package.sh index 2375018b..2fa883e0 100755 --- a/scripts/build_a_package.sh +++ b/scripts/build_a_package.sh @@ -15,8 +15,8 @@ # limitations under the License. # This script builds wheels for a single package when triggered from per-package release -# GitHub workflow (see .github/package-release.yml). -# The wheel is then published to PyPI by the workflow. +# GitHub workflow (see .github/workflows/_release-package.yml). +# The wheel is then published to PyPI via Trusted Publishing in that workflow. set -ev diff --git a/scripts/bump_package_dev_version.sh b/scripts/bump_package_dev_version.sh new file mode 100755 index 00000000..48d22d14 --- /dev/null +++ b/scripts/bump_package_dev_version.sh @@ -0,0 +1,37 @@ +#!/bin/bash + +# Bump a package from a released version (no .dev suffix) to the next .dev version. + +set -euo pipefail + +package="${1:?usage: bump_package_dev_version.sh PACKAGE}" + +path="./$(./scripts/eachdist.py find-package --package "$package")" +version="$(./scripts/eachdist.py version --package "$package")" +version_file="$(find "$path" -type f -path "**/version.py")" + +if [[ "$version" =~ ^([0-9]+)\.([0-9]+)\.([0-9]+) ]]; then + major="${BASH_REMATCH[1]}" + minor="${BASH_REMATCH[2]}" + patch="${BASH_REMATCH[3]}" + if [[ "$patch" != 0 ]]; then + next_version="${major}.${minor}.$((patch + 1)).dev" + else + next_version="${major}.$((minor + 1)).0.dev" + fi +elif [[ "$version" =~ ^([0-9]+)\.([0-9]+)b([0-9]+)$ ]]; then + major="${BASH_REMATCH[1]}" + minor="${BASH_REMATCH[2]}" + patch="${BASH_REMATCH[3]}" + if [[ "$patch" != 0 ]]; then + next_version="${major}.${minor}b$((patch + 1)).dev" + else + next_version="${major}.$((minor + 1))b0.dev" + fi +else + echo "unexpected version: ${version}" + exit 1 +fi + +sed -i -E "s/__version__\\s*=\\s*\"${version}\"/__version__ = \"${next_version}\"/g" "$version_file" +echo "Bumped ${package} from ${version} to ${next_version}" diff --git a/scripts/eachdist.py b/scripts/eachdist.py index 760befae..25751db0 100755 --- a/scripts/eachdist.py +++ b/scripts/eachdist.py @@ -292,6 +292,19 @@ def setup_instparser(instparser): required=True, help="Name of the package to find", ) + + listreleaseparser = subparsers.add_parser( + "list-release-packages", + help="List publishable package names from eachdist.ini.", + ) + listreleaseparser.set_defaults(func=list_release_packages_args) + + fragmentsparser = subparsers.add_parser( + "packages-with-changelog-fragments", + help="List publishable packages with towncrier changelog fragments.", + ) + fragmentsparser.set_defaults(func=packages_with_changelog_fragments_args) + return parser.parse_args(args) @@ -846,6 +859,42 @@ def find_package_args(args): sys.exit(1) +def list_release_package_names() -> list[str]: + cfg = ConfigParser() + cfg.read(str(find_projectroot() / "eachdist.ini")) + return cfg["exclude_release"]["packages"].split() + + +def list_release_packages_args(args): + for package in list_release_package_names(): + print(package) + + +def packages_with_changelog_fragments_args(args): + root = find_projectroot() + packages: list[str] = [] + for package_name in list_release_package_names(): + for package in find_targets_unordered(root): + if package_name != package.name: + continue + changelog_dir = package / ".changelog" + if not changelog_dir.is_dir(): + continue + fragments = [ + fragment + for fragment in changelog_dir.iterdir() + if fragment.is_file() + and fragment.name not in {".gitignore", ".gitkeep"} + ] + if fragments: + packages.append(package_name) + break + if not packages: + sys.exit(1) + for package in packages: + print(package) + + def main(): args = parse_args() args.func(args) diff --git a/scripts/prepare_package_for_patch_release.sh b/scripts/prepare_package_for_patch_release.sh new file mode 100755 index 00000000..2557075a --- /dev/null +++ b/scripts/prepare_package_for_patch_release.sh @@ -0,0 +1,49 @@ +#!/bin/bash + +# Prepare a patch release for a package on a backport branch: bump the patch +# version and run towncrier. Expects the current version without a .dev suffix. + +set -euo pipefail + +package="${1:?usage: prepare_package_for_patch_release.sh PACKAGE}" + +path="./$(./scripts/eachdist.py find-package --package "$package")" +changelog="${path}/CHANGELOG.md" + +if [[ ! -f "$changelog" ]]; then + echo "missing ${changelog}" + exit 1 +fi + +version="$(./scripts/eachdist.py version --package "$package")" + +version_file="$(find "$path" -type f -path "**/version.py")" +file_count="$(echo "$version_file" | wc -l | tr -d ' ')" + +if [[ "$file_count" -ne 1 ]]; then + echo "Error: expected one version file, found ${file_count}" + echo "$version_file" + exit 1 +fi + +if [[ "$version" =~ ^([0-9]+)\.([0-9]+)\.([0-9]+) ]]; then + major="${BASH_REMATCH[1]}" + minor="${BASH_REMATCH[2]}" + patch="${BASH_REMATCH[3]}" + next_version="${major}.${minor}.$((patch + 1))" +elif [[ "$version" =~ ^([0-9]+)\.([0-9]+)b([0-9]+)$ ]]; then + major="${BASH_REMATCH[1]}" + minor="${BASH_REMATCH[2]}" + patch="${BASH_REMATCH[3]}" + next_version="${major}.${minor}b$((patch + 1))" +else + echo "unexpected version: '${version}'" + exit 1 +fi + +sed -i -E "s/__version__\\s*=\\s*\"${version}\"/__version__ = \"${next_version}\"/g" "$version_file" + +tox -e generate +towncrier build --yes --version "$next_version" --dir "$(dirname "$changelog")" + +echo "Prepared ${package} for patch release v${next_version}" diff --git a/scripts/prepare_package_for_release.sh b/scripts/prepare_package_for_release.sh new file mode 100755 index 00000000..e64252e4 --- /dev/null +++ b/scripts/prepare_package_for_release.sh @@ -0,0 +1,41 @@ +#!/bin/bash + +# Prepare a single package for release on the current branch: drop the .dev suffix +# from version.py and run towncrier. Called from release prepare workflows. + +set -euo pipefail + +package="${1:?usage: prepare_package_for_release.sh PACKAGE}" + +path="./$(./scripts/eachdist.py find-package --package "$package")" +changelog="${path}/CHANGELOG.md" + +if [[ ! -f "$changelog" ]]; then + echo "missing ${changelog}" + exit 1 +fi + +version_dev="$(./scripts/eachdist.py version --package "$package")" + +if [[ ! "$version_dev" =~ ^([0-9]+)\.([0-9]+)[\.|b]{1}([0-9]+).*\.dev$ ]]; then + echo "unexpected version: ${version_dev}" + exit 1 +fi + +version="${version_dev%.dev}" + +version_file="$(find "$path" -type f -path "**/version.py")" +file_count="$(echo "$version_file" | wc -l | tr -d ' ')" + +if [[ "$file_count" -ne 1 ]]; then + echo "Error: expected one version file, found ${file_count}" + echo "$version_file" + exit 1 +fi + +sed -i -E "s/__version__\\s*=\\s*\"${version}\\.dev\"/__version__ = \"${version}\"/g" "$version_file" + +tox -e generate +towncrier build --yes --version "$version" --dir "$(dirname "$changelog")" + +echo "Prepared ${package} for release v${version}" From 44f60847a38232d7a163f2153fe9ba87f787820e Mon Sep 17 00:00:00 2001 From: Mike Goldsmith Date: Mon, 15 Jun 2026 17:22:24 +0100 Subject: [PATCH 8/9] Simplify release docs and release package list. Rename eachdist.ini exclude_release to release_packages, omit scaffolding packages from release workflows, and focus RELEASING.md on release steps. --- .../package-prepare-patch-release.yml | 2 - .github/workflows/package-prepare-release.yml | 2 - .github/workflows/package-release.yml | 2 - RELEASING.md | 90 ++++++------------- eachdist.ini | 5 +- scripts/eachdist.py | 18 ++-- 6 files changed, 37 insertions(+), 82 deletions(-) diff --git a/.github/workflows/package-prepare-patch-release.yml b/.github/workflows/package-prepare-patch-release.yml index 0a5b7baa..9f473291 100644 --- a/.github/workflows/package-prepare-patch-release.yml +++ b/.github/workflows/package-prepare-patch-release.yml @@ -6,12 +6,10 @@ on: type: choice options: - opentelemetry-instrumentation-genai-anthropic - - opentelemetry-instrumentation-genai-claude-agent-sdk - opentelemetry-instrumentation-google-genai - opentelemetry-instrumentation-genai-langchain - opentelemetry-instrumentation-genai-openai-agents - opentelemetry-instrumentation-genai-openai - - opentelemetry-instrumentation-genai-weaviate-client - opentelemetry-util-genai description: 'Package to be released' required: true diff --git a/.github/workflows/package-prepare-release.yml b/.github/workflows/package-prepare-release.yml index 5b1de096..b357f1f7 100644 --- a/.github/workflows/package-prepare-release.yml +++ b/.github/workflows/package-prepare-release.yml @@ -6,12 +6,10 @@ on: type: choice options: - opentelemetry-instrumentation-genai-anthropic - - opentelemetry-instrumentation-genai-claude-agent-sdk - opentelemetry-instrumentation-google-genai - opentelemetry-instrumentation-genai-langchain - opentelemetry-instrumentation-genai-openai-agents - opentelemetry-instrumentation-genai-openai - - opentelemetry-instrumentation-genai-weaviate-client - opentelemetry-util-genai description: 'Package to be released' required: true diff --git a/.github/workflows/package-release.yml b/.github/workflows/package-release.yml index ab76d9de..374dfe1f 100644 --- a/.github/workflows/package-release.yml +++ b/.github/workflows/package-release.yml @@ -6,12 +6,10 @@ on: type: choice options: - opentelemetry-instrumentation-genai-anthropic - - opentelemetry-instrumentation-genai-claude-agent-sdk - opentelemetry-instrumentation-google-genai - opentelemetry-instrumentation-genai-langchain - opentelemetry-instrumentation-genai-openai-agents - opentelemetry-instrumentation-genai-openai - - opentelemetry-instrumentation-genai-weaviate-client - opentelemetry-util-genai description: 'Package to be released' required: true diff --git a/RELEASING.md b/RELEASING.md index fa776eed..f9c3f862 100644 --- a/RELEASING.md +++ b/RELEASING.md @@ -27,66 +27,6 @@ are created on demand from an existing tag when patching an older minor. | Backport to older minor | Same branch (already exists) | Branch from old tag (lazy) | | Branch sprawl | One branch per package per minor | Branches only for backports | -## Prerequisites (one-time, repo-level) - -These must be configured by maintainers before the workflows can run: - -- **`OTELBOT_APP_ID`** (repo or org variable) and **`OTELBOT_PRIVATE_KEY`** - (repo or org secret) — credentials for the `otelbot` GitHub App. The - workflows commit and open PRs through this App so the resulting PRs - trigger CI (a plain `GITHUB_TOKEN` doesn't). -- **PyPI Trusted Publishing** — each publishable package needs a trusted - publisher on PyPI (see [PyPI publishing setup](#pypi-publishing-setup)). - No long-lived PyPI API token is stored in GitHub secrets. -- **`pypi` GitHub environment** — release jobs deploy through this - environment so publishing can be restricted (branch rules, required - reviewers). See [GitHub environment setup](#github-environment-setup). -- **Branch protection** on `package-release/*/v*` branches so backport changes - only land via reviewed PRs. - -### PyPI publishing setup - -For each publishable package, add a trusted publisher on PyPI -(*Manage* → *Publishing* → *Add a new pending publisher*): - -| Field | Value | -|-------|-------| -| PyPI project name | e.g. `opentelemetry-util-genai` | -| Owner | `open-telemetry` | -| Repository name | `opentelemetry-python-genai` | -| Workflow name | `_release-package.yml` | -| Environment name | `pypi` | - -All packages share the same workflow and environment. Register one publisher -entry per PyPI project. The first upload from CI activates the publisher. - -Publishable packages (from `eachdist.ini`): - -- `opentelemetry-instrumentation-genai-anthropic` -- `opentelemetry-instrumentation-genai-claude-agent-sdk` -- `opentelemetry-instrumentation-google-genai` -- `opentelemetry-instrumentation-genai-langchain` -- `opentelemetry-instrumentation-genai-openai` -- `opentelemetry-instrumentation-genai-openai-agents` -- `opentelemetry-instrumentation-genai-weaviate-client` -- `opentelemetry-util-genai` - -### GitHub environment setup - -Create a **`pypi`** environment under repository *Settings* → *Environments*. - -Recommended restrictions: - -- **Deployment branches**: limit to `main` and `package-release/**` (backport - releases run from backport branches). -- **Required reviewers** (optional): gate PyPI uploads behind maintainer - approval. Bulk `[All] Release` runs one matrix job per package; each job - waits on the environment, so consider whether reviewers should approve once - per run or per matrix leg. - -Release jobs in `_release-package.yml` set `environment: pypi` and request -`id-token: write` so GitHub can mint a short-lived OIDC token for PyPI. - ## Bulk release (default) For releasing every package that has towncrier changelog fragments: @@ -165,12 +105,32 @@ release section produced by `towncrier build` (or convert them into fragments first). The do-not-edit comment in each `CHANGELOG.md` flags this. -## Claiming a PyPI namespace for a new package +## Adding a new publishable package + +When a new package is ready to ship: + +1. Add its name to the `packages=` list under `[release_packages]` in + `eachdist.ini`. Packages not listed here are skipped by the release + workflows. +2. Add the package to the dropdown options in the per-package workflow files + (`package-prepare-release.yml`, `package-release.yml`, + `package-prepare-patch-release.yml`). +3. Create the PyPI project and register a trusted publisher (*Manage* → + *Publishing* → *Add a new pending publisher*): + +| Field | Value | +|-------|-------| +| PyPI project name | e.g. `opentelemetry-util-genai` | +| Owner | `open-telemetry` | +| Repository name | `opentelemetry-python-genai` | +| Workflow name | `_release-package.yml` | +| Environment name | `pypi` | + +4. Optionally upload the current `.dev` version manually once to prevent + name-squatting, shortly after the introductory PR lands on `main`. -When a new package is introduced, create the PyPI project and register a -trusted publisher (see above) before the first CI release. Optionally upload -the current `.dev` version manually once to prevent name-squatting, shortly -after the introductory PR lands on `main`. +All packages share the same workflow and environment. The first upload from CI +activates the publisher. ## Troubleshooting diff --git a/eachdist.ini b/eachdist.ini index 7bdb949b..65f83f55 100644 --- a/eachdist.ini +++ b/eachdist.ini @@ -13,15 +13,14 @@ packages= all opentelemetry-util-genai -[exclude_release] +[release_packages] +# Packages the release workflows may publish (eachdist.py list-release-packages). packages= opentelemetry-instrumentation-genai-anthropic - opentelemetry-instrumentation-genai-claude-agent-sdk opentelemetry-instrumentation-google-genai opentelemetry-instrumentation-genai-langchain opentelemetry-instrumentation-genai-openai opentelemetry-instrumentation-genai-openai-agents - opentelemetry-instrumentation-genai-weaviate-client opentelemetry-util-genai [lintroots] diff --git a/scripts/eachdist.py b/scripts/eachdist.py index 25751db0..925ed235 100755 --- a/scripts/eachdist.py +++ b/scripts/eachdist.py @@ -733,10 +733,11 @@ def release_args(args): versions = args.versions updated_versions = [] - # remove excluded packages - excluded = cfg["exclude_release"]["packages"].split() + release_packages = list_release_package_names() targets = [ - target for target in targets if basename(target) not in excluded + target + for target in targets + if basename(target) not in release_packages ] for group in versions.split(","): @@ -746,7 +747,7 @@ def release_args(args): packages = None if "packages" in mcfg: packages = [ - pkg for pkg in mcfg["packages"].split() if pkg not in excluded + pkg for pkg in mcfg["packages"].split() if pkg not in release_packages ] print(f"update {group} packages to {version}") update_dependencies(targets, version, packages) @@ -763,10 +764,11 @@ def patch_release_args(args): cfg = ConfigParser() cfg.read(str(find_projectroot() / "eachdist.ini")) - # remove excluded packages - excluded = cfg["exclude_release"]["packages"].split() + release_packages = list_release_package_names() targets = [ - target for target in targets if basename(target) not in excluded + target + for target in targets + if basename(target) not in release_packages ] # stable @@ -862,7 +864,7 @@ def find_package_args(args): def list_release_package_names() -> list[str]: cfg = ConfigParser() cfg.read(str(find_projectroot() / "eachdist.ini")) - return cfg["exclude_release"]["packages"].split() + return cfg["release_packages"]["packages"].split() def list_release_packages_args(args): From 3754e74d6856ad17ac89e6484ed9e18595f15974 Mon Sep 17 00:00:00 2001 From: Mike Goldsmith Date: Tue, 16 Jun 2026 11:05:25 +0100 Subject: [PATCH 9/9] style: apply ruff format to scripts/eachdist.py Fix pre-commit ruff-format CI failure on PR #20. --- scripts/eachdist.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/scripts/eachdist.py b/scripts/eachdist.py index 925ed235..d594fc56 100755 --- a/scripts/eachdist.py +++ b/scripts/eachdist.py @@ -747,7 +747,9 @@ def release_args(args): packages = None if "packages" in mcfg: packages = [ - pkg for pkg in mcfg["packages"].split() if pkg not in release_packages + pkg + for pkg in mcfg["packages"].split() + if pkg not in release_packages ] print(f"update {group} packages to {version}") update_dependencies(targets, version, packages)