diff --git a/.github/dependabot.yaml b/.github/dependabot.yaml index 69d0cfd..fccacae 100644 --- a/.github/dependabot.yaml +++ b/.github/dependabot.yaml @@ -2,6 +2,10 @@ version: 2 updates: - package-ecosystem: "github-actions" directory: "/" + labels: + - dependencies + - github_actions + - skip news schedule: interval: "weekly" day: "tuesday" diff --git a/.github/workflows/check.yaml b/.github/workflows/check.yaml index 7d367a6..3fc7062 100644 --- a/.github/workflows/check.yaml +++ b/.github/workflows/check.yaml @@ -5,6 +5,7 @@ on: branches: ["main"] tags-ignore: ["**"] pull_request: + types: [opened, synchronize, reopened, labeled, unlabeled] schedule: - cron: "0 8 * * *" @@ -46,7 +47,7 @@ jobs: fetch-depth: 0 persist-credentials: false - name: Install the latest version of uv - uses: astral-sh/setup-uv@c18668ad3cf93ea998bef934396af7bb5c839dc7 # v10.2.0 + uses: astral-sh/setup-uv@1c37ad07a6a961277cf70c0d37d6f313000f5884 # v10.3.0 with: python-version: "3.15" enable-cache: false @@ -71,3 +72,34 @@ jobs: PYTEST_ADDOPTS: "-vv --durations=20" DIFF_AGAINST: HEAD UV_PYTHON_PREFERENCE: only-managed + + changelog: + if: github.event_name == 'pull_request' && !contains(github.event.pull_request.labels.*.name, 'skip news') + runs-on: ubuntu-26.04 + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + fetch-depth: 0 + persist-credentials: false + - uses: astral-sh/setup-uv@1c37ad07a6a961277cf70c0d37d6f313000f5884 # v10.3.0 + with: + python-version: "3.15" + enable-cache: false + - run: uvx towncrier check --compare-with "origin/${GITHUB_BASE_REF}" + + gate: + name: Required checks pass + if: always() + needs: [test, changelog] + runs-on: ubuntu-26.04 + steps: + - env: + NEEDS_JSON: ${{ toJSON(needs) }} + run: | + jq -e ' + all( + to_entries[]; + .value.result == "success" or + (.key == "changelog" and .value.result == "skipped") + ) + ' <<< "$NEEDS_JSON" diff --git a/.github/workflows/prepare-release.yaml b/.github/workflows/prepare-release.yaml new file mode 100644 index 0000000..56419bc --- /dev/null +++ b/.github/workflows/prepare-release.yaml @@ -0,0 +1,109 @@ +name: Prepare release +on: + workflow_dispatch: + inputs: + version: + description: Version to release, without a v prefix + required: true + +permissions: {} + +concurrency: + group: prepare-release + cancel-in-progress: false + +jobs: + assemble: + if: github.ref == 'refs/heads/main' + runs-on: ubuntu-26.04 + permissions: + contents: read + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + fetch-depth: 0 + persist-credentials: false + - uses: astral-sh/setup-uv@1c37ad07a6a961277cf70c0d37d6f313000f5884 # v10.3.0 + with: + python-version: "3.15" + enable-cache: false + - name: Build release patch + env: + VERSION: ${{ inputs.version }} + run: | + set -euo pipefail + if [[ ! "$VERSION" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then + echo "::error::expected a major.minor.patch version" + exit 1 + fi + latest=$(git tag --list '[0-9]*' | sort -V | tail -n 1) + if [[ "$(printf '%s\n' "$latest" "$VERSION" | sort -V | tail -n 1)" != "$VERSION" || "$latest" == "$VERSION" ]]; then + echo "::error::$VERSION must be newer than $latest" + exit 1 + fi + if ! compgen -G 'docs/changelog.d/*.rst' > /dev/null; then + echo "::error::no changelog fragments to release" + exit 1 + fi + uvx towncrier build --yes --version "$VERSION" + git diff HEAD --binary > "$RUNNER_TEMP/release.patch" + test -s "$RUNNER_TEMP/release.patch" + - uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2 + with: + name: release-patch + path: ${{ runner.temp }}/release.patch + if-no-files-found: error + push: + needs: assemble + runs-on: ubuntu-26.04 + environment: release-auth + permissions: + contents: read + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + fetch-depth: 0 + persist-credentials: false + - uses: actions/download-artifact@9000827ccba6bdab643e8b6fd33ac0654aef8333 # v8.0.2 + with: + name: release-patch + path: ${{ runner.temp }}/release-patch + - name: Validate release patch + env: + VERSION: ${{ inputs.version }} + run: | + set -euo pipefail + git fetch origin main + git reset --hard origin/main + git apply --check "$RUNNER_TEMP/release-patch/release.patch" + git apply --index "$RUNNER_TEMP/release-patch/release.patch" + while IFS= read -r path; do + case "$path" in + docs/changelog.rst|docs/changelog.d/*.rst) ;; + *) echo "::error::unexpected release path: $path"; exit 1 ;; + esac + done < <(git diff --cached --name-only) + grep -Fq "v$VERSION (" docs/changelog.rst + git diff --cached --check + - id: token + uses: actions/create-github-app-token@64ab729a6294000f5aa4a4bc0b73d1f32bc45510 # v3.3.0 + with: + client-id: ${{ vars.RELEASE_APP_CLIENT_ID }} + private-key: ${{ secrets.RELEASE_APP_PRIVATE_KEY }} + owner: ${{ github.repository_owner }} + repositories: ${{ github.event.repository.name }} + permission-contents: write + - name: Commit and tag the release + env: + VERSION: ${{ inputs.version }} + GH_TOKEN: ${{ steps.token.outputs.token }} + APP_SLUG: ${{ steps.token.outputs.app-slug }} + run: | + set -euo pipefail + app_user_id=$(gh api "/users/${APP_SLUG}[bot]" --jq .id) + git config user.name "${APP_SLUG}[bot]" + git config user.email "${app_user_id}+${APP_SLUG}[bot]@users.noreply.github.com" + git commit -m "chore: prepare $VERSION" + git tag "$VERSION" + gh auth setup-git --hostname github.com --force + git push --atomic origin HEAD:main "refs/tags/$VERSION" diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml index 00757bb..18dd4c9 100644 --- a/.github/workflows/release.yaml +++ b/.github/workflows/release.yaml @@ -17,8 +17,20 @@ jobs: with: fetch-depth: 0 persist-credentials: false + - name: Require a changelog section for the tag + env: + TAG: ${{ github.ref_name }} + run: | + if [[ ! "$TAG" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then + echo "::error::unexpected release tag $TAG" + exit 1 + fi + if ! grep -qF "v$TAG (" docs/changelog.rst; then + echo "::error::docs/changelog.rst has no section for $TAG" + exit 1 + fi - name: Install the latest version of uv - uses: astral-sh/setup-uv@c18668ad3cf93ea998bef934396af7bb5c839dc7 # v10.2.0 + uses: astral-sh/setup-uv@1c37ad07a6a961277cf70c0d37d6f313000f5884 # v10.3.0 with: python-version: "3.15" enable-cache: false @@ -51,3 +63,33 @@ jobs: uses: pypa/gh-action-pypi-publish@dc37677b2e1c63e2034f94d8a5b11f265b73ba33 # v1.14.2 with: attestations: true + github-release: + needs: release + runs-on: ubuntu-26.04 + permissions: + contents: write + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + fetch-depth: 0 + persist-credentials: false + - name: Publish GitHub Release + env: + GH_TOKEN: ${{ github.token }} + TAG: ${{ github.ref_name }} + run: | + set -euo pipefail + awk -v tag="$TAG" ' + index($0, "v" tag " (") == 1 { found = 1; next } + found && /^v[0-9]+\.[0-9]+\.[0-9]+ \(/ { exit } + found && $0 !~ /^-+$/ { print } + ' docs/changelog.rst > "$RUNNER_TEMP/release-notes.md" + test -s "$RUNNER_TEMP/release-notes.md" + printf '\n[PyPI release](https://pypi.org/project/pyproject-api/%s/)\n' "$TAG" >> "$RUNNER_TEMP/release-notes.md" + previous=$(git tag --list '[0-9]*' | grep -E '^[0-9]+\.[0-9]+\.[0-9]+$' | sort -V | awk -v tag="$TAG" '$0 == tag { print previous; exit } { previous = $0 }') + if [[ -n "$previous" ]]; then + printf '\n**Full Changelog**: https://github.com/%s/compare/%s...%s\n' "$GITHUB_REPOSITORY" "$previous" "$TAG" >> "$RUNNER_TEMP/release-notes.md" + else + printf '\n**Full Changelog**: https://github.com/%s/commits/%s\n' "$GITHUB_REPOSITORY" "$TAG" >> "$RUNNER_TEMP/release-notes.md" + fi + gh release create "$TAG" --verify-tag --title "$TAG" --notes-file "$RUNNER_TEMP/release-notes.md" diff --git a/docs/changelog.d/.gitkeep b/docs/changelog.d/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/docs/changelog.rst b/docs/changelog.rst new file mode 100644 index 0000000..2d21afb --- /dev/null +++ b/docs/changelog.rst @@ -0,0 +1,148 @@ +Release History +=============== + +.. towncrier release notes start + +v1.12.0 (2026-10-18) +-------------------- +- Validate in-tree backend paths and reject metadata directories that contain the project root. +- Contain response serialization failures. + +v1.11.7 (2026-10-15) +-------------------- +- Resolve dotted backend object paths. + +v1.11.6 (2026-10-14) +-------------------- +- Resolve output directories before invoking backends. + +v1.11.5 (2026-10-10) +-------------------- +- Require Python 3.11, default to Python 3.15, and import in-tree backends from relative roots. + +v1.11.4 (2026-10-07) +-------------------- +- Stop hanging on undecodable backend output. + +v1.11.3 (2026-10-03) +-------------------- +- Validate ``build-system`` value types when reading a project folder. + +v1.11.2 (2026-09-18) +-------------------- +- Avoid pipe deadlocks when sending large backend requests. + +v1.11.1 (2026-09-15) +-------------------- +- Remove directory symlinks in ``ensure_empty_dir``. + +v1.11.0 (2026-07-21) +-------------------- +- Add Python 3.15 to the test matrix. + +v1.10.1 (2026-05-28) +-------------------- +- Move development tooling to ``tox.toml``, dependency groups, and ty. + +v1.10.0 (2025-10-09) +-------------------- +- Add Python 3.14 support and end support for Python 3.9. + +v1.9.1 (2025-05-12) +------------------- +- Remove ``wheel`` from the default requirements. + +v1.9.0 (2025-01-21) +------------------- +- Update project maintenance metadata. + +v1.8.0 (2024-09-18) +------------------- +- Finish dropping Python 2 support. + +v1.7.2 (2024-09-17) +------------------- +- Improve CI. + +v1.7.1 (2024-06-22) +------------------- +- Move release notes from the documentation site to GitHub Releases. + +v1.7.0 (2024-06-22) +------------------- +- Correct the documentation homepage URL and update build dependencies. + +v1.6.1 - (2023-08-29) +--------------------- +- Fix :meth:`pyproject_api.Frontend.metadata_from_built` only extracts one of the dist info files. + +v1.6.0 - (2023-08-29) +--------------------- +- Remove ``build_`` from ``prepare_metadata_for_build_`` to allow separate config + parametrization and instead add :meth:`pyproject_api.Frontend.metadata_from_built` the user can call when the prepare + fails. Pass ``None`` for ``metadata_directory`` for such temporary wheel builds. + +v1.5.4 - (2023-08-17) +--------------------- +- Make sure that the order of Requires-Dist does not matter + +v1.5.3 - (2023-07-06) +--------------------- +- Fix ``read_line`` to raise ``EOFError`` if nothing was read + +v1.5.2 - (2023-06-14) +--------------------- +- Use ruff for linting. +- Drop 2.7 test run. + +v1.5.1 - (2023-03-12) +--------------------- +- docs: set html_last_updated_fmt to format string + +v1.5.0 - (2023-01-17) +--------------------- +- When getting metadata from a built wheel, do not pass ``metadata_directory`` + to ``build_wheel``, which forces the backend to generate the metadata - by :user:`masenf`. + (`#47 `_) + +v1.4.0 - (2023-01-04) +--------------------- +- Add minimal CLI for debugging + +v1.3.0 - (2023-01-03) +--------------------- +- Do not allow exceptions to propagate in backend + +v1.2.1 - (2022-12-04) +--------------------- +- Fix Python 2 incompatibility on the backend +- Allow skipping prepare metadata for the full build by returning None as basename + +v1.2.0 - (2022-12-04) +--------------------- +- Expose which optional hooks are present or missing via :meth:`pyproject_api.Frontend.optional_hooks` + +v1.1.2 - (2022-10-30) +--------------------- +- Fix editable classes not exported at root level + +v1.1.1 - (2022-09-10) +--------------------- +- Add missed ``wheel`` as test dependency + +v1.1.0 - (2022-09-10) +--------------------- +- PEP-660 support + +v1.0.0 - (2022-09-10) +--------------------- +- Use hatchling as build backend +- 3.11 support + +v0.1.1 - (2021-12-30) +--------------------- +- Drop Python 3.6 support + +v0.1.0 - (2021-10-21) +--------------------- +- first version diff --git a/docs/conf.py b/docs/conf.py index adebbeb..c12d81b 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -29,6 +29,7 @@ "sphinx.ext.intersphinx", ] master_doc, source_suffix = "index", ".rst" +exclude_patterns = ["changelog.d/*"] html_theme = "furo" html_title, html_last_updated_fmt = "pyproject-api docs", "%Y-%m-%dT%H:%M:%S" diff --git a/docs/index.rst b/docs/index.rst index 6c1ffa7..5a8da6d 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -8,3 +8,4 @@ self api + changelog diff --git a/pyproject.toml b/pyproject.toml index 43ec608..9bfb9f7 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -48,7 +48,7 @@ dynamic = [ dependencies = [ "packaging>=26.3", ] -urls.Changelog = "https://github.com/tox-dev/pyproject-api/releases" +urls.Changelog = "https://pyproject-api.readthedocs.io/en/latest/changelog.html" urls.Homepage = "https://pyproject-api.readthedocs.io" urls.Source = "https://github.com/tox-dev/pyproject-api" urls.Tracker = "https://github.com/tox-dev/pyproject-api/issues" @@ -166,3 +166,14 @@ paths.source = [ report.omit = [] html.show_contexts = true html.skip_covered = false + +[tool.towncrier] +name = "pyproject-api" +directory = "docs/changelog.d" +filename = "docs/changelog.rst" +title_format = "v{version} ({project_date})" +type = [ + { directory = "feature", name = "Features", showcontent = true }, + { directory = "bugfix", name = "Bug fixes", showcontent = true }, + { directory = "doc", name = "Documentation", showcontent = true }, +]