diff --git a/.changelog/.gitignore b/.changelog/.gitignore new file mode 100644 index 00000000000..f935021a8f8 --- /dev/null +++ b/.changelog/.gitignore @@ -0,0 +1 @@ +!.gitignore diff --git a/.github/workflows/changelog.yml b/.github/workflows/changelog.yml index 39fc5f8e63d..4878663dd7b 100644 --- a/.github/workflows/changelog.yml +++ b/.github/workflows/changelog.yml @@ -1,6 +1,6 @@ -# This action requires that any PR targeting the main branch should touch at -# least one CHANGELOG file. If a CHANGELOG entry is not required, add the "Skip -# Changelog" label to disable this action. +# This action requires that any PR targeting the main branch should add a +# changelog fragment file in the .changelog/ directory. If a changelog entry +# is not required, add the "Skip Changelog" label to disable this action. name: changelog @@ -22,18 +22,39 @@ jobs: steps: - uses: actions/checkout@v4 + with: + fetch-depth: 0 - - name: Check for CHANGELOG changes + - name: Fetch base branch + run: git fetch origin ${{ github.base_ref }} --depth=1 + + - name: Ensure no direct changes to CHANGELOG.md run: | - # Only the latest commit of the feature branch is available - # automatically. To diff with the base branch, we need to - # fetch that too (and we only need its latest commit). - git fetch origin ${{ github.base_ref }} --depth=1 - if [[ $(git diff --name-only FETCH_HEAD | grep CHANGELOG) ]] + if [[ $(git diff --name-only FETCH_HEAD -- 'CHANGELOG.md') ]] then - echo "A CHANGELOG was modified. Looks good!" - else - echo "No CHANGELOG was modified." - echo "Please add a CHANGELOG entry, or add the \"Skip Changelog\" label if not required." + echo "CHANGELOG.md should not be directly modified." + echo "Please add a changelog fragment file to the .changelog/ directory instead." + echo "See CONTRIBUTING.md for details." + echo "" + echo "Or add the \"Skip Changelog\" label if this job should be skipped." false fi + + - name: Install towncrier + run: pip install towncrier==25.8.0 + + - name: Check for changelog fragment + run: | + if ! towncrier check --compare-with origin/${{ github.base_ref }}; then + echo "" + echo "No changelog fragment found for this PR." + echo "Add a file named .changelog/${{ github.event.pull_request.number }}." + echo "where is one of: added, changed, deprecated, removed, fixed" + echo "See CONTRIBUTING.md for details." + echo "" + echo "Or add the \"Skip Changelog\" label if this job should be skipped." + false + fi + + - name: Preview changelog + run: towncrier build --draft --version Unreleased diff --git a/.github/workflows/prepare-patch-release.yml b/.github/workflows/prepare-patch-release.yml index 8414d821115..a4856eafdd0 100644 --- a/.github/workflows/prepare-patch-release.yml +++ b/.github/workflows/prepare-patch-release.yml @@ -14,8 +14,8 @@ jobs: steps: - uses: actions/checkout@v4 - - name: Install toml - run: pip install toml + - name: Install dependencies + run: pip install toml towncrier - run: | if [[ ! $GITHUB_REF_NAME =~ ^release/v[0-9]+\.[0-9]+\.x-0\.[0-9]+bx$ ]]; then @@ -23,11 +23,6 @@ jobs: exit 1 fi - if ! grep --quiet "^## Unreleased$" CHANGELOG.md; then - echo the change log is missing an \"Unreleased\" section - exit 1 - fi - - name: Set environment variables run: | stable_version=$(./scripts/eachdist.py version --mode stable) @@ -62,10 +57,8 @@ jobs: - name: Update version run: .github/scripts/update-version-patch.sh $STABLE_VERSION $UNSTABLE_VERSION $STABLE_VERSION_PREV $UNSTABLE_VERSION_PREV - - name: Update the change log with the approximate release date - run: | - date=$(date "+%Y-%m-%d") - sed -Ei "s/^## Unreleased$/## Version ${STABLE_VERSION}\/${UNSTABLE_VERSION} ($date)/" CHANGELOG.md + - name: Generate changelog + run: towncrier build --yes --version "$STABLE_VERSION/$UNSTABLE_VERSION" - name: Use CLA approved github bot run: .github/scripts/use-cla-approved-github-bot.sh @@ -99,3 +92,31 @@ jobs: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} run: | gh pr edit ${{ steps.create_pr.outputs.pr_url }} --add-label "prepare-release" + + - uses: actions/checkout@v4 + with: + ref: main + + - name: Use CLA approved github bot + run: .github/scripts/use-cla-approved-github-bot.sh + + - name: Backport patch release changelog to main + env: + GITHUB_TOKEN: ${{ steps.otelbot-token.outputs.token }} + run: | + release_branch="otelbot/prepare-release-${STABLE_VERSION}-${UNSTABLE_VERSION}" + message="Backport ${STABLE_VERSION}/${UNSTABLE_VERSION} changelog" + body="Backport \`${STABLE_VERSION}/${UNSTABLE_VERSION}\` changelog" + branch="otelbot/backport-changelog-from-${STABLE_VERSION}-${UNSTABLE_VERSION}" + + git fetch origin $release_branch + + # Copy the updated CHANGELOG.md from the release branch + git checkout $release_branch -- CHANGELOG.md + git commit -m "$message" + + git push origin HEAD:$branch + gh pr create --title "$message" \ + --body "$body" \ + --head $branch \ + --base main diff --git a/.github/workflows/prepare-release-branch.yml b/.github/workflows/prepare-release-branch.yml index ee8e971caf8..47874581a3f 100644 --- a/.github/workflows/prepare-release-branch.yml +++ b/.github/workflows/prepare-release-branch.yml @@ -27,11 +27,6 @@ jobs: exit 1 fi - if ! grep --quiet "^## Unreleased$" CHANGELOG.md; then - echo the change log is missing an \"Unreleased\" section - exit 1 - fi - if [[ ! -z $PRERELEASE_VERSION ]]; then stable_version=$(./scripts/eachdist.py version --mode stable) stable_version=${stable_version//.dev/} @@ -50,8 +45,8 @@ jobs: steps: - uses: actions/checkout@v4 - - name: Install toml - run: pip install toml + - name: Install dependencies + run: pip install toml towncrier - name: Create release branch env: @@ -88,10 +83,8 @@ jobs: - name: Update version run: .github/scripts/update-version.sh $STABLE_VERSION $UNSTABLE_VERSION - - name: Update the change log with the approximate release date - run: | - date=$(date "+%Y-%m-%d") - sed -Ei "s/^## Unreleased$/## Version ${STABLE_VERSION}\/${UNSTABLE_VERSION} ($date)/" CHANGELOG.md + - name: Generate changelog + run: towncrier build --yes --version "$STABLE_VERSION/$UNSTABLE_VERSION" - name: Use CLA approved github bot run: .github/scripts/use-cla-approved-github-bot.sh @@ -135,8 +128,8 @@ jobs: steps: - uses: actions/checkout@v4 - - name: Install toml - run: pip install toml + - name: Install dependencies + run: pip install toml towncrier - name: Set environment variables env: @@ -184,11 +177,8 @@ jobs: - name: Update version run: .github/scripts/update-version.sh $STABLE_NEXT_VERSION $UNSTABLE_NEXT_VERSION - - name: Update the change log on main - run: | - # the actual release date on main will be updated at the end of the release workflow - date=$(date "+%Y-%m-%d") - sed -Ei "s/^## Unreleased$/## Unreleased\n\n## Version ${STABLE_VERSION}\/${UNSTABLE_VERSION} ($date)/" CHANGELOG.md + - name: Generate changelog + run: towncrier build --yes --version "$STABLE_VERSION/$UNSTABLE_VERSION" - name: Use CLA approved github bot run: .github/scripts/use-cla-approved-github-bot.sh diff --git a/CHANGELOG.md b/CHANGELOG.md index 5a3dbd81ac5..4875ec59176 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,14 @@ All notable changes to this project will be documented in this file. + + The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). @@ -10,6 +18,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 > [!IMPORTANT] > We are working on stabilizing the Log signal that would require making deprecations and breaking changes. We will try to reduce the releases that may require an update to your code, especially for instrumentations or for sdk developers. + + ## Unreleased - `opentelemetry-sdk`: fix multi-processor `force_flush` skipping remaining processors when one returns `None` diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 0eecffd778b..f44e1804551 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -73,6 +73,37 @@ You can run `tox` with the following arguments: - `tox -e tracecontext` to run integration tests for tracecontext. - `tox -e precommit` to run all `pre-commit` actions +### Changelog + +This project uses [towncrier](https://towncrier.readthedocs.io/) to manage the changelog. Instead of editing `CHANGELOG.md` directly, each PR should include a changelog fragment file in the `.changelog/` directory. + +**Creating a changelog fragment:** + +Create a file named `.changelog/.` where `TYPE` is one of: `added`, `changed`, `deprecated`, `removed`, `fixed`. + +The file should contain a one-line description of the change. For example, `.changelog/1234.added`: + +``` +`opentelemetry-sdk`: add support for new feature +``` + +**Writing a good changelog entry:** + +- Write in imperative tone, as if completing the phrase "This change will..." +- Keep entries concise — ideally under 80 characters +- Prefix with the affected package name when applicable (e.g. `` `opentelemetry-sdk`: ... ``) +- Don't include the PR number — towncrier adds it automatically + +**Preview the changelog:** + +```console +towncrier build --draft --version Unreleased +``` + +The CI will verify that a changelog fragment exists and that `CHANGELOG.md` is not directly modified. + +If your change does not need a changelog entry, add the "Skip Changelog" label to the PR. + `ruff check` and `ruff format` are executed when `tox -e ruff` is run. We strongly recommend you to configure [pre-commit](https://pre-commit.com/) locally to run `ruff` and `rstcheck` automatically before each commit by installing it as git hooks. You just need to [install pre-commit](https://pre-commit.com/#install) in your environment: ```console diff --git a/RELEASING.md b/RELEASING.md index c1387beffe8..dc5cb33b331 100644 --- a/RELEASING.md +++ b/RELEASING.md @@ -19,11 +19,7 @@ ### Backporting -Creating manual backports of pull request(s) requires the `backport` label to be added in order to have a green CI. Even if there where -no changes on a repo the patch release preparation workflow requires an empty `## Unreleased` header in `CHANGELOG.md`. - -Backport of pull request(s) can be automated by a workflow only if there where no changes that will create conflicts in the release -branch, unfortunately every `CHANGELOG.md` change will create one. +Creating manual backports of pull request(s) requires the `backport` label to be added in order to have a green CI. To use the workflow to backport pull request(s) to the release branch: * Run the [Backport workflow](https://github.com/open-telemetry/opentelemetry-python/actions/workflows/backport.yml). @@ -33,8 +29,6 @@ To use the workflow to backport pull request(s) to the release branch: * Add the label `backport` to the generated pull request. * In case label automation doesn't work, just close and reopen the PR so that the workflow will take into account the label automation we have in place. * Review and merge the backport pull request that it generates. -* Merge a pull request to the release branch updating the `CHANGELOG.md`. - * The heading for the unreleased entries should be `## Unreleased`. ### Preparing a patch release diff --git a/pyproject.toml b/pyproject.toml index 83b489aa940..66e4a179203 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -165,6 +165,40 @@ target-python-version = "3.10" custom-template-dir = "opentelemetry-sdk/codegen" additional-imports = "typing.ClassVar,opentelemetry.sdk._configuration._common._additional_properties" +[tool.towncrier] +directory = ".changelog" +filename = "CHANGELOG.md" +start_string = "\n" +template = "scripts/changelog_template.j2" +issue_format = "[#{issue}](https://github.com/open-telemetry/opentelemetry-python/pull/{issue})" +wrap = true # wrap fragments to 79 char line length +issue_pattern = "^(\\d+)" # only PR numbers as fragment prefix (e.g. 1234.fixed) + +[[tool.towncrier.type]] +directory = "added" +name = "Added" +showcontent = true + +[[tool.towncrier.type]] +directory = "changed" +name = "Changed" +showcontent = true + +[[tool.towncrier.type]] +directory = "deprecated" +name = "Deprecated" +showcontent = true + +[[tool.towncrier.type]] +directory = "removed" +name = "Removed" +showcontent = true + +[[tool.towncrier.type]] +directory = "fixed" +name = "Fixed" +showcontent = true + [dependency-groups] dev = [ "tox", @@ -172,4 +206,5 @@ dev = [ "pre-commit", "datamodel-code-generator[http]", "datamodel-code-generator[ruff]", + "towncrier", ] diff --git a/scripts/changelog_template.j2 b/scripts/changelog_template.j2 new file mode 100644 index 00000000000..55ac56fe534 --- /dev/null +++ b/scripts/changelog_template.j2 @@ -0,0 +1,24 @@ +## Version {{ versiondata.version }} ({{ versiondata.date }}) + +{% for section, _ in sections.items() %} +{%- if section %}{{ section }}{% endif -%} + +{% if sections[section] %} +{% for category, val in definitions.items() if category in sections[section] %} +### {{ definitions[category]['name'] }} + +{% for text, values in sections[section][category].items() %} +{% if "\n - " in text or '\n * ' in text %} +{%- set main_text, sub_items = text.split('\n', 1) %} +- {{ main_text }} ({{ values|join(', ') }}) +{% if sub_items %} + {{- sub_items }} +{% endif %} +{% else %} +- {{ text }} ({{ values|join(', ') }}) +{% endif %} +{% endfor %} + +{% endfor %} +{% endif %} +{% endfor %} diff --git a/uv.lock b/uv.lock index e3402bd70b1..0a37927e0ac 100644 --- a/uv.lock +++ b/uv.lock @@ -1054,6 +1054,7 @@ dependencies = [ dev = [ { name = "datamodel-code-generator", extra = ["http", "ruff"] }, { name = "pre-commit" }, + { name = "towncrier" }, { name = "tox" }, { name = "tox-uv" }, ] @@ -1082,6 +1083,7 @@ dev = [ { name = "datamodel-code-generator", extras = ["http"] }, { name = "datamodel-code-generator", extras = ["ruff"] }, { name = "pre-commit" }, + { name = "towncrier" }, { name = "tox" }, { name = "tox-uv", specifier = ">=1" }, ] @@ -1750,6 +1752,20 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/c7/18/c86eb8e0202e32dd3df50d43d7ff9854f8e0603945ff398974c1d91ac1ef/tomli_w-1.2.0-py3-none-any.whl", hash = "sha256:188306098d013b691fcadc011abd66727d3c414c571bb01b1a174ba8c983cf90", size = 6675, upload-time = "2025-01-15T12:07:22.074Z" }, ] +[[package]] +name = "towncrier" +version = "25.8.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "click" }, + { name = "jinja2" }, + { name = "tomli", marker = "python_full_version < '3.11'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c2/eb/5bf25a34123698d3bbab39c5bc5375f8f8bcbcc5a136964ade66935b8b9d/towncrier-25.8.0.tar.gz", hash = "sha256:eef16d29f831ad57abb3ae32a0565739866219f1ebfbdd297d32894eb9940eb1", size = 76322, upload-time = "2025-08-30T11:41:55.393Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/42/06/8ba22ec32c74ac1be3baa26116e3c28bc0e76a5387476921d20b6fdade11/towncrier-25.8.0-py3-none-any.whl", hash = "sha256:b953d133d98f9aeae9084b56a3563fd2519dfc6ec33f61c9cd2c61ff243fb513", size = 65101, upload-time = "2025-08-30T11:41:53.644Z" }, +] + [[package]] name = "tox" version = "4.52.1"