From 076ea19f3fb219fca53175a60c15ca8c6337888f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bern=C3=A1t=20G=C3=A1bor?= Date: Mon, 19 Oct 2026 11:18:12 -0700 Subject: [PATCH 01/65] =?UTF-8?q?=F0=9F=90=9B=20fix(release):=20check=20ge?= =?UTF-8?q?nerated=20changelog?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/prepare-release.yaml | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/.github/workflows/prepare-release.yaml b/.github/workflows/prepare-release.yaml index 144d291..c727caf 100644 --- a/.github/workflows/prepare-release.yaml +++ b/.github/workflows/prepare-release.yaml @@ -43,6 +43,23 @@ jobs: exit 1 fi uvx towncrier build --yes --version "$VERSION" + uvx prek run --all-files --show-diff-on-failure || uvx prek run --all-files --show-diff-on-failure + unexpected=$( + while IFS= read -r entry; do + path=${entry:3} + case "$path" in + CHANGELOG.md|changelog.d/*.md) ;; + *) printf '%s\n' "$path" ;; + esac + done < <(git status --short --untracked-files=all) + ) + if [[ -n "$unexpected" ]]; then + echo "::error::release preparation changed paths outside CHANGELOG.md and changelog.d" + echo "$unexpected" + exit 1 + fi + grep -Fq "## $VERSION (" CHANGELOG.md + git diff HEAD --check git diff HEAD --binary > "$RUNNER_TEMP/release.patch" test -s "$RUNNER_TEMP/release.patch" - uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2 @@ -82,6 +99,14 @@ jobs: done < <(git diff --cached --name-only) grep -Fq "## $VERSION (" CHANGELOG.md git diff --cached --check + - name: Check the release tree before authentication + run: | + set -euo pipefail + uvx prek run --all-files --show-diff-on-failure || uvx prek run --all-files --show-diff-on-failure + if ! git diff --quiet || [[ -n $(git ls-files --others --exclude-standard) ]]; then + echo "::error::prek changed the validated release patch; inspect the generated changelog and rerun" + exit 1 + fi - id: token uses: actions/create-github-app-token@64ab729a6294000f5aa4a4bc0b73d1f32bc45510 # v3.3.0 with: From c878e064ecbbbf914e6aae7d83a6ce66e537119a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bern=C3=A1t=20G=C3=A1bor?= Date: Mon, 19 Oct 2026 13:31:28 -0700 Subject: [PATCH 02/65] =?UTF-8?q?=F0=9F=A9=B9=20fix(release):=20validate?= =?UTF-8?q?=20the=20full=20release=20path?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/check.yaml | 132 +++++++++++++- .github/workflows/label-maintenance-pr.yaml | 36 ++++ .github/workflows/prepare-release.yaml | 78 ++++++--- .github/workflows/release.yaml | 81 ++++++--- .github/workflows/upgrade-release-tools.yaml | 137 +++++++++++++++ CHANGELOG.md | 128 ++++++++++++-- CONTRIBUTING.md | 31 ++++ README.md | 3 +- pylock.toml | 174 +++++++++++++++++++ pyproject.toml | 15 +- 10 files changed, 741 insertions(+), 74 deletions(-) create mode 100644 .github/workflows/label-maintenance-pr.yaml create mode 100644 .github/workflows/upgrade-release-tools.yaml create mode 100644 CONTRIBUTING.md create mode 100644 pylock.toml diff --git a/.github/workflows/check.yaml b/.github/workflows/check.yaml index 8a097c8..6ca9cf1 100644 --- a/.github/workflows/check.yaml +++ b/.github/workflows/check.yaml @@ -10,7 +10,7 @@ on: - cron: "0 8 * * *" concurrency: group: check-${{ github.ref }} - cancel-in-progress: true + cancel-in-progress: false permissions: contents: read jobs: @@ -53,7 +53,7 @@ jobs: env: PYTEST_ADDOPTS: "-vv --durations=5" changelog: - if: github.event_name == 'pull_request' && !contains(github.event.pull_request.labels.*.name, 'skip news') + if: github.event_name == 'pull_request' runs-on: ubuntu-24.04 steps: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 @@ -64,20 +64,140 @@ jobs: with: python-version: "3.15" enable-cache: false - - run: uvx towncrier check --compare-with "origin/${GITHUB_BASE_REF}" + - name: ๐Ÿ“ฆ Install locked release tools + run: | + uv venv --python 3.15 "$RUNNER_TEMP/release-tools" + uv pip sync --preview-features pylock --python "$RUNNER_TEMP/release-tools/bin/python" --group release pylock.toml + echo "$RUNNER_TEMP/release-tools/bin" >> "$GITHUB_PATH" + - name: Require or validate a changelog fragment + env: + SKIP_NEWS: ${{ contains(github.event.pull_request.labels.*.name, 'skip news') }} + BOT_AUTHOR: ${{ github.event.pull_request.user.type == 'Bot' }} + run: | + set -euo pipefail + fragments=0 + while IFS= read -r path; do + name=${path##*/} + case "$name" in + +*.*.md|[0-9]*.*.md) ;; + *) continue ;; + esac + category=${name%.md} + category=${category##*.} + case "$category" in + feature|change|bugfix|doc|breaking|security|performance|deprecation|removal|packaging) ;; + *) echo "::error::unknown changelog category in $path"; exit 1 ;; + esac + if [[ -z $(tr -d '[:space:]' < "$path") ]]; then + echo "::error::empty changelog fragment: $path" + exit 1 + fi + fragments=$((fragments + 1)) + done < <(git diff --diff-filter=AM --name-only "origin/${GITHUB_BASE_REF}...HEAD" -- 'changelog.d/*.md') + if ((fragments)); then + towncrier check --compare-with "origin/${GITHUB_BASE_REF}" + elif [[ "$SKIP_NEWS" != true && "$BOT_AUTHOR" != true ]]; then + echo '::error::add a changelog fragment or apply the skip news label' + exit 1 + fi + release-dry-run: + name: Release without publishing + if: github.event_name == 'pull_request' + runs-on: ubuntu-26.04 + permissions: + contents: read + pull-requests: read + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + fetch-depth: 0 + persist-credentials: false + ref: ${{ github.event.pull_request.head.sha }} + - uses: astral-sh/setup-uv@1c37ad07a6a961277cf70c0d37d6f313000f5884 # v10.3.0 + with: + python-version: "3.15" + enable-cache: false + - name: Install locked release tools + run: | + set -euo pipefail + uv venv --python 3.15 "$RUNNER_TEMP/release-tools" + uv pip sync --preview-features pylock --python "$RUNNER_TEMP/release-tools/bin/python" --group release pylock.toml + echo "$RUNNER_TEMP/release-tools/bin" >> "$GITHUB_PATH" + - name: Assemble and validate a local release + env: + BASE_SHA: ${{ github.event.pull_request.base.sha }} + HEAD_SHA: ${{ github.event.pull_request.head.sha }} + PR_NUMBER: ${{ github.event.pull_request.number }} + GH_TOKEN: ${{ github.token }} + run: | + set -euo pipefail + git checkout -B release-validation "$BASE_SHA" + git merge --squash "$HEAD_SHA" + printf 'Verify a user-visible release fix.\n' > "changelog.d/+validation-${PR_NUMBER}.bugfix.md" + git add -A + git -c user.name=release-validation -c user.email=release-validation@users.noreply.github.com commit -m "Validate release pipeline (#${PR_NUMBER})" + version=$(git tag --list | release-creator tags --next auto --directory changelog.d) + git tag --list | release-creator tags "$version" --require-newer + previous=$(git tag --list | release-creator tags "$version" --previous) + release-creator fragments --repository "$GITHUB_REPOSITORY" --git --resolve-authors + towncrier build --yes --version "$version" + mdformat --wrap 120 CHANGELOG.md + prek run --all-files + git diff HEAD --binary > "$RUNNER_TEMP/release.patch" + test -s "$RUNNER_TEMP/release.patch" + git reset --hard HEAD + git apply --check "$RUNNER_TEMP/release.patch" + git apply --index "$RUNNER_TEMP/release.patch" + while IFS= read -r path; do + case "$path" in + CHANGELOG.md|changelog.d/*.md) ;; + *) echo "::error::unexpected release path: $path"; exit 1 ;; + esac + done < <(git diff --cached --name-only) + grep -Fq "## $version (" CHANGELOG.md + git diff --cached --check + prek run --all-files + if ! git diff --quiet || [[ -n $(git ls-files --others --exclude-standard) ]]; then + echo "::error::prek changed the validated release patch" + exit 1 + fi + git -c user.name=release-validation -c user.email=release-validation@users.noreply.github.com commit -m "release $version" + git tag "$version" + uv build --python 3.15 --python-preference only-managed --sdist --wheel . --out-dir "$RUNNER_TEMP/dist" + args=() + if [[ -n "$previous" ]]; then + args+=(--previous "$previous") + fi + release-creator notes --changelog CHANGELOG.md --tag "$version" --repository "$GITHUB_REPOSITORY" --artifact-url "https://pypi.org/project/devpi-process/${version}/" "${args[@]}" > "$RUNNER_TEMP/release-notes.md" + test "$(grep -c '^\*\*Full Changelog\*\*:' "$RUNNER_TEMP/release-notes.md")" -eq 1 + sha256sum "$RUNNER_TEMP"/dist/* > "$RUNNER_TEMP/dist.sha256" + cp CHANGELOG.md "$RUNNER_TEMP/changelog.md" + - uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2 + with: + name: release-dry-run + path: | + ${{ runner.temp }}/release.patch + ${{ runner.temp }}/release-notes.md + ${{ runner.temp }}/changelog.md + ${{ runner.temp }}/dist.sha256 + ${{ runner.temp }}/dist/* + if-no-files-found: error gate: name: Required checks pass if: always() - needs: [test, changelog] + needs: [test, changelog, release-dry-run] runs-on: ubuntu-24.04 steps: - env: NEEDS_JSON: ${{ toJSON(needs) }} + EVENT_NAME: ${{ github.event_name }} run: | - jq -e ' + jq -e --arg event "$EVENT_NAME" ' all( to_entries[]; .value.result == "success" or - (.key == "changelog" and .value.result == "skipped") + (.value.result == "skipped" and + ((.key == "changelog" and $event != "pull_request") or + (.key == "release-dry-run" and $event != "pull_request"))) ) ' <<< "$NEEDS_JSON" diff --git a/.github/workflows/label-maintenance-pr.yaml b/.github/workflows/label-maintenance-pr.yaml new file mode 100644 index 0000000..fde2cbd --- /dev/null +++ b/.github/workflows/label-maintenance-pr.yaml @@ -0,0 +1,36 @@ +name: Label maintenance PRs +on: + workflow_run: # zizmor: ignore[dangerous-triggers] Reads PR metadata without checkout, artifacts, caches, or PR code. + workflows: ["๐Ÿงช check"] + types: [completed] +permissions: + contents: read + issues: write + pull-requests: read +jobs: + label: + if: github.event.workflow_run.event == 'pull_request' && github.event.workflow_run.pull_requests[0] != null + runs-on: ubuntu-26.04 + steps: + - name: Set news exception when no fragment exists + env: + GH_TOKEN: ${{ github.token }} + PR_NUMBER: ${{ github.event.workflow_run.pull_requests[0].number }} + run: | + set -euo pipefail + author=$(gh api "repos/${GITHUB_REPOSITORY}/pulls/${PR_NUMBER}" --jq .user.login) + case "$author" in + 'dependabot[bot]'|'pre-commit-ci[bot]'|'github-actions[bot]') ;; + *) exit 0 ;; + esac + has_fragment=false + while IFS= read -r path; do + case "$path" in + changelog.d/*.md) has_fragment=true; break ;; + esac + done < <(gh api --paginate "repos/${GITHUB_REPOSITORY}/pulls/${PR_NUMBER}/files" --jq '.[].filename') + if [[ "$has_fragment" == false ]]; then + gh api -X POST "repos/${GITHUB_REPOSITORY}/issues/${PR_NUMBER}/labels" -f 'labels[]=skip news' > /dev/null + elif gh api "repos/${GITHUB_REPOSITORY}/issues/${PR_NUMBER}/labels" --jq '.[].name' | grep -Fxq 'skip news'; then + gh api -X DELETE "repos/${GITHUB_REPOSITORY}/issues/${PR_NUMBER}/labels/skip%20news" > /dev/null + fi diff --git a/.github/workflows/prepare-release.yaml b/.github/workflows/prepare-release.yaml index c727caf..f4f8b99 100644 --- a/.github/workflows/prepare-release.yaml +++ b/.github/workflows/prepare-release.yaml @@ -2,9 +2,17 @@ name: Prepare release on: workflow_dispatch: inputs: - version: - description: Version to release, without a v prefix + bump: + description: Version bump type required: true + type: choice + options: + - auto + - major + - minor + - patch + - calver + default: auto permissions: {} concurrency: group: prepare-release @@ -15,6 +23,9 @@ jobs: runs-on: ubuntu-26.04 permissions: contents: read + pull-requests: read + outputs: + version: ${{ steps.version.outputs.version }} steps: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: @@ -24,26 +35,31 @@ jobs: with: python-version: "3.15" enable-cache: false + - name: ๐Ÿ“ฆ Install locked release tools + run: | + uv venv --python 3.15 "$RUNNER_TEMP/release-tools" + uv pip sync --preview-features pylock --python "$RUNNER_TEMP/release-tools/bin/python" --group release pylock.toml + echo "$RUNNER_TEMP/release-tools/bin" >> "$GITHUB_PATH" + - name: Calculate next version + id: version + env: + BUMP: ${{ inputs.bump }} + run: | + set -euo pipefail + version=$(git tag --list | release-creator tags --next "$BUMP") + echo "Next version: $version" + echo "version=$version" >> "$GITHUB_OUTPUT" - name: Build release patch env: - VERSION: ${{ inputs.version }} + GH_TOKEN: ${{ github.token }} + VERSION: ${{ steps.version.outputs.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 'changelog.d/*.md' > /dev/null; then - echo "::error::no changelog fragments to release" - exit 1 - fi - uvx towncrier build --yes --version "$VERSION" - uvx prek run --all-files --show-diff-on-failure || uvx prek run --all-files --show-diff-on-failure + git tag --list | release-creator tags "$VERSION" --require-newer + release-creator fragments --repository "$GITHUB_REPOSITORY" --git --resolve-authors + towncrier build --yes --version "$VERSION" + mdformat --wrap 120 CHANGELOG.md + prek run --all-files unexpected=$( while IFS= read -r entry; do path=${entry:3} @@ -51,7 +67,7 @@ jobs: CHANGELOG.md|changelog.d/*.md) ;; *) printf '%s\n' "$path" ;; esac - done < <(git status --short --untracked-files=all) + done < <(git status --porcelain=v1 --untracked-files=all) ) if [[ -n "$unexpected" ]]; then echo "::error::release preparation changed paths outside CHANGELOG.md and changelog.d" @@ -59,6 +75,7 @@ jobs: exit 1 fi grep -Fq "## $VERSION (" CHANGELOG.md + release-creator notes --changelog CHANGELOG.md --tag "$VERSION" --repository "$GITHUB_REPOSITORY" > "$RUNNER_TEMP/release-notes.md" git diff HEAD --check git diff HEAD --binary > "$RUNNER_TEMP/release.patch" test -s "$RUNNER_TEMP/release.patch" @@ -82,12 +99,20 @@ jobs: with: name: release-patch path: ${{ runner.temp }}/release-patch + - uses: astral-sh/setup-uv@1c37ad07a6a961277cf70c0d37d6f313000f5884 # v10.3.0 + with: + python-version: "3.15" + enable-cache: false - name: Validate release patch env: - VERSION: ${{ inputs.version }} + VERSION: ${{ needs.assemble.outputs.version }} run: | set -euo pipefail git fetch origin main + if [[ $(git rev-parse origin/main) != "$GITHUB_SHA" ]]; then + echo "::error::main changed during release preparation; dispatch again" + exit 1 + fi git reset --hard origin/main git apply --check "$RUNNER_TEMP/release-patch/release.patch" git apply --index "$RUNNER_TEMP/release-patch/release.patch" @@ -99,12 +124,11 @@ jobs: done < <(git diff --cached --name-only) grep -Fq "## $VERSION (" CHANGELOG.md git diff --cached --check - - name: Check the release tree before authentication - run: | - set -euo pipefail - uvx prek run --all-files --show-diff-on-failure || uvx prek run --all-files --show-diff-on-failure + uv venv --python 3.15 "$RUNNER_TEMP/release-tools" + uv pip sync --preview-features pylock --python "$RUNNER_TEMP/release-tools/bin/python" --group release pylock.toml + "$RUNNER_TEMP/release-tools/bin/prek" run --all-files if ! git diff --quiet || [[ -n $(git ls-files --others --exclude-standard) ]]; then - echo "::error::prek changed the validated release patch; inspect the generated changelog and rerun" + echo "::error::prek changed the validated release patch" exit 1 fi - id: token @@ -117,7 +141,7 @@ jobs: permission-contents: write - name: Commit and tag the release env: - VERSION: ${{ inputs.version }} + VERSION: ${{ needs.assemble.outputs.version }} GH_TOKEN: ${{ steps.token.outputs.token }} APP_SLUG: ${{ steps.token.outputs.app-slug }} run: | @@ -125,7 +149,7 @@ jobs: 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 commit -m "release $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 0874cb6..cbeb935 100644 --- a/.github/workflows/release.yaml +++ b/.github/workflows/release.yaml @@ -10,32 +10,47 @@ jobs: build: runs-on: ubuntu-26.04 steps: - - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + - name: ๐Ÿ“ฅ Checkout code + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: fetch-depth: 0 persist-credentials: false + - name: ๐Ÿš€ Install the latest version of uv + uses: astral-sh/setup-uv@1c37ad07a6a961277cf70c0d37d6f313000f5884 # v10.3.0 + with: + python-version: "3.15" + cache-dependency-glob: "pyproject.toml" + enable-cache: false + - name: ๐Ÿ“ฆ Install locked release tools + run: | + uv venv --python 3.15 "$RUNNER_TEMP/release-tools" + uv pip sync --preview-features pylock --python "$RUNNER_TEMP/release-tools/bin/python" --group release pylock.toml + echo "$RUNNER_TEMP/release-tools/bin" >> "$GITHUB_PATH" - 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 + set -euo pipefail + release-creator tags "$TAG" if ! grep -qF "## $TAG (" CHANGELOG.md; then echo "::error::CHANGELOG.md has no section for $TAG" exit 1 fi - - name: Install the latest version of uv - uses: astral-sh/setup-uv@1c37ad07a6a961277cf70c0d37d6f313000f5884 # v10.3.0 - with: - python-version: "3.15" - enable-cache: false - cache-dependency-glob: "pyproject.toml" - github-token: ${{ secrets.GITHUB_TOKEN }} - - name: Build package + previous=$(git tag --list | release-creator tags "$TAG" --previous) + args=() + if [[ -n "$previous" ]]; then + args+=(--previous "$previous") + fi + release-creator notes \ + --changelog CHANGELOG.md \ + --tag "$TAG" \ + --repository "$GITHUB_REPOSITORY" \ + --artifact-url "https://pypi.org/project/devpi-process/${TAG}/" \ + "${args[@]}" > "$RUNNER_TEMP/release-notes.md" + test "$(grep -c '^\*\*Full Changelog\*\*:' "$RUNNER_TEMP/release-notes.md")" -eq 1 + - name: ๐Ÿ“ฆ Build package run: uv build --python 3.15 --python-preference only-managed --sdist --wheel . --out-dir dist - - name: Store the distribution packages + - name: ๐Ÿ“ค Store the distribution packages uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2 with: name: ${{ env.dists-artifact-name }} @@ -50,12 +65,12 @@ jobs: permissions: id-token: write steps: - - name: Download all the dists + - name: ๐Ÿ“ฅ Download all the dists uses: actions/download-artifact@9000827ccba6bdab643e8b6fd33ac0654aef8333 # v8.0.2 with: name: ${{ env.dists-artifact-name }} path: dist/ - - name: Publish to PyPI + - name: ๐Ÿš€ Publish to PyPI uses: pypa/gh-action-pypi-publish@dc37677b2e1c63e2034f94d8a5b11f265b73ba33 # v1.14.2 with: attestations: true @@ -69,19 +84,39 @@ jobs: 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: ๐Ÿ“ฆ Install locked release tools + run: | + uv venv --python 3.15 "$RUNNER_TEMP/release-tools" + uv pip sync --preview-features pylock --python "$RUNNER_TEMP/release-tools/bin/python" --group release pylock.toml + echo "$RUNNER_TEMP/release-tools/bin" >> "$GITHUB_PATH" - name: Publish GitHub Release env: GH_TOKEN: ${{ github.token }} TAG: ${{ github.ref_name }} run: | set -euo pipefail - awk -v tag="$TAG" 'index($0, "## " tag " (") == 1 { found = 1; next } found && /^## / { exit } found { print }' CHANGELOG.md > "$RUNNER_TEMP/release-notes.md" - test -s "$RUNNER_TEMP/release-notes.md" - printf '\n[PyPI release](https://pypi.org/project/devpi-process/%s/)\n' "$TAG" >> "$RUNNER_TEMP/release-notes.md" - previous=$(git tag --list '[0-9]*' | grep -E '^[0-9]+(\.[0-9]+){1,2}$' | sort -V | awk -v tag="$TAG" '$0 == tag { print previous; exit } { previous = $0 }') + previous=$(git tag --list | release-creator tags "$TAG" --previous) + args=() 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" + args+=(--previous "$previous") + gh api -X POST "repos/${GITHUB_REPOSITORY}/releases/generate-notes" \ + -f tag_name="$TAG" \ + -f previous_tag_name="$previous" \ + --jq .body > "$RUNNER_TEMP/generated-notes.md" else - printf '\n**Full Changelog**: https://github.com/%s/commits/%s\n' "$GITHUB_REPOSITORY" "$TAG" >> "$RUNNER_TEMP/release-notes.md" + gh api -X POST "repos/${GITHUB_REPOSITORY}/releases/generate-notes" \ + -f tag_name="$TAG" \ + --jq .body > "$RUNNER_TEMP/generated-notes.md" fi - gh release create "$TAG" --verify-tag --title "$TAG" --notes-file "$RUNNER_TEMP/release-notes.md" + release-creator notes \ + --changelog CHANGELOG.md \ + --tag "$TAG" \ + --repository "$GITHUB_REPOSITORY" \ + --artifact-url "https://pypi.org/project/devpi-process/${TAG}/" \ + --generated-notes "$RUNNER_TEMP/generated-notes.md" \ + "${args[@]}" > "$RUNNER_TEMP/release-notes.md" + gh release create "$TAG" --verify-tag --title "v$TAG" --notes-file "$RUNNER_TEMP/release-notes.md" diff --git a/.github/workflows/upgrade-release-tools.yaml b/.github/workflows/upgrade-release-tools.yaml new file mode 100644 index 0000000..d3886a2 --- /dev/null +++ b/.github/workflows/upgrade-release-tools.yaml @@ -0,0 +1,137 @@ +name: Upgrade release tools +on: + workflow_dispatch: + schedule: + - cron: "0 8 * * 2" +permissions: {} +concurrency: + group: upgrade-release-tools + cancel-in-progress: false +jobs: + generate: + runs-on: ubuntu-26.04 + permissions: + contents: read + outputs: + changed: ${{ steps.lock.outputs.changed }} + 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: Refresh release tool lock + run: | + set -euo pipefail + uv pip compile \ + --preview-features pylock \ + --group pyproject.toml:release \ + --universal \ + --python-version 3.15 \ + --generate-hashes \ + --upgrade \ + --output-file pylock.toml + - name: Run prek with the refreshed tools + run: | + set -euo pipefail + uv venv --python 3.15 "$RUNNER_TEMP/release-tools" + uv pip sync \ + --preview-features pylock \ + --python "$RUNNER_TEMP/release-tools/bin/python" \ + --group release \ + pylock.toml + "$RUNNER_TEMP/release-tools/bin/prek" run --all-files + unexpected=$( + while IFS= read -r entry; do + path=${entry:3} + if [[ "$path" != pylock.toml ]]; then + printf '%s\n' "$path" + fi + done < <(git status --porcelain=v1 --untracked-files=all) + ) + if [[ -n "$unexpected" ]]; then + echo "::error::prek changed files outside pylock.toml" + echo "$unexpected" + exit 1 + fi + - id: lock + run: | + set -euo pipefail + if git diff --quiet -- pylock.toml; then + echo "changed=false" >> "$GITHUB_OUTPUT" + exit 0 + fi + echo "changed=true" >> "$GITHUB_OUTPUT" + git diff --binary -- pylock.toml > "$RUNNER_TEMP/lock.patch" + test -s "$RUNNER_TEMP/lock.patch" + - uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2 + if: steps.lock.outputs.changed == 'true' + with: + name: release-tools-lock + path: ${{ runner.temp }}/lock.patch + if-no-files-found: error + retention-days: 1 + publish: + needs: generate + if: needs.generate.outputs.changed == 'true' && github.ref == 'refs/heads/main' + runs-on: ubuntu-26.04 + permissions: + actions: write + contents: write + pull-requests: write + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + ref: ${{ github.sha }} + fetch-depth: 0 + persist-credentials: false + - uses: actions/download-artifact@9000827ccba6bdab643e8b6fd33ac0654aef8333 # v8.0.2 + with: + name: release-tools-lock + path: ${{ runner.temp }}/release-tools-lock + - name: Validate and apply lock patch + run: | + set -euo pipefail + git apply --check "$RUNNER_TEMP/release-tools-lock/lock.patch" + git apply --index "$RUNNER_TEMP/release-tools-lock/lock.patch" + paths=$(git diff --cached --name-only) + if [[ "$paths" != pylock.toml ]]; then + echo "::error::unexpected lock paths: $paths" + exit 1 + fi + git diff --cached --check + - name: Open or update dependency pull request + env: + GH_TOKEN: ${{ github.token }} + BRANCH: automation/update-release-tools + run: | + set -euo pipefail + git config user.name "github-actions[bot]" + git config user.email "41898282+github-actions[bot]@users.noreply.github.com" + git commit -m "๐Ÿ”ง build(deps): refresh release lock" + gh auth setup-git --hostname github.com --force + git push --force origin "HEAD:$BRANCH" + cat > "$RUNNER_TEMP/pull-request.md" <<'EOF' + ๐Ÿ”ง Refresh the locked release tools to the latest versions allowed by the `release` dependency group. + `pylock.toml` records the exact versions used by release and changelog workflows. + EOF + pr_number=$(gh pr list --base main --head "$BRANCH" --state open --json number --jq '.[0].number') + if [[ -n "$pr_number" ]]; then + gh pr edit "$pr_number" \ + --title "๐Ÿ”ง build(deps): refresh release tool lock" \ + --body-file "$RUNNER_TEMP/pull-request.md" \ + --add-label dependencies \ + --add-label "skip news" + else + gh pr create \ + --base main \ + --head "$BRANCH" \ + --title "๐Ÿ”ง build(deps): refresh release tool lock" \ + --body-file "$RUNNER_TEMP/pull-request.md" \ + --label dependencies \ + --label "skip news" + fi + gh workflow run check.yaml --ref "$BRANCH" diff --git a/CHANGELOG.md b/CHANGELOG.md index 863cc5a..6fca724 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,54 +2,156 @@ + + ## 1.3.1 (2026-10-19) + + ### Documentation -- Publish a GitHub Release with the tagged changelog after each PyPI upload. (#290) +- Publish a GitHub Release with the tagged changelog after each PyPI upload + ([#290](https://github.com/tox-dev/devpi-process/pull/290)) by [@gaborbernat](https://github.com/gaborbernat). + + ## 1.3.0 (2026-10-10) -- Require Python 3.11 and use Python 3.15 by default ([#284](https://github.com/tox-dev/devpi-process/pull/284)). + + +### Added + +- Require Python 3.11 ([#284](https://github.com/tox-dev/devpi-process/pull/284)) by + [@gaborbernat](https://github.com/gaborbernat). + + + +### Changes + +- Use Python 3.15 by default ([#284](https://github.com/tox-dev/devpi-process/pull/284)) by + [@gaborbernat](https://github.com/gaborbernat). + + ## 1.2.0 (2026-07-21) -- Test on Python 3.15 and tighten GitHub Actions security checks. + + +### Added + +- Test on Python 3.15 ([#262](https://github.com/tox-dev/devpi-process/pull/262)) by + [@gaborbernat](https://github.com/gaborbernat). + + + +### Other changes + +- Tighten GitHub Actions security checks ([#232](https://github.com/tox-dev/devpi-process/pull/232)) by + [@gaborbernat](https://github.com/gaborbernat). + + ## 1.1.1 (2026-02-23) -- Move the development tooling to `tox.toml`, dependency groups, and ty. + + +### Other changes + +- Move the development tooling to `tox.toml`, dependency groups, and ty by + [@gaborbernat](https://github.com/gaborbernat). + + ## 1.1.0 (2025-10-09) + + +### Added + - Add Python 3.14 support and end support for Python 3.8 and 3.9 - ([#196](https://github.com/tox-dev/devpi-process/pull/196)). + ([#196](https://github.com/tox-dev/devpi-process/pull/196)) by [@gaborbernat](https://github.com/gaborbernat). + + ## 1.0.2 (2024-09-18) -- Improve CI ([#134](https://github.com/tox-dev/devpi-process/pull/134)). + + +### Other changes + +- Improve CI ([#134](https://github.com/tox-dev/devpi-process/pull/134)) by + [@gaborbernat](https://github.com/gaborbernat). + + ## 1.0.1 (2024-09-08) -- Declare Python 3.13 support ([#130](https://github.com/tox-dev/devpi-process/pull/130)). + + +### Added + +- Declare Python 3.13 support ([#130](https://github.com/tox-dev/devpi-process/pull/130)) by + [@gaborbernat](https://github.com/gaborbernat). + + ## 1.0.0 (2023-08-22) -- Exclude automated dependency and hook updates from generated release notes. + + +### Other changes + +- Exclude automated dependency and hook updates from generated release notes by + [@gaborbernat](https://github.com/gaborbernat). + + ## 0.3.1 (2023-06-15) -- Update dependencies and development tooling. + + +### Other changes + +- Update dependencies and development tooling by [@gaborbernat](https://github.com/gaborbernat). + + ## 0.3.0 (2022-12-03) -- Show stdout/stderr details if a command fails + + +### Changes + +- Show stdout/stderr details if a command fails by [@gaborbernat](https://github.com/gaborbernat). + + ## 0.2 (2021-12-29) -- Drop 3.6 support and bump linters -- Index URL now ends with `/` to avoid redirect when sending a GET + + +### Added + +- Drop Python 3.6 support by [@gaborbernat](https://github.com/gaborbernat). + + + +### Fixed + +- Index URL now ends with `/` to avoid redirect when sending a GET by [@gaborbernat](https://github.com/gaborbernat). + + + +### Other changes + +- Bump linters by [@gaborbernat](https://github.com/gaborbernat). + + ## 0.1 (2021-06-17) -- First release + + +### Added + +- First release by [@gaborbernat](https://github.com/gaborbernat). diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..3035643 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,31 @@ +# Contributing + +Run the tests and type checker with `tox -e 3.15,type`. Run `prek run --all-files` before pushing a pull request. + +## Release notes + +Add a `changelog.d/+short-name..md` fragment when a pull request changes what package users can use or observe. +Use one fragment per user-facing change. The release workflow links each fragment to its merged squash pull request. + +| Type | Changelog section | Use for | +| ------------- | ----------------- | ---------------------------------------------------------- | +| `feature` | Added | New capabilities and Python support additions or removals | +| `change` | Changes | Changed behavior that remains available | +| `bugfix` | Fixed | Corrected behavior | +| `doc` | Documentation | Documentation changes users need to know about | +| `breaking` | Breaking changes | Changes that require users to update code or configuration | +| `security` | Security | Security fixes users need to act on | +| `performance` | Performance | User-visible speed or resource improvements | +| `deprecation` | Deprecated | Supported behavior scheduled for removal | +| `removal` | Removed | Removed user-facing capabilities other than Python support | +| `packaging` | Packaging | Install, wheel, or distribution changes | + +Use `skip news` for a pull request with no user-facing change. The label can be applied after the pull request opens. A +fragment still has to be valid when the label is present. Dependabot, pre-commit.ci, and repository automation receive +the label when their pull request has no fragment. + +The release workflow credits the pull request author. If several people authored the change, add their linked GitHub +handles in one `by` phrase in the fragment. Omit bots and reviewers. + +The historical changelog uses `Other changes` where the original release did not identify a user-facing category. New +pull requests use a specific category or `skip news`. diff --git a/README.md b/README.md index 9e1324e..9ed93c2 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,6 @@ # devpi-process -See the [changelog](CHANGELOG.md) for released changes. Pull requests need a `changelog.d/..md` fragment; use -the `skip news` label for changes that do not need release notes. +See the [changelog](CHANGELOG.md) for released changes and [contributing guide](CONTRIBUTING.md) for fragment rules. [![PyPI](https://img.shields.io/pypi/v/devpi-process?style=flat-square)](https://pypi.org/project/devpi-process) [![PyPI - Implementation](https://img.shields.io/pypi/implementation/devpi-process?style=flat-square)](https://pypi.org/project/devpi-process) diff --git a/pylock.toml b/pylock.toml new file mode 100644 index 0000000..7b3df0b --- /dev/null +++ b/pylock.toml @@ -0,0 +1,174 @@ +# This file was autogenerated by uv via the following command: +# uv pip compile --preview-features pylock --group pyproject.toml:release --universal --python-version 3.15 --generate-hashes --output-file pylock.toml +lock-version = "1.0" +created-by = "uv" +requires-python = ">=3.15" + +[[packages]] +name = "click" +version = "8.5.0" +sdist = { url = "https://files.pythonhosted.org/packages/c7/0e/7fa0ef50764b67090eca4114772a2abf8b6148198475e54c660b97caeee6/click-8.5.0.tar.gz", upload-time = 2026-08-26T13:33:14Z, size = 382235, hashes = { sha256 = "ba0d2089de75ea0310e2dde03160e6ca10009947fb95a182f9b54021bb272e34" } } +wheels = [{ url = "https://files.pythonhosted.org/packages/58/50/6c0d534c5f134586a8e1ba4e330569e32f057e33372ae556463212fb4cd3/click-8.5.0-py3-none-any.whl", upload-time = 2026-08-26T13:33:12Z, size = 125251, hashes = { sha256 = "255bc9599cf7748b4b1a446ccc735421bd08a2ae529a8b88597d3de5664ee360" } }] + +[[packages]] +name = "jinja2" +version = "3.1.6" +sdist = { url = "https://files.pythonhosted.org/packages/df/bf/f7da0350254c0ed7c72f3e33cef02e048281fec7ecec5f032d4aac52226b/jinja2-3.1.6.tar.gz", upload-time = 2025-03-05T20:05:02Z, size = 245115, hashes = { sha256 = "0137fb05990d35f1275a587e9aee6d56da821fc83491a0fb838183be43f66d6d" } } +wheels = [{ url = "https://files.pythonhosted.org/packages/62/a1/3d680cbfd5f4b8f15abc1d571870c5fc3e594bb582bc3b64ea099db13e56/jinja2-3.1.6-py3-none-any.whl", upload-time = 2025-03-05T20:05:00Z, size = 134899, hashes = { sha256 = "85ece4451f492d0c13c5dd7c13a64681a86afae63a5f347908daf103ce6d2f67" } }] + +[[packages]] +name = "markdown-it-py" +version = "4.2.0" +sdist = { url = "https://files.pythonhosted.org/packages/06/ff/7841249c247aa650a76b9ee4bbaeae59370dc8bfd2f6c01f3630c35eb134/markdown_it_py-4.2.0.tar.gz", upload-time = 2026-05-07T12:08:28Z, size = 82454, hashes = { sha256 = "04a21681d6fbb623de53f6f364d352309d4094dd4194040a10fd51833e418d49" } } +wheels = [{ url = "https://files.pythonhosted.org/packages/b3/81/4da04ced5a082363ecfa159c010d200ecbd959ae410c10c0264a38cac0f5/markdown_it_py-4.2.0-py3-none-any.whl", upload-time = 2026-05-07T12:08:27Z, size = 91687, hashes = { sha256 = "9f7ebbcd14fe59494226453aed97c1070d83f8d24b6fc3a3bcf9a38092641c4a" } }] + +[[packages]] +name = "markupsafe" +version = "3.0.4" +sdist = { url = "https://files.pythonhosted.org/packages/38/9b/e422a865e1d5d57d0e509b4e0bf1c1a70a7f6382c29a5aa428df994c8bc8/markupsafe-3.0.4.tar.gz", upload-time = 2026-10-02T23:07:22Z, size = 153777, hashes = { sha256 = "2e9ad7dd851bf45fab9f75cbff4cb493fee9979e8d8c7c9c3ee119022518edd6" } } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a9/30/54d11c8ca027114898cab97421fb39e4ffd9ddf47cdbc44df2ec76722da9/markupsafe-3.0.4-cp315-cp315-android_24_arm64_v8a.whl", upload-time = 2026-10-02T23:06:10Z, size = 12791, hashes = { sha256 = "737c9c3981998eba27f11786f84fddcbabc74068b72a4a1f454ea02094b57b65" } }, + { url = "https://files.pythonhosted.org/packages/10/6d/97c913e253a14bd3cd0e15a5c56d13203b823fa7ee32498342896a072dc4/markupsafe-3.0.4-cp315-cp315-android_24_x86_64.whl", upload-time = 2026-10-02T23:06:11Z, size = 12527, hashes = { sha256 = "489505b03f692c3f376394e49194fa7a7f9e8558d6e293a7056a0032b0c38163" } }, + { url = "https://files.pythonhosted.org/packages/26/f9/b86d032042a4d597d9e1997f0e5f63a3eedaf11258e0a05760b0a0a826ea/markupsafe-3.0.4-cp315-cp315-ios_13_0_arm64_iphoneos.whl", upload-time = 2026-10-02T23:06:13Z, size = 11602, hashes = { sha256 = "077293e425f28ec737dbcad442a71752e28f8ae27cde3d68acd1fb212091cd92" } }, + { url = "https://files.pythonhosted.org/packages/f2/dc/73c14c1eedf0ac5fa3292ba43435e6c49d2c2050f33cebde541f8f4807f1/markupsafe-3.0.4-cp315-cp315-ios_13_0_arm64_iphonesimulator.whl", upload-time = 2026-10-02T23:06:14Z, size = 11817, hashes = { sha256 = "9348cbb300d224fe3b89793262cb093504d4ae927004468463f745188a193e4a" } }, + { url = "https://files.pythonhosted.org/packages/8f/69/2c2fcaa5fcee22d72c7819c0d536fd181c74a688e6143845419579cd2863/markupsafe-3.0.4-cp315-cp315-ios_13_0_x86_64_iphonesimulator.whl", upload-time = 2026-10-02T23:06:15Z, size = 11446, hashes = { sha256 = "b807e598953730f82e4eae3bd30f6a122cf6b31c398c6b504c0e04c13c170429" } }, + { url = "https://files.pythonhosted.org/packages/88/54/9e5ec76c62e6e2834d5a93623018c943e8b3bb41d663e3fd4c03303b9b85/markupsafe-3.0.4-cp315-cp315-macosx_10_15_x86_64.whl", upload-time = 2026-10-02T23:06:16Z, size = 11719, hashes = { sha256 = "799c39bdf5e2f1292fedd3009f7b3c9e760f10b2420cb9638d56920840ff6db8" } }, + { url = "https://files.pythonhosted.org/packages/96/24/3ec292b44064c16229e064d770b2625bd8ea941aa61f44905a9fa44942c0/markupsafe-3.0.4-cp315-cp315-macosx_11_0_arm64.whl", upload-time = 2026-10-02T23:06:17Z, size = 12075, hashes = { sha256 = "ae9dcb8fbe244cb82f8a6458b455b927a03685e383d9bacf1ea5ce180b96dc97" } }, + { url = "https://files.pythonhosted.org/packages/aa/85/b64fdb1f304848518742136983c24e96d967bfb59a0ea160e92736901ab0/markupsafe-3.0.4-cp315-cp315-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", upload-time = 2026-10-02T23:06:18Z, size = 24656, hashes = { sha256 = "4bced6e2a6dba6a28f7dd3c6ce14df1b2dd495923f16ea484cad03decd463b2b" } }, + { url = "https://files.pythonhosted.org/packages/9c/18/23997d4c65b355da6390d61cd56e0ab3befd6ba8dda25cb40c602bd0fa6b/markupsafe-3.0.4-cp315-cp315-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", upload-time = 2026-10-02T23:06:20Z, size = 27737, hashes = { sha256 = "3882fb412298575bae3b9c46868251f15cc69307359f87bb1b382e53d6e5a2c9" } }, + { url = "https://files.pythonhosted.org/packages/d4/36/35998dead3c6af88c38265a56e58100211f036234ab88eb2283fd4cbce44/markupsafe-3.0.4-cp315-cp315-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", upload-time = 2026-10-02T23:06:21Z, size = 25003, hashes = { sha256 = "04e7902ba80ee4bac1d50a549606527a1dcf0476cd81403db41099d3b60ec653" } }, + { url = "https://files.pythonhosted.org/packages/82/96/ef49135ce260db4ca4a12b119ed468449cd248db6b1468e2112b546d7a2e/markupsafe-3.0.4-cp315-cp315-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", upload-time = 2026-10-02T23:06:22Z, size = 23111, hashes = { sha256 = "925f929d6b59a8b3f8b8c6ac363cd0af7eecc81efb3071770b3c6717c450a369" } }, + { url = "https://files.pythonhosted.org/packages/50/7d/83126e338bd88c17a220668235368ad719fd4638e426739858cbb8508f77/markupsafe-3.0.4-cp315-cp315-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", upload-time = 2026-10-02T23:06:23Z, size = 22403, hashes = { sha256 = "f68edfc67aabac33708941f26f22a7b8e9f81429bc0cf249fcf7d66b23af8d19" } }, + { url = "https://files.pythonhosted.org/packages/83/dd/daf7e420de23c8206c365204e7b85e1251d8e19d34196a56336f316e5ed2/markupsafe-3.0.4-cp315-cp315-musllinux_1_2_aarch64.whl", upload-time = 2026-10-02T23:06:25Z, size = 24083, hashes = { sha256 = "e5c802729725bd07e2bc3ab7b76dc7e0bbfc53129d8f1eb1c002c24cf774717e" } }, + { url = "https://files.pythonhosted.org/packages/19/3c/11eecdc06bc44ad5570350085b572ebf049e8f9a38d1ece6d76640b739cd/markupsafe-3.0.4-cp315-cp315-musllinux_1_2_armv7l.whl", upload-time = 2026-10-02T23:06:26Z, size = 22931, hashes = { sha256 = "55ffd6ce583d97dc71dc92e930324c8c0d25aea7e3ade6ae54ef77cedb096811" } }, + { url = "https://files.pythonhosted.org/packages/0d/9e/ac0fd77f2a726e56ecc3ca0235d095feace1358d1b822406c2a2ef26a4dc/markupsafe-3.0.4-cp315-cp315-musllinux_1_2_ppc64le.whl", upload-time = 2026-10-02T23:06:27Z, size = 24299, hashes = { sha256 = "2cb3dd71fc6be918ad4264346a8ed69485f9b7ed7bf35495d8e22807cd6b8bea" } }, + { url = "https://files.pythonhosted.org/packages/d7/09/c6bd842ad58ff5b3bc76eeed7e9a42a6f11adc5d090ec697b72c9672731e/markupsafe-3.0.4-cp315-cp315-musllinux_1_2_riscv64.whl", upload-time = 2026-10-02T23:06:29Z, size = 21948, hashes = { sha256 = "94f5407f7bc64fa6463906b896f9904beeeb7dd8dc116ee8e9056c8714ff9916" } }, + { url = "https://files.pythonhosted.org/packages/a3/46/82f586711fed61e86faa1ee1bc317d68cd45a10c8bdbe3f7d1fdf9026ad8/markupsafe-3.0.4-cp315-cp315-musllinux_1_2_x86_64.whl", upload-time = 2026-10-02T23:06:30Z, size = 23150, hashes = { sha256 = "2dad610540cb2e6272855c178f08ae9a1c7ac258a7fb71660553a5f104b42741" } }, + { url = "https://files.pythonhosted.org/packages/19/2d/2dfdce99318abbfa26925195fbc17db188c46a1ec6457be121b6f9cfeb42/markupsafe-3.0.4-cp315-cp315-win32.whl", upload-time = 2026-10-02T23:06:31Z, size = 14269, hashes = { sha256 = "03470d1a8268e692ecf79ecd565593e59d44219377a7ead61f1f1b94c1f7ff6b" } }, + { url = "https://files.pythonhosted.org/packages/5b/ec/6000fd82e8791e58fcd0456ec20f098957e2b03d5ed02eb73241a577c0ba/markupsafe-3.0.4-cp315-cp315-win_amd64.whl", upload-time = 2026-10-02T23:06:33Z, size = 14496, hashes = { sha256 = "d882a373d8093c2941e01291b7ced96e9cbe4781da9a7751ca7e6c70385e5214" } }, + { url = "https://files.pythonhosted.org/packages/bc/66/e73bd5016421d5d6e2fb6de7dd609f9de020942ac8c626526bd8c6eeaf82/markupsafe-3.0.4-cp315-cp315-win_arm64.whl", upload-time = 2026-10-02T23:06:34Z, size = 14361, hashes = { sha256 = "353bd63081912ab8cfa6a0c7d185934cdf8426f04c618bba6bc4b394f2069b67" } }, + { url = "https://files.pythonhosted.org/packages/90/df/cb8c3dc98d313a951df2f8968f44e4cb5643df6d3cab749a530ce2f7d972/markupsafe-3.0.4-cp315-cp315t-macosx_10_15_x86_64.whl", upload-time = 2026-10-02T23:06:35Z, size = 11760, hashes = { sha256 = "c61750fadcd119d0825bcb7d7d675dd264dcc89cc05292aab5be68ebdbb374ad" } }, + { url = "https://files.pythonhosted.org/packages/d6/bb/4af9b3ca0753d654ac75f9531d5bd741bb77ca6e696f36807c475ffc099a/markupsafe-3.0.4-cp315-cp315t-macosx_11_0_arm64.whl", upload-time = 2026-10-02T23:06:37Z, size = 12108, hashes = { sha256 = "1c0df495a977d10460a94941799c72d5b5ab03d3858d949b55b5a66c8f371c99" } }, + { url = "https://files.pythonhosted.org/packages/3f/d4/b56429313aee5fd59b079c3df5615299959e25e7113eb6d8caadbdd7d38a/markupsafe-3.0.4-cp315-cp315t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", upload-time = 2026-10-02T23:06:38Z, size = 25762, hashes = { sha256 = "02fa4acbc6a3fc5c693c34d4dd8c1130b7fe99cc915181b0ddd6f72aeb296002" } }, + { url = "https://files.pythonhosted.org/packages/65/f5/34c181e891aa4f7d59c918584672e0c5eb7fffe76c1387d1246008bf4081/markupsafe-3.0.4-cp315-cp315t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", upload-time = 2026-10-02T23:06:39Z, size = 28400, hashes = { sha256 = "05295589e619b9bed252a86b532b8e27350abc372d18ba89b59375325e91ec1e" } }, + { url = "https://files.pythonhosted.org/packages/ce/b5/ad14694fd0ac9a5ce30bc6498f2999378f418583dd1679cca5a1b512957e/markupsafe-3.0.4-cp315-cp315t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", upload-time = 2026-10-02T23:06:41Z, size = 25946, hashes = { sha256 = "be6cb0c799abb0e2ba3e618e6d28ddddf7e485f6c2ce938dfa237daf3905072c" } }, + { url = "https://files.pythonhosted.org/packages/d6/a8/26b606445387d0ceb1eb1f21840094b84e4e3c3c3983d80d10b89823b490/markupsafe-3.0.4-cp315-cp315t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", upload-time = 2026-10-02T23:06:42Z, size = 23634, hashes = { sha256 = "26e9867520db70d37f7fb421a7f0d8adb40171011fb84ce869afa1a83370dfa8" } }, + { url = "https://files.pythonhosted.org/packages/39/a2/b8814de672f1f0094d498bf646f2fec9d6356b503d28ef500b71c5095377/markupsafe-3.0.4-cp315-cp315t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", upload-time = 2026-10-02T23:06:44Z, size = 23488, hashes = { sha256 = "f03460ff076f70ab595bb45a0205ccea1971443575b6920c52e755dec2b3fbfe" } }, + { url = "https://files.pythonhosted.org/packages/db/c7/287223376fb73335a3cc5d6eb22c6ab01358cf33945a9c39c06b9dac3f4b/markupsafe-3.0.4-cp315-cp315t-musllinux_1_2_aarch64.whl", upload-time = 2026-10-02T23:06:45Z, size = 25037, hashes = { sha256 = "436e3ffc6310d3c41878c601db29098102fe5d8a467c49da4a4125254e0980f2" } }, + { url = "https://files.pythonhosted.org/packages/f9/29/4df8355e313426d19e62ba33e0253c009ca12a0894ee77d67fa67255361c/markupsafe-3.0.4-cp315-cp315t-musllinux_1_2_armv7l.whl", upload-time = 2026-10-02T23:06:47Z, size = 23543, hashes = { sha256 = "4e2c4809c14559aa7ef426f27fb35afbb38104c349a903bf8f3600456764bb38" } }, + { url = "https://files.pythonhosted.org/packages/71/e5/8377731e8495668dcc768f645e717df18318c841edaf023a99395f6da9b4/markupsafe-3.0.4-cp315-cp315t-musllinux_1_2_ppc64le.whl", upload-time = 2026-10-02T23:06:48Z, size = 25282, hashes = { sha256 = "da2af0d7aebfc2074080d72efa6ab8317c62481ef1f896f65d9999c1c01f4494" } }, + { url = "https://files.pythonhosted.org/packages/ed/5f/373456e37ceb1478d657d6fe769cbe0a39f0a8dfc1548eeb19c471eefdd9/markupsafe-3.0.4-cp315-cp315t-musllinux_1_2_riscv64.whl", upload-time = 2026-10-02T23:06:50Z, size = 22993, hashes = { sha256 = "aa2c838cc024642cc04c6854232f32b43e5e22833dd11119c1766c7873b8370d" } }, + { url = "https://files.pythonhosted.org/packages/d7/93/2cbd5628435afb6f541bbaced4bce0c2edac4b09a142e6e928b8b0da9858/markupsafe-3.0.4-cp315-cp315t-musllinux_1_2_x86_64.whl", upload-time = 2026-10-02T23:06:51Z, size = 23712, hashes = { sha256 = "b91cc9d336957239ff200f30097e6fea2dc6d6fb3c81e853eaa09eac904fd894" } }, + { url = "https://files.pythonhosted.org/packages/81/99/157e10966b033b363aeda5263e82596ee232a0b1d082fdbf90aa417ff083/markupsafe-3.0.4-cp315-cp315t-win32.whl", upload-time = 2026-10-02T23:06:53Z, size = 14300, hashes = { sha256 = "e49fb0d1ce92cfa0cb198cc5b1b11cdf9d0638658e2a2db2687e39db7c87fc78" } }, + { url = "https://files.pythonhosted.org/packages/33/05/55884815414c9706a23deca150b72c25a62109e65b0b6ce232077802c719/markupsafe-3.0.4-cp315-cp315t-win_amd64.whl", upload-time = 2026-10-02T23:06:54Z, size = 14522, hashes = { sha256 = "4f6e0852a0283b1b1fd776eeb7b766a5f440b3e2bd31ab51af3b400585f3965c" } }, + { url = "https://files.pythonhosted.org/packages/92/f9/ecbde7149e95b8a0f18e16d5d747f7dc06049d5da2e4f77f6f5e4a1f46a8/markupsafe-3.0.4-cp315-cp315t-win_arm64.whl", upload-time = 2026-10-02T23:06:56Z, size = 14417, hashes = { sha256 = "39dbacefc411633db5b4378b066a9aca70a3d7e2922c9e578d825f844026eeba" } }, +] + +[[packages]] +name = "mdformat" +version = "1.0.0" +sdist = { url = "https://files.pythonhosted.org/packages/3f/05/32b5e14b192b0a8a309f32232c580aefedd9d06017cb8fe8fce34bec654c/mdformat-1.0.0.tar.gz", upload-time = 2025-10-16T12:05:03Z, size = 56953, hashes = { sha256 = "4954045fcae797c29f86d4ad879e43bb151fa55dbaf74ac6eaeacf1d45bb3928" } } +wheels = [{ url = "https://files.pythonhosted.org/packages/54/9a/8fe71b95985ca7a4001effbcc58e5a07a1f2a2884203f74dcf48a3b08315/mdformat-1.0.0-py3-none-any.whl", upload-time = 2025-10-16T12:05:02Z, size = 53288, hashes = { sha256 = "bca015d65a1d063a02e885a91daee303057bc7829c2cd37b2075a50dbb65944b" } }] + +[[packages]] +name = "mdformat-gfm" +version = "1.0.0" +sdist = { url = "https://files.pythonhosted.org/packages/56/6f/a626ebb142a290474401b67e2d61e73ce096bf7798ee22dfe6270f924b3f/mdformat_gfm-1.0.0.tar.gz", upload-time = 2025-10-16T09:12:22Z, size = 10112, hashes = { sha256 = "d1d49a409a6acb774ce7635c72d69178df7dce1dc8cdd10e19f78e8e57b72623" } } +wheels = [{ url = "https://files.pythonhosted.org/packages/e6/18/6bc2189b744dd383cad03764f41f30352b1278d2205096f77a29c0b327ad/mdformat_gfm-1.0.0-py3-none-any.whl", upload-time = 2025-10-16T09:12:21Z, size = 10970, hashes = { sha256 = "7305a50efd2a140d7c83505b58e3ac5df2b09e293f9bbe72f6c7bee8c678b005" } }] + +[[packages]] +name = "mdformat-ruff" +version = "0.1.3" +sdist = { url = "https://files.pythonhosted.org/packages/3a/e2/9003dc57c78aea34b21f67854d842985ceac48028d288432efa113df3887/mdformat_ruff-0.1.3.tar.gz", upload-time = 2024-02-11T01:03:58Z, size = 3416, hashes = { sha256 = "2ddd2758dd3f6738c1c7bfbeb89320d271454f4cd8c28a99968a50d3578b15d6" } } +wheels = [{ url = "https://files.pythonhosted.org/packages/b1/22/3f2464d478f145bbcb731cee9fe720b9e4d69efb6ffd8bc4491f14ca20a2/mdformat_ruff-0.1.3-py3-none-any.whl", upload-time = 2024-02-11T01:03:57Z, size = 3932, hashes = { sha256 = "9c0cf7bfe6bf99b9e0e617e960041d06e9fa84300d5645f922306dd2ed0d1296" } }] + +[[packages]] +name = "mdit-py-plugins" +version = "0.6.1" +sdist = { url = "https://files.pythonhosted.org/packages/59/fc/f8d0863f8862f25602c0404d75568e89fb6b4109804645e5cdfb1be5cf56/mdit_py_plugins-0.6.1.tar.gz", upload-time = 2026-05-13T09:03:38Z, size = 56114, hashes = { sha256 = "a2bca0f039f39dbd35fb74ae1b5f998608c437463371f0ff7f49a19a17a114d0" } } +wheels = [{ url = "https://files.pythonhosted.org/packages/a5/69/6da5581c6a7fede7dc261bf4e67d6adca4196f176b43288b55b3db395b6e/mdit_py_plugins-0.6.1-py3-none-any.whl", upload-time = 2026-05-13T09:03:37Z, size = 66663, hashes = { sha256 = "214c82fb2ac524472ab6a5bcab1de80f73b50443e187f401bfd77efbc7c6481d" } }] + +[[packages]] +name = "mdurl" +version = "0.1.2" +sdist = { url = "https://files.pythonhosted.org/packages/d6/54/cfe61301667036ec958cb99bd3efefba235e65cdeb9c84d24a8293ba1d90/mdurl-0.1.2.tar.gz", upload-time = 2022-08-14T12:40:10Z, size = 8729, hashes = { sha256 = "bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba" } } +wheels = [{ url = "https://files.pythonhosted.org/packages/b3/38/89ba8ad64ae25be8de66a6d463314cf1eb366222074cfda9ee839c56a4b4/mdurl-0.1.2-py3-none-any.whl", upload-time = 2022-08-14T12:40:09Z, size = 9979, hashes = { sha256 = "84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8" } }] + +[[packages]] +name = "prek" +version = "0.5.6" +sdist = { url = "https://files.pythonhosted.org/packages/4f/96/c4062bcaebdabfa945d07c424fb8c05d53a72d4f3fdcf255b1188abbb150/prek-0.5.6.tar.gz", upload-time = 2026-10-19T05:46:52Z, size = 579962, hashes = { sha256 = "87a8c19f873b96be809b4368959ed908cfa06255dba15082d0fb91173e9293bc" } } +wheels = [ + { url = "https://files.pythonhosted.org/packages/4d/1b/763b3c673e954cb59f7eb9a6ad1f75871b9d6fb82d5216db440127da0aff/prek-0.5.6-py3-none-macosx_10_12_x86_64.whl", upload-time = 2026-10-19T05:46:34Z, size = 6066432, hashes = { sha256 = "fd60c48d25dca3269f0f17ba8bdf7a89a7beb9cb02e00b7ca8127a0e41caa15a" } }, + { url = "https://files.pythonhosted.org/packages/69/c7/fd59deefb3c4018d94d2c9b736891d1cccc8651e9f264c51769931cdd4c1/prek-0.5.6-py3-none-macosx_11_0_arm64.whl", upload-time = 2026-10-19T05:46:36Z, size = 5698795, hashes = { sha256 = "4325b7ff03720394825b99b0696d1cc6f83999f429110c3f9381b2645a4c398f" } }, + { url = "https://files.pythonhosted.org/packages/62/6a/233b321c72fdbf96124b8235c250eef29b25a7a9bfacbad7740fec9f92d4/prek-0.5.6-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.musllinux_1_1_aarch64.whl", upload-time = 2026-10-19T05:46:37Z, size = 5823392, hashes = { sha256 = "a10d206fe05cc26c551bb986de255115d52e11306a36cdbcdcd0ab725317a5b2" } }, + { url = "https://files.pythonhosted.org/packages/67/87/dc12aaf396877824f54ac4483a2604213fb44061c38a4ec11892766bcb4f/prek-0.5.6-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", upload-time = 2026-10-19T05:46:40Z, size = 6283256, hashes = { sha256 = "b2e6b232654b16e37a554951f84af35470a4bd857af89c21e8fc87e2a475351d" } }, + { url = "https://files.pythonhosted.org/packages/e1/f7/04e08d2343ce1d54c5da879e2dfea7df20f3140b1b2023a937fac87fa545/prek-0.5.6-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", upload-time = 2026-10-19T05:46:42Z, size = 6165929, hashes = { sha256 = "aabec01beeb372bcdc48f6b339021bc33c07edf477b337fc92dc5d3b01f99150" } }, + { url = "https://files.pythonhosted.org/packages/61/72/727103d1ce2be13eba7948640db9ceae5c58303ab4cd2bd299da41d5ef1e/prek-0.5.6-py3-none-manylinux_2_28_aarch64.whl", upload-time = 2026-10-19T05:46:44Z, size = 5837437, hashes = { sha256 = "86407b8be824faa81f299fa41cf466bc811882ddf3a05ef5c578b172abaf1183" } }, + { url = "https://files.pythonhosted.org/packages/64/84/4e54e226023063f426d84cc0da612eaae360283a82e3d76195c75ff81bba/prek-0.5.6-py3-none-musllinux_1_1_x86_64.whl", upload-time = 2026-10-19T05:46:46Z, size = 6305234, hashes = { sha256 = "84215a6b8b3e8825eba924d91422df7c35e0413c134796a6821dcb917858ec8e" } }, + { url = "https://files.pythonhosted.org/packages/c2/3c/064a1df7c0841d0cec8351eca492fc5b9a526314cb1ddb99276a21fdcf63/prek-0.5.6-py3-none-win_amd64.whl", upload-time = 2026-10-19T05:46:48Z, size = 5709609, hashes = { sha256 = "e32821fb947700d322ca9d401009962bc9652e017fa232ceb27ce89168044e5b" } }, + { url = "https://files.pythonhosted.org/packages/e4/67/3dad56a4a998d17cbb23c4577c101dc15298a0f3e572d3c87ca148449d80/prek-0.5.6-py3-none-win_arm64.whl", upload-time = 2026-10-19T05:46:50Z, size = 5643378, hashes = { sha256 = "0ac40373841d087ec5efebd5b84c5c0eb5671285fdc7c2b4473096b30d0bf1e5" } }, +] + +[[packages]] +name = "release-creator" +version = "0.4.1" +sdist = { url = "https://files.pythonhosted.org/packages/98/3e/22a3249fecc6ee0e166ef81227a4ba8cb5c83e0bddd5e57b081050ca2bcd/release_creator-0.4.1.tar.gz", upload-time = 2026-10-19T20:26:30Z, size = 63943, hashes = { sha256 = "80f064352f1c0546f12e27ef643fed35e91059b095def95cb5f5baa44357a0d2" } } +wheels = [{ url = "https://files.pythonhosted.org/packages/7b/a2/acae8a1ef434bf427f9e0726f66ddf6ee4913704e4f31891a280a712a58d/release_creator-0.4.1-py3-none-any.whl", upload-time = 2026-10-19T20:26:29Z, size = 20115, hashes = { sha256 = "0328c9044719b5e011312628a2b2cb818288f862e6ac0933510778e412b5e569" } }] + +[[packages]] +name = "ruff" +version = "0.17.1" +sdist = { url = "https://files.pythonhosted.org/packages/f9/e7/e6d5749983b020e6cb1055abec0a6f361a8e94f0184cdcf56d4760995f02/ruff-0.17.1.tar.gz", upload-time = 2026-10-15T15:23:18Z, size = 5323349, hashes = { sha256 = "5bb796c5112e9fb9527f2ef2faa0a130d6312d19de92bcfaddd70dbb2d41020f" } } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2e/78/90c46ca6250709f7c37e397b60d06061adee8d0f7d13959ed6a8e49b14fd/ruff-0.17.1-py3-none-linux_armv6l.whl", upload-time = 2026-10-15T15:22:33Z, size = 10147528, hashes = { sha256 = "1796c41eafec66f4635125dd1e074a7aa27b8d20c187d484360acf11d247fc4a" } }, + { url = "https://files.pythonhosted.org/packages/aa/d2/a2e28bc35b083ae51fa07dfba5280a667c8b01ecaee7802fc8849dcd5c46/ruff-0.17.1-py3-none-macosx_10_12_x86_64.whl", upload-time = 2026-10-15T15:22:36Z, size = 10225974, hashes = { sha256 = "8eadb09a788010be29c895418bdfa513e4e1c65e64b94e5b19e4cd381f6cc7fb" } }, + { url = "https://files.pythonhosted.org/packages/8e/71/7a9029a81c8206ee01a98f6cfeeb56e6c095af86857581a37045c46d7e94/ruff-0.17.1-py3-none-macosx_11_0_arm64.whl", upload-time = 2026-10-15T15:22:38Z, size = 9270361, hashes = { sha256 = "29b454f18ca3eeaf37fbf140841dcf27d36109e29dee9aae8acecb68c26c6f74" } }, + { url = "https://files.pythonhosted.org/packages/c0/8c/e075e9c917cb0575297880c96768304e881240171c870cbd8d9b7b54bec2/ruff-0.17.1-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", upload-time = 2026-10-15T15:22:41Z, size = 9938300, hashes = { sha256 = "872e03dd231744d6c5ec5c8af6be52b71323fc6ee096e4c8b34d9f844ee3b978" } }, + { url = "https://files.pythonhosted.org/packages/87/27/0b4bb926a49de2383f161272b4d863571a4c7ecdbf595a6bc58813a13367/ruff-0.17.1-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", upload-time = 2026-10-15T15:22:43Z, size = 9948464, hashes = { sha256 = "4c0d6cb5f8fbac77eec9af49f4fc0eaf856b05545e372b0694d24b4e76b1c640" } }, + { url = "https://files.pythonhosted.org/packages/14/27/518d57e0e02aecb8ea4acac6b57b51a53173d25fa734952bfdd7d531e88e/ruff-0.17.1-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", upload-time = 2026-10-15T15:22:46Z, size = 10638639, hashes = { sha256 = "78beced4a3c94dfedbee4b999b95b6a27b35820fdc0afbacc6d37a8da26c1cd1" } }, + { url = "https://files.pythonhosted.org/packages/01/82/d89a4e498ade59e6cee52d67f8da569019372212b87468c1fdeae884601c/ruff-0.17.1-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", upload-time = 2026-10-15T15:22:48Z, size = 11337238, hashes = { sha256 = "7feefc63921202894f0e158b0e32f45a5950d70a7fb693ccb9264816c25b1c05" } }, + { url = "https://files.pythonhosted.org/packages/a2/7c/3b0fdef9352656317c977086fe1d802902406e4f2a0d8578da90c0e6d1c5/ruff-0.17.1-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", upload-time = 2026-10-15T15:22:51Z, size = 10896327, hashes = { sha256 = "684154861a819bf1f433669d2a0ce9c3e54677c65b8ebb866cc2a7ec86345785" } }, + { url = "https://files.pythonhosted.org/packages/62/86/5566290219708ce2928f3a29c4f843a1185b341ec6111c4631a33c5ceced/ruff-0.17.1-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", upload-time = 2026-10-15T15:22:54Z, size = 10350811, hashes = { sha256 = "a6ef98e51de8ef38082f90653ceeb84944bd7673783eea13e1647c5b9b064189" } }, + { url = "https://files.pythonhosted.org/packages/ad/d2/ca8462dabb3c413990d49703ba306c30c862195a75448d080fa8fcce9775/ruff-0.17.1-py3-none-manylinux_2_31_riscv64.whl", upload-time = 2026-10-15T15:22:57Z, size = 10569994, hashes = { sha256 = "8f9aa5b5bd5fa1937803ea609c8f153b8de39bf3e0e149897ae34b0c5b52f467" } }, + { url = "https://files.pythonhosted.org/packages/e3/ad/dd1864c1f1db17c8cfb297ce70b352388e6977620f66f7182c52855407df/ruff-0.17.1-py3-none-musllinux_1_2_aarch64.whl", upload-time = 2026-10-15T15:23:00Z, size = 10059672, hashes = { sha256 = "80e53b0e5faa3c5667f035daaa032893ae42b7cfe708c886d39ed131e54688ab" } }, + { url = "https://files.pythonhosted.org/packages/65/45/3f56cf1d2168c2165cdd5ac6f9668aee66a4cfdd4caf2b2cc5ccb2b1d568/ruff-0.17.1-py3-none-musllinux_1_2_armv7l.whl", upload-time = 2026-10-15T15:23:03Z, size = 9947803, hashes = { sha256 = "056c5d5fa0ac790051ae165d7035c7bf956d02de2db557f2e9b31d877a505fca" } }, + { url = "https://files.pythonhosted.org/packages/aa/4b/5457cb2be4c1d1ea5e919558131a0f197a4a454a0530d9e30820e8206e4c/ruff-0.17.1-py3-none-musllinux_1_2_i686.whl", upload-time = 2026-10-15T15:23:05Z, size = 10351807, hashes = { sha256 = "bd8f5a55b10f371642eacd0e8665a42a8b97da15c9a7187c79e697b77b5e0ce4" } }, + { url = "https://files.pythonhosted.org/packages/49/fb/db15b4f8fa24a1e36b99e1401f42ef6df16ee299a48670d5d262b771e7d7/ruff-0.17.1-py3-none-musllinux_1_2_x86_64.whl", upload-time = 2026-10-15T15:23:08Z, size = 10765716, hashes = { sha256 = "ce05e6f3973611d9550a600e04bb531061e368baba31610eb57c1509a8f21a0a" } }, + { url = "https://files.pythonhosted.org/packages/0a/e8/1e186578076aff7a80735f625005fde678412ab609cd323ed182eb7d6b87/ruff-0.17.1-py3-none-win32.whl", upload-time = 2026-10-15T15:23:10Z, size = 9895093, hashes = { sha256 = "1de0030a8aa78c65c5fecb84b2e5c390ac684c0452875a02afc7c083d49a496a" } }, + { url = "https://files.pythonhosted.org/packages/6a/2e/da3d9589bae8e8b58f3a48bd5bc0b980232fb14a83bf02c3be7660f87c1b/ruff-0.17.1-py3-none-win_amd64.whl", upload-time = 2026-10-15T15:23:13Z, size = 10097955, hashes = { sha256 = "5df76f16580fab58d0f6c98a3ac254a4abceb7b2489b8b678335869cff77b3d7" } }, + { url = "https://files.pythonhosted.org/packages/df/c9/0f2a4dea06a5e12c1191ee3d96b1e1233797d0d5e18f97a2b909a95a9836/ruff-0.17.1-py3-none-win_arm64.whl", upload-time = 2026-10-15T15:23:16Z, size = 9853542, hashes = { sha256 = "42400b9a6ff8515ff27796f07f92e0195be011c86e81e381606f58ea5502fef4" } }, +] + +[[packages]] +name = "towncrier" +version = "26.9.0" +sdist = { url = "https://files.pythonhosted.org/packages/92/d4/e85ce614bf45b1a7fe13b82cc687819c2eaac5a1dd94b84834322095fd98/towncrier-26.9.0.tar.gz", upload-time = 2026-09-04T12:57:25Z, size = 79338, hashes = { sha256 = "ace9031631c718cc0709107b8755e6e31350eb8acdc877af43df3177addb9a55" } } +wheels = [{ url = "https://files.pythonhosted.org/packages/82/88/f9340b545dafa64de053798e760515a2736fbb60092cf9aa6b146ccf3ede/towncrier-26.9.0-py3-none-any.whl", upload-time = 2026-09-04T12:57:23Z, size = 67030, hashes = { sha256 = "ae4d223e6aadaff98d29b7f09e58d9c9ccf4cd3b455a2d2e0486d432cd8bf660" } }] + +[[packages]] +name = "wcwidth" +version = "0.9.2" +sdist = { url = "https://files.pythonhosted.org/packages/f0/b4/7830542634bb2d3e62aa3b586a72d5b3b6c91c3168929e7000ef3fed041d/wcwidth-0.9.2.tar.gz", upload-time = 2026-10-05T00:24:05Z, size = 955039, hashes = { sha256 = "ae0ef90b90f6af38b54f1fe6d58662ec33b3cb4b8391958a62416d654231727b" } } +wheels = [ + { url = "https://files.pythonhosted.org/packages/59/1e/4532a81fb9dfbf4114a816775e0a36c3a64ee1d1f4bba2094e2da50be5dc/wcwidth-0.9.2-cp310-abi3-macosx_10_9_x86_64.whl", upload-time = 2026-10-05T00:23:22Z, size = 605343, hashes = { sha256 = "7ef5a940bd5e30bac6e721f1a48fce0cd7bb3ece19e9c5d139e72c76c35cfd07" } }, + { url = "https://files.pythonhosted.org/packages/a0/07/cb6940e81134b7ed25fa312ee9ab536a63db0793b149f88a90e603ceace9/wcwidth-0.9.2-cp310-abi3-macosx_11_0_arm64.whl", upload-time = 2026-10-05T00:23:27Z, size = 609960, hashes = { sha256 = "ae0800c5339423cc53d33a266ad264b42ba8aaa16d4464f6e6b1bee607f50b17" } }, + { url = "https://files.pythonhosted.org/packages/a4/80/15ad05d40bfa99155639fb9e13b3d77083aa0fab893c816db2543d29005c/wcwidth-0.9.2-cp310-abi3-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", upload-time = 2026-10-05T00:23:38Z, size = 762566, hashes = { sha256 = "9e542f1f8475b78452a295495d7a5bc3ead565112e9446a64dc93462a41c2a79" } }, + { url = "https://files.pythonhosted.org/packages/bc/f0/b8ef7758003d66b60f093695831a86dcc726aac01ee6446ffcbda27b61e3/wcwidth-0.9.2-cp310-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", upload-time = 2026-10-05T00:23:32Z, size = 771632, hashes = { sha256 = "674b518af28d38ee645ff97b74f5760abee5fad4bac74413bfc4b881ef2ce724" } }, + { url = "https://files.pythonhosted.org/packages/db/6c/f940133c71427c208575910e981942bd78c98b1f7cd0d1425ca4b7457c04/wcwidth-0.9.2-cp310-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", upload-time = 2026-10-05T00:23:40Z, size = 769763, hashes = { sha256 = "751bef0ab404b6a1dc028b56b4b85d46486be1c55833f80da533e42dc691f389" } }, + { url = "https://files.pythonhosted.org/packages/92/8f/285f862826f721964ec7c42f81dc53d23afbd723a0f4cd989651f8218e25/wcwidth-0.9.2-cp310-abi3-musllinux_1_2_aarch64.whl", upload-time = 2026-10-05T00:23:33Z, size = 782134, hashes = { sha256 = "c3d80f39ba4653a595edae9aa46a509d14883790a8fc23c5db221ceb207f64b7" } }, + { url = "https://files.pythonhosted.org/packages/c2/2d/64aa54882a5d556d3654c1f926d9118b797461033e23a158409941a37c8f/wcwidth-0.9.2-cp310-abi3-musllinux_1_2_i686.whl", upload-time = 2026-10-05T00:23:41Z, size = 782851, hashes = { sha256 = "0a47e03d8293590ecce66c45dc20ff7b4b885e3c78093722239585eca0d77ab2" } }, + { url = "https://files.pythonhosted.org/packages/59/39/52389f6de7fe2e9c14ceb8253dd99034bd86e1c87847ea3c100a97dded9a/wcwidth-0.9.2-cp310-abi3-musllinux_1_2_x86_64.whl", upload-time = 2026-10-05T00:23:43Z, size = 784621, hashes = { sha256 = "67d901a4ad99249eb775b4ee4769ca97fa405d35a75f46e83166910a47003f04" } }, + { url = "https://files.pythonhosted.org/packages/b3/8b/20225500a076ace27bbcc8a6fd7c55125133c57a618816c7b7b8b73070b1/wcwidth-0.9.2-cp310-abi3-win32.whl", upload-time = 2026-10-05T00:23:55Z, size = 593123, hashes = { sha256 = "ee1fd0db9d9fd711a70f3e7765e0e04c05d26982fa05361456163062549d7da4" } }, + { url = "https://files.pythonhosted.org/packages/5a/d6/b0690f55ea0483530a18bac917fbadbf54f35122510446fc370f5f1c2453/wcwidth-0.9.2-cp310-abi3-win_amd64.whl", upload-time = 2026-10-05T00:23:57Z, size = 597827, hashes = { sha256 = "2a9746de704242bd4fdaabb31dd46b82f694a56a8d21081ad89b679a89da9fec" } }, + { url = "https://files.pythonhosted.org/packages/e5/11/6ecf4e9e268ab1a4ec617ffcccc2ee4a71301625f5490912dbaba462fa9c/wcwidth-0.9.2-cp310-abi3-win_arm64.whl", upload-time = 2026-10-05T00:23:51Z, size = 598415, hashes = { sha256 = "b9c6ab615e03723b7f8760ea2f27758d656e7e13b51515c9dca5c3e8b04612fa" } }, + { url = "https://files.pythonhosted.org/packages/82/8a/a28d61d910005ac93dfe48be3a0ebaa49352d88cebd25323e69e6ff2f4a8/wcwidth-0.9.2-cp315-cp315t-macosx_10_15_x86_64.whl", upload-time = 2026-10-05T00:23:25Z, size = 606575, hashes = { sha256 = "42dbcb76ce8af39e2c9db410ac3f9bdf4e47eb41d6f44525952f172d3d98f724" } }, + { url = "https://files.pythonhosted.org/packages/01/c2/a3c66bd32766c8f4d6dc47d572532ba014fe5be30489f2576aff7cada363/wcwidth-0.9.2-cp315-cp315t-macosx_11_0_arm64.whl", upload-time = 2026-10-05T00:23:30Z, size = 611045, hashes = { sha256 = "138e1f8898e431b2f2d7881f8ca8d75591c1d3c21aa53f54e989bd6b39811da2" } }, + { url = "https://files.pythonhosted.org/packages/ec/8a/d39964f8f8c019d7d439b9b501d3e7bb42fee69f00354040ba0b27b5824c/wcwidth-0.9.2-cp315-cp315t-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", upload-time = 2026-10-05T00:23:47Z, size = 778143, hashes = { sha256 = "5175609bf8cc7398a5f48aa35207bd64ebf9f45e4c70df65f7fdc7a988041a3c" } }, + { url = "https://files.pythonhosted.org/packages/2f/53/525da13e8f9ff7b5b4e74ec6f8d68bdee63905796972e086c6b1b96670d2/wcwidth-0.9.2-cp315-cp315t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", upload-time = 2026-10-05T00:23:36Z, size = 786630, hashes = { sha256 = "e5f669ae8c3d969c72032f9cdee019674b666e522d45e1e2099a2e9dda4a341d" } }, + { url = "https://files.pythonhosted.org/packages/ef/9f/d6a0c6df354b9d93466548a65cbf4ffcb48c719bbd307504cf3e76740837/wcwidth-0.9.2-cp315-cp315t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", upload-time = 2026-10-05T00:23:49Z, size = 784839, hashes = { sha256 = "196b47cf32f9df27ccda6dc513237f3c2429c4c659db428d60a5bc443d10f270" } }, + { url = "https://files.pythonhosted.org/packages/bf/d7/3021feed1ed7926021ec134943ad3b24a2f7ea742cc9976461171482ed77/wcwidth-0.9.2-cp315-cp315t-win32.whl", upload-time = 2026-10-05T00:24:02Z, size = 600845, hashes = { sha256 = "0cd4f7f2e53905dcb110d213a4c8529b6733fa3d232d8c717f946cc69a10349b" } }, + { url = "https://files.pythonhosted.org/packages/63/80/6a03356d8ee38261e3a78cf89ee03d8e7f12c572d969237be00869e2dc73/wcwidth-0.9.2-cp315-cp315t-win_amd64.whl", upload-time = 2026-10-05T00:24:04Z, size = 605441, hashes = { sha256 = "33df042f96c61ed3cd5fb3742fba427553a635bc578799857a48aa79f774a0b9" } }, + { url = "https://files.pythonhosted.org/packages/0c/48/1a308a86a833fd12ff7a08d0d2491ff4a72c8a92d12f5ead8317630f771e/wcwidth-0.9.2-cp315-cp315t-win_arm64.whl", upload-time = 2026-10-05T00:23:54Z, size = 605885, hashes = { sha256 = "48719a9bc76c2f84238693fe5013571fa5beffa3621cf228f1f3a9e30dae84b8" } }, + { url = "https://files.pythonhosted.org/packages/9c/b4/0bfa065af506540d9d558e3e5548cff00bc1f9b24e6e2a8512498e8628de/wcwidth-0.9.2-py3-none-any.whl", upload-time = 2026-10-05T00:23:21Z, size = 301667, hashes = { sha256 = "89ca642c5bf0101157a09366be69fad0379db1f700ae39a920e103234573670e" } }, +] diff --git a/pyproject.toml b/pyproject.toml index 63c0a45..ed667cc 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -78,6 +78,7 @@ pkg-meta = [ "twine>=7", "uv>=0.12.22", ] +release = [ "mdformat-gfm>=1", "mdformat-ruff>=0.1.3", "prek>=0.5", "release-creator>=0.4.1", "towncrier>=26.9" ] [tool.hatch] version.source = "vcs" @@ -153,9 +154,17 @@ html.skip_covered = false name = "devpi-process" directory = "changelog.d" filename = "CHANGELOG.md" -title_format = "## {version} ({project_date})" +template = "release_creator.fragments:template.jinja2" +title_format = false type = [ - { directory = "feature", name = "Features", showcontent = true }, - { directory = "bugfix", name = "Bug fixes", showcontent = true }, + { directory = "feature", name = "Added", showcontent = true }, + { directory = "change", name = "Changes", showcontent = true }, + { directory = "bugfix", name = "Fixed", showcontent = true }, { directory = "doc", name = "Documentation", showcontent = true }, + { directory = "breaking", name = "Breaking changes", showcontent = true }, + { directory = "security", name = "Security", showcontent = true }, + { directory = "performance", name = "Performance", showcontent = true }, + { directory = "deprecation", name = "Deprecated", showcontent = true }, + { directory = "removal", name = "Removed", showcontent = true }, + { directory = "packaging", name = "Packaging", showcontent = true }, ] From cec7b6e5d16772de4d4e0fbe2134a9dddbe6e8f7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bern=C3=A1t=20G=C3=A1bor?= Date: Mon, 19 Oct 2026 13:33:38 -0700 Subject: [PATCH 03/65] =?UTF-8?q?=F0=9F=90=9B=20fix(release):=20match=20ma?= =?UTF-8?q?intenance=20workflow=20name?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/check.yaml | 2 +- .github/workflows/label-maintenance-pr.yaml | 2 +- .github/workflows/prepare-release.yaml | 2 +- .github/workflows/release.yaml | 18 +++++++++--------- .github/workflows/upgrade-release-tools.yaml | 8 ++++---- 5 files changed, 16 insertions(+), 16 deletions(-) diff --git a/.github/workflows/check.yaml b/.github/workflows/check.yaml index 6ca9cf1..7991ddb 100644 --- a/.github/workflows/check.yaml +++ b/.github/workflows/check.yaml @@ -64,7 +64,7 @@ jobs: with: python-version: "3.15" enable-cache: false - - name: ๐Ÿ“ฆ Install locked release tools + - name: Install locked release tools run: | uv venv --python 3.15 "$RUNNER_TEMP/release-tools" uv pip sync --preview-features pylock --python "$RUNNER_TEMP/release-tools/bin/python" --group release pylock.toml diff --git a/.github/workflows/label-maintenance-pr.yaml b/.github/workflows/label-maintenance-pr.yaml index fde2cbd..7d2bea6 100644 --- a/.github/workflows/label-maintenance-pr.yaml +++ b/.github/workflows/label-maintenance-pr.yaml @@ -1,7 +1,7 @@ name: Label maintenance PRs on: workflow_run: # zizmor: ignore[dangerous-triggers] Reads PR metadata without checkout, artifacts, caches, or PR code. - workflows: ["๐Ÿงช check"] + workflows: ["check"] types: [completed] permissions: contents: read diff --git a/.github/workflows/prepare-release.yaml b/.github/workflows/prepare-release.yaml index f4f8b99..04bbb75 100644 --- a/.github/workflows/prepare-release.yaml +++ b/.github/workflows/prepare-release.yaml @@ -35,7 +35,7 @@ jobs: with: python-version: "3.15" enable-cache: false - - name: ๐Ÿ“ฆ Install locked release tools + - name: Install locked release tools run: | uv venv --python 3.15 "$RUNNER_TEMP/release-tools" uv pip sync --preview-features pylock --python "$RUNNER_TEMP/release-tools/bin/python" --group release pylock.toml diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml index cbeb935..046c4e3 100644 --- a/.github/workflows/release.yaml +++ b/.github/workflows/release.yaml @@ -10,18 +10,18 @@ jobs: build: runs-on: ubuntu-26.04 steps: - - name: ๐Ÿ“ฅ Checkout code + - name: Checkout code uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: fetch-depth: 0 persist-credentials: false - - name: ๐Ÿš€ Install the latest version of uv + - name: Install uv uses: astral-sh/setup-uv@1c37ad07a6a961277cf70c0d37d6f313000f5884 # v10.3.0 with: python-version: "3.15" cache-dependency-glob: "pyproject.toml" enable-cache: false - - name: ๐Ÿ“ฆ Install locked release tools + - name: Install locked release tools run: | uv venv --python 3.15 "$RUNNER_TEMP/release-tools" uv pip sync --preview-features pylock --python "$RUNNER_TEMP/release-tools/bin/python" --group release pylock.toml @@ -48,9 +48,9 @@ jobs: --artifact-url "https://pypi.org/project/devpi-process/${TAG}/" \ "${args[@]}" > "$RUNNER_TEMP/release-notes.md" test "$(grep -c '^\*\*Full Changelog\*\*:' "$RUNNER_TEMP/release-notes.md")" -eq 1 - - name: ๐Ÿ“ฆ Build package + - name: Build package run: uv build --python 3.15 --python-preference only-managed --sdist --wheel . --out-dir dist - - name: ๐Ÿ“ค Store the distribution packages + - name: Store the distribution packages uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2 with: name: ${{ env.dists-artifact-name }} @@ -65,12 +65,12 @@ jobs: permissions: id-token: write steps: - - name: ๐Ÿ“ฅ Download all the dists + - name: Download all the distributions uses: actions/download-artifact@9000827ccba6bdab643e8b6fd33ac0654aef8333 # v8.0.2 with: name: ${{ env.dists-artifact-name }} path: dist/ - - name: ๐Ÿš€ Publish to PyPI + - name: Publish to PyPI uses: pypa/gh-action-pypi-publish@dc37677b2e1c63e2034f94d8a5b11f265b73ba33 # v1.14.2 with: attestations: true @@ -88,7 +88,7 @@ jobs: with: python-version: "3.15" enable-cache: false - - name: ๐Ÿ“ฆ Install locked release tools + - name: Install locked release tools run: | uv venv --python 3.15 "$RUNNER_TEMP/release-tools" uv pip sync --preview-features pylock --python "$RUNNER_TEMP/release-tools/bin/python" --group release pylock.toml @@ -119,4 +119,4 @@ jobs: --artifact-url "https://pypi.org/project/devpi-process/${TAG}/" \ --generated-notes "$RUNNER_TEMP/generated-notes.md" \ "${args[@]}" > "$RUNNER_TEMP/release-notes.md" - gh release create "$TAG" --verify-tag --title "v$TAG" --notes-file "$RUNNER_TEMP/release-notes.md" + gh release create "$TAG" --verify-tag --title "$TAG" --notes-file "$RUNNER_TEMP/release-notes.md" diff --git a/.github/workflows/upgrade-release-tools.yaml b/.github/workflows/upgrade-release-tools.yaml index d3886a2..8dd59da 100644 --- a/.github/workflows/upgrade-release-tools.yaml +++ b/.github/workflows/upgrade-release-tools.yaml @@ -111,17 +111,17 @@ jobs: set -euo pipefail git config user.name "github-actions[bot]" git config user.email "41898282+github-actions[bot]@users.noreply.github.com" - git commit -m "๐Ÿ”ง build(deps): refresh release lock" + git commit -m "build(deps): refresh release lock" gh auth setup-git --hostname github.com --force git push --force origin "HEAD:$BRANCH" cat > "$RUNNER_TEMP/pull-request.md" <<'EOF' - ๐Ÿ”ง Refresh the locked release tools to the latest versions allowed by the `release` dependency group. + Refresh the locked release tools to the latest versions allowed by the `release` dependency group. `pylock.toml` records the exact versions used by release and changelog workflows. EOF pr_number=$(gh pr list --base main --head "$BRANCH" --state open --json number --jq '.[0].number') if [[ -n "$pr_number" ]]; then gh pr edit "$pr_number" \ - --title "๐Ÿ”ง build(deps): refresh release tool lock" \ + --title "build(deps): refresh release tool lock" \ --body-file "$RUNNER_TEMP/pull-request.md" \ --add-label dependencies \ --add-label "skip news" @@ -129,7 +129,7 @@ jobs: gh pr create \ --base main \ --head "$BRANCH" \ - --title "๐Ÿ”ง build(deps): refresh release tool lock" \ + --title "build(deps): refresh release tool lock" \ --body-file "$RUNNER_TEMP/pull-request.md" \ --label dependencies \ --label "skip news" From c42c4e55612ddfa76bf1190602d66a9e1571734a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bern=C3=A1t=20G=C3=A1bor?= Date: Mon, 19 Oct 2026 13:41:15 -0700 Subject: [PATCH 04/65] =?UTF-8?q?=F0=9F=90=9B=20fix(ci):=20rehearse=20disp?= =?UTF-8?q?atched=20lock=20updates?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/check.yaml | 28 ++++++++++++++++++++-------- 1 file changed, 20 insertions(+), 8 deletions(-) diff --git a/.github/workflows/check.yaml b/.github/workflows/check.yaml index 7991ddb..6921dca 100644 --- a/.github/workflows/check.yaml +++ b/.github/workflows/check.yaml @@ -102,7 +102,7 @@ jobs: fi release-dry-run: name: Release without publishing - if: github.event_name == 'pull_request' + if: github.event_name == 'pull_request' || (github.event_name == 'workflow_dispatch' && github.ref != 'refs/heads/main') runs-on: ubuntu-26.04 permissions: contents: read @@ -112,7 +112,7 @@ jobs: with: fetch-depth: 0 persist-credentials: false - ref: ${{ github.event.pull_request.head.sha }} + ref: ${{ github.event.pull_request.head.sha || github.sha }} - uses: astral-sh/setup-uv@1c37ad07a6a961277cf70c0d37d6f313000f5884 # v10.3.0 with: python-version: "3.15" @@ -131,11 +131,21 @@ jobs: GH_TOKEN: ${{ github.token }} run: | set -euo pipefail - git checkout -B release-validation "$BASE_SHA" - git merge --squash "$HEAD_SHA" - printf 'Verify a user-visible release fix.\n' > "changelog.d/+validation-${PR_NUMBER}.bugfix.md" + base_sha=${BASE_SHA:-$(git rev-parse origin/main)} + head_sha=${HEAD_SHA:-$GITHUB_SHA} + pr_number=$PR_NUMBER + if [[ -z "$pr_number" ]]; then + pr_number=$(gh pr list --head "$GITHUB_REF_NAME" --base main --state open --json number --jq '.[0].number') + if [[ -z "$pr_number" ]]; then + echo "::error::no open pull request for $GITHUB_REF_NAME" + exit 1 + fi + fi + git checkout -B release-validation "$base_sha" + git merge --squash "$head_sha" + printf 'Verify a user-visible release fix.\n' > "changelog.d/+validation-${pr_number}.bugfix.md" git add -A - git -c user.name=release-validation -c user.email=release-validation@users.noreply.github.com commit -m "Validate release pipeline (#${PR_NUMBER})" + git -c user.name=release-validation -c user.email=release-validation@users.noreply.github.com commit -m "Validate release pipeline (#${pr_number})" version=$(git tag --list | release-creator tags --next auto --directory changelog.d) git tag --list | release-creator tags "$version" --require-newer previous=$(git tag --list | release-creator tags "$version" --previous) @@ -191,13 +201,15 @@ jobs: - env: NEEDS_JSON: ${{ toJSON(needs) }} EVENT_NAME: ${{ github.event_name }} + REF: ${{ github.ref }} run: | - jq -e --arg event "$EVENT_NAME" ' + jq -e --arg event "$EVENT_NAME" --arg ref "$REF" ' all( to_entries[]; .value.result == "success" or (.value.result == "skipped" and ((.key == "changelog" and $event != "pull_request") or - (.key == "release-dry-run" and $event != "pull_request"))) + (.key == "release-dry-run" and $event != "pull_request" and + ($event != "workflow_dispatch" or $ref == "refs/heads/main")))) ) ' <<< "$NEEDS_JSON" From 28284ffb5186a2f2849b3cadeed2738ff3cf8d6e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bern=C3=A1t=20G=C3=A1bor?= Date: Mon, 19 Oct 2026 17:24:50 -0700 Subject: [PATCH 05/65] =?UTF-8?q?=F0=9F=91=B7=20ci(release):=20share=20che?= =?UTF-8?q?cked=20release=20Actions?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/check.yaml | 101 ++++++++++++++++-------- .github/workflows/prepare-release.yaml | 103 ++++++------------------- .github/workflows/release.yaml | 3 +- pylock.toml | 5 +- pyproject.toml | 8 +- 5 files changed, 103 insertions(+), 117 deletions(-) diff --git a/.github/workflows/check.yaml b/.github/workflows/check.yaml index 6921dca..f9b3970 100644 --- a/.github/workflows/check.yaml +++ b/.github/workflows/check.yaml @@ -121,9 +121,14 @@ jobs: run: | set -euo pipefail uv venv --python 3.15 "$RUNNER_TEMP/release-tools" - uv pip sync --preview-features pylock --python "$RUNNER_TEMP/release-tools/bin/python" --group release pylock.toml + uv pip sync \ + --preview-features pylock \ + --python "$RUNNER_TEMP/release-tools/bin/python" \ + --group release \ + pylock.toml echo "$RUNNER_TEMP/release-tools/bin" >> "$GITHUB_PATH" - - name: Assemble and validate a local release + - id: fixture + name: Create a local release candidate env: BASE_SHA: ${{ github.event.pull_request.base.sha }} HEAD_SHA: ${{ github.event.pull_request.head.sha }} @@ -146,41 +151,72 @@ jobs: printf 'Verify a user-visible release fix.\n' > "changelog.d/+validation-${pr_number}.bugfix.md" git add -A git -c user.name=release-validation -c user.email=release-validation@users.noreply.github.com commit -m "Validate release pipeline (#${pr_number})" - version=$(git tag --list | release-creator tags --next auto --directory changelog.d) - git tag --list | release-creator tags "$version" --require-newer - previous=$(git tag --list | release-creator tags "$version" --previous) - release-creator fragments --repository "$GITHUB_REPOSITORY" --git --resolve-authors - towncrier build --yes --version "$version" - mdformat --wrap 120 CHANGELOG.md - prek run --all-files - git diff HEAD --binary > "$RUNNER_TEMP/release.patch" - test -s "$RUNNER_TEMP/release.patch" - git reset --hard HEAD - git apply --check "$RUNNER_TEMP/release.patch" - git apply --index "$RUNNER_TEMP/release.patch" - while IFS= read -r path; do - case "$path" in - CHANGELOG.md|changelog.d/*.md) ;; - *) echo "::error::unexpected release path: $path"; exit 1 ;; - esac - done < <(git diff --cached --name-only) - grep -Fq "## $version (" CHANGELOG.md - git diff --cached --check - prek run --all-files - if ! git diff --quiet || [[ -n $(git ls-files --others --exclude-standard) ]]; then - echo "::error::prek changed the validated release patch" - exit 1 - fi - git -c user.name=release-validation -c user.email=release-validation@users.noreply.github.com commit -m "release $version" - git tag "$version" - uv build --python 3.15 --python-preference only-managed --sdist --wheel . --out-dir "$RUNNER_TEMP/dist" + git init --bare "$RUNNER_TEMP/rehearsal.git" + git remote add rehearsal "$RUNNER_TEMP/rehearsal.git" + git push rehearsal HEAD:main + { + echo "sha=$(git rev-parse HEAD)" + echo "pr=$pr_number" + gh api "repos/$GITHUB_REPOSITORY/pulls/$pr_number" \ + --jq '"author=\(.user.login)\nauthor_type=\(.user.type)"' + } >> "$GITHUB_OUTPUT" + - id: prepare + uses: tox-dev/github-workflows/prepare-towncrier@f0c484a3cdf6a144312f4c8d31bc74ea745a2fa4 + with: + changelog: CHANGELOG.md + fragments: changelog.d + format: markdown + patch: ${{ runner.temp }}/release.patch + - name: Restore release candidate + run: git reset --hard HEAD + - uses: tox-dev/github-workflows/validate-release-patch@f0c484a3cdf6a144312f4c8d31bc74ea745a2fa4 + with: + version: ${{ steps.prepare.outputs.version }} + patch: ${{ runner.temp }}/release.patch + changelog: CHANGELOG.md + fragments: changelog.d + remote: rehearsal + expected-sha: ${{ steps.fixture.outputs.sha }} + - name: Commit and tag the local candidate + env: + VERSION: ${{ steps.prepare.outputs.version }} + run: | + git -c user.name=release-validation -c user.email=release-validation@users.noreply.github.com commit -m "release $VERSION" + git tag "$VERSION" + - uses: tox-dev/github-workflows/build-python-distribution@f0c484a3cdf6a144312f4c8d31bc74ea745a2fa4 + with: + out-dir: ${{ runner.temp }}/dist + - name: Inspect the candidate + env: + VERSION: ${{ steps.prepare.outputs.version }} + PR_NUMBER: ${{ steps.fixture.outputs.pr }} + PR_AUTHOR: ${{ steps.fixture.outputs.author }} + PR_AUTHOR_TYPE: ${{ steps.fixture.outputs.author_type }} + run: | + set -euo pipefail + previous=$(git tag --list | release-creator tags "$VERSION" --previous) args=() if [[ -n "$previous" ]]; then args+=(--previous "$previous") fi - release-creator notes --changelog CHANGELOG.md --tag "$version" --repository "$GITHUB_REPOSITORY" --artifact-url "https://pypi.org/project/devpi-process/${version}/" "${args[@]}" > "$RUNNER_TEMP/release-notes.md" + printf '* Validate release pipeline by @%s in https://github.com/%s/pull/%s\n' \ + "$PR_AUTHOR" "$GITHUB_REPOSITORY" "$PR_NUMBER" >"$RUNNER_TEMP/generated-notes.md" + release-creator notes --changelog CHANGELOG.md --tag "$VERSION" --repository "$GITHUB_REPOSITORY" --artifact-url "https://pypi.org/project/devpi-process/${VERSION}/" --generated-notes "$RUNNER_TEMP/generated-notes.md" "${args[@]}" >"$RUNNER_TEMP/release-notes.md" test "$(grep -c '^\*\*Full Changelog\*\*:' "$RUNNER_TEMP/release-notes.md")" -eq 1 - sha256sum "$RUNNER_TEMP"/dist/* > "$RUNNER_TEMP/dist.sha256" + grep -Fq "[#${PR_NUMBER}](https://github.com/${GITHUB_REPOSITORY}/pull/${PR_NUMBER})" "$RUNNER_TEMP/release-notes.md" + if [[ "$PR_AUTHOR_TYPE" == Bot ]]; then + if grep -Fq "by @${PR_AUTHOR}" "$RUNNER_TEMP/release-notes.md"; then + echo '::error::bot credit in release notes' + exit 1 + fi + else + grep -Fq "by @${PR_AUTHOR}" "$RUNNER_TEMP/release-notes.md" + fi + if grep -Fq '