diff --git a/.github/workflows/codespell.yml b/.github/workflows/codespell.yml index 863f4edb..fcf5693c 100644 --- a/.github/workflows/codespell.yml +++ b/.github/workflows/codespell.yml @@ -4,7 +4,9 @@ name: Codespell on: pull_request: + branches: [master] push: + branches: [master] permissions: contents: read diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index 81ae37f2..89921a97 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -2,7 +2,9 @@ name: Lints on: pull_request: + branches: [master] push: + branches: [master] paths-ignore: - '**.rst' diff --git a/.github/workflows/parse_release_notes.py b/.github/workflows/parse_release_notes.py new file mode 100644 index 00000000..a2b25cda --- /dev/null +++ b/.github/workflows/parse_release_notes.py @@ -0,0 +1,94 @@ +"""Parse the latest release notes from CHANGELOG.md. + +If running in GitHub Actions, set the `release_title` output +variable for use in subsequent step(s). + +If running in CI, write the release notes to ReleaseNotes.md +for upload as an artifact. + +Otherwise, print the release title and notes to stdout. +""" + +import re +import subprocess +from os import environ +from pathlib import Path + + +class ChangesEntry: + def __init__(self, version: str, notes: str) -> None: + self.version = version + title = notes.splitlines()[0] + self.title = f'{version} {title}' + self.notes = notes[len(title) :].strip() + + +H1 = re.compile(r'^# (\d+\.\d+\.\d+)', re.MULTILINE) + + +def parse_changelog() -> list[ChangesEntry]: + changelog = Path('CHANGELOG.md').read_text(encoding='utf-8') + parsed = H1.split(changelog) # may result in a blank line at index 0 + if not parsed[0]: # leading entry is a blank line due to re.split() implementation + parsed = parsed[1:] + assert len(parsed) % 2 == 0, ( + 'Malformed CHANGELOG.md; Entries expected to start with "# x.y.x"' + ) + + changes: list[ChangesEntry] = [] + for i in range(0, len(parsed), 2): + version = parsed[i] + notes = parsed[i + 1].strip() + changes.append(ChangesEntry(version, notes)) + return changes + + +def get_version_tag() -> str | None: + if 'GITHUB_REF' in environ: # for use in GitHub Actions + git_ref = environ['GITHUB_REF'] + else: # for local use + git_out = subprocess.run( + ['git', 'rev-parse', '--symbolic-full-name', 'HEAD'], + capture_output=True, + text=True, + check=True, + ) + git_ref = git_out.stdout.strip() + version: str | None = None + if git_ref and git_ref.startswith('refs/tags/'): + version = git_ref[len('refs/tags/') :].lstrip('v') + else: + print( + f"Using latest CHANGELOG.md entry because the git ref '{git_ref}' is not a tag." + ) + return version + + +def get_entry(changes: list[ChangesEntry], version: str | None) -> ChangesEntry: + latest = changes[0] + if version is not None: + for entry in changes: + if entry.version == version: + latest = entry + break + else: + raise ValueError(f'No changelog entry found for version {version}') + return latest + + +def main() -> None: + changes = parse_changelog() + version = get_version_tag() + latest = get_entry(changes=changes, version=version) + if 'GITHUB_OUTPUT' in environ: + with Path(environ['GITHUB_OUTPUT']).open('a') as gh_out: + print(f'release_title={latest.title}', file=gh_out) + if environ.get('CI', 'false') == 'true': + Path('ReleaseNotes.md').write_text(latest.notes, encoding='utf-8') + else: + print('Release notes:') + print(f'# {latest.title}\n{latest.notes}') + + +if __name__ == '__main__': + main() diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 91851e3b..66303432 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -1,40 +1,14 @@ -name: Tests +name: Tests (s390x) on: pull_request: + branches: [master] push: + branches: [master] paths-ignore: - '**.rst' jobs: - linux: - runs-on: ${{ matrix.os }} - strategy: - matrix: - include: - - os: ubuntu-24.04 - python-version: '3.10' - - os: ubuntu-24.04 - python-version: '3.13' - - os: ubuntu-24.04 - python-version: 'pypy3.10' - - os: ubuntu-24.04-arm - python-version: '3.13' - - steps: - - name: Checkout pygit2 - uses: actions/checkout@v5 - - - name: Set up Python - uses: actions/setup-python@v6 - with: - python-version: ${{ matrix.python-version }} - - - name: Linux - run: | - sudo apt install tinyproxy - LIBSSH2_VERSION=1.11.1 LIBGIT2_VERSION=1.9.1 /bin/sh build.sh test - linux-s390x: runs-on: ubuntu-24.04 if: github.ref == 'refs/heads/master' @@ -53,19 +27,3 @@ jobs: run: | LIBSSH2_VERSION=1.11.1 LIBGIT2_VERSION=1.9.1 /bin/sh build.sh test continue-on-error: true # Tests are expected to fail, see issue #812 - - macos-arm64: - runs-on: macos-latest - steps: - - name: Checkout pygit2 - uses: actions/checkout@v5 - - - name: Set up Python - uses: actions/setup-python@v6 - with: - python-version: '3.13' - - - name: macOS - run: | - export OPENSSL_PREFIX=`brew --prefix openssl@3` - LIBSSH2_VERSION=1.11.1 LIBGIT2_VERSION=1.9.1 /bin/sh build.sh test diff --git a/.github/workflows/wheels.yml b/.github/workflows/wheels.yml index 13303889..3cfb8d46 100644 --- a/.github/workflows/wheels.yml +++ b/.github/workflows/wheels.yml @@ -1,12 +1,18 @@ name: Wheels +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: ${{ github.ref_name != 'master' }} + on: push: - branches: - - master - - wheels-* + branches: [master] tags: - - 'v*' + - 'v*' + pull_request: + branches: [master] + paths-ignore: + - 'docs/**' jobs: build_wheels: @@ -54,6 +60,7 @@ jobs: build_wheels_ppc: name: Wheels for linux-ppc + if: github.ref == 'refs/heads/master' runs-on: ubuntu-24.04 steps: @@ -86,6 +93,8 @@ jobs: sdist: runs-on: ubuntu-latest + outputs: + release_title: ${{ steps.parse_changelog.outputs.release_title }} steps: - uses: actions/checkout@v5 with: @@ -104,6 +113,17 @@ jobs: name: wheels-sdist path: dist/* + - name: parse CHANGELOG for release notes + id: parse_changelog + run: python .github/workflows/parse_release_notes.py + + - name: Upload Release Notes + uses: actions/upload-artifact@v4 + with: + name: release-notes + path: ReleaseNotes.md + + twine-check: name: Twine check # It is good to do this check on non-tagged commits. @@ -123,7 +143,9 @@ jobs: pypi: if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags/v') - needs: [build_wheels, build_wheels_ppc] + needs: [build_wheels, build_wheels_ppc, sdist] + permissions: + contents: write # to create GitHub Release runs-on: ubuntu-24.04 steps: @@ -140,3 +162,21 @@ jobs: with: user: __token__ password: ${{ secrets.PYPI_API_TOKEN }} + skip-existing: true + + - uses: actions/download-artifact@v5 + with: + name: release-notes + - name: Create GitHub Release + env: + GITHUB_TOKEN: ${{ github.token }} + TAG: ${{ github.ref_name }} + REPO: ${{ github.repository }} + TITLE: ${{ needs.sdist.outputs.release_title }} + # https://cli.github.com/manual/gh_release_create + run: >- + gh release create ${TAG} + --verify-tag + --repo ${REPO} + --title ${TITLE} + --notes-file ReleaseNotes.md diff --git a/pyproject.toml b/pyproject.toml index 1a5a95a6..2db7c957 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -12,7 +12,7 @@ environment = {LIBGIT2_VERSION="1.9.1", LIBSSH2_VERSION="1.11.1", OPENSSL_VERSIO before-all = "sh build.sh" test-command = "pytest" -test-sources = ["test"] +test-sources = ["test", "pytest.ini"] before-test = "pip install -r {project}/requirements-test.txt" # Will avoid testing on emulated architectures (specifically ppc64le) test-skip = "*-*linux_ppc64le"