Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
184 changes: 184 additions & 0 deletions .github/workflows/publish.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,184 @@
---
name: publish

# Publishes a tagged release to PyPI using PyPI Trusted Publishing (OIDC), so
# no API token is stored anywhere. Replaces the long manual "build in a clean
# clone and twine upload" dance in docs/design/RELEASE-HOWTO.md.
#
# The trigger is a pushed tag. Both flavours of tag go through here:
#
# v3.3.1 - the real release, tagged on the release branch
# v3.3.1a1 - the companion niquests-free alpha, tagged on a throwaway
# branch that is never pushed (see the RELEASE-HOWTO). The
# workflow file comes from the tagged tree, so the alpha builds
# with whatever pyproject.toml that branch carries.
#
# Nothing is uploaded until the build has been checked and - if the `pypi`
# environment is configured with required reviewers - a human has clicked
# Approve. A wrong tag can therefore be rejected without burning the version
# number on PyPI.
#
# TODO (next patch release): verify that the tag carries a good signature from
# the maintainer before anything is built. See the "Verifying the tag
# signature" section of docs/design/RELEASE-HOWTO.md for the recipe.

on:
push:
tags:
# Release and pre-release tags alike: v3.3.1, v3.3.1a1, v3.4.0rc2.
- "v[0-9]+.[0-9]+.[0-9]+*"
# Lets the whole build+check path be rehearsed on an existing tag without
# publishing anything. Use this to shake out workflow mistakes.
workflow_dispatch:

concurrency:
group: publish-${{ github.ref }}
cancel-in-progress: false

permissions:
contents: read

jobs:
build:
name: build and check the artifacts
runs-on: ubuntu-latest
outputs:
version: ${{ steps.version.outputs.version }}
prerelease: ${{ steps.version.outputs.prerelease }}
steps:
- uses: actions/checkout@v5
with:
# hatch-vcs derives the version from the tags.
fetch-depth: 0
- uses: actions/setup-python@v5
with:
python-version: "3.13"
- run: pip install tox packaging
# Same check the nightly `package` workflow runs: builds the sdist and
# the wheel and refuses anything git does not track. This is what keeps
# a stray venv/ or conf_private.py out of the tarball.
- name: Build sdist and wheel, and check what is in them
run: tox -e package
- name: Check the built version against the tag
id: version
env:
REF_NAME: ${{ github.ref_name }}
REF_TYPE: ${{ github.ref_type }}
run: python tests/tools/check_release_version.py .tox/package/tmp/dist
- uses: actions/upload-artifact@v4
with:
name: dist
path: .tox/package/tmp/dist/*
if-no-files-found: error

smoke:
# The full suite already ran on master before the tag was pushed, and it
# needs half a dozen docker servers, so re-running it here would cost
# twenty minutes to learn nothing new. What master CI has *not* seen is
# the alpha's tree, where a dependency was removed from pyproject.toml -
# hence the emphasis on installing the built wheel with only its declared
# dependencies and checking it still imports.
name: smoke test the wheel (${{ matrix.python }})
needs: build
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
python: ["3.10", "3.14"]
steps:
- uses: actions/checkout@v5
with:
# The editable install below goes through hatch-vcs too.
fetch-depth: 0
- uses: actions/setup-python@v5
with:
python-version: ${{ matrix.python }}
- uses: actions/download-artifact@v4
with:
name: dist
path: dist
- name: Install the wheel with only its declared dependencies
run: |
python -m venv /tmp/wheelenv
/tmp/wheelenv/bin/pip install --upgrade pip
/tmp/wheelenv/bin/pip install dist/*.whl
/tmp/wheelenv/bin/pip show caldav
# Run from /tmp, not from the repository root, where the caldav/ source
# tree shadows the installed package and this would be testing the
# checkout rather than the wheel.
#
# Only the bare import is checked against the declared dependencies
# alone, because the alpha declares no HTTP library at all: reaching
# DAVClient without one is *meant* to raise, and does. A supported
# library is then installed - as the alpha's consumers have to - and the
# client is reached for real.
- name: Import the installed package
working-directory: /tmp
run: |
/tmp/wheelenv/bin/python -c "import caldav; print('bare import:', caldav.__version__)"
/tmp/wheelenv/bin/pip install requests
/tmp/wheelenv/bin/python -c "
import caldav
from caldav import DAVClient
print('caldav', caldav.__version__, 'imports cleanly')
"
# A fast sanity run over the tests that need no server at all - it takes
# seconds, and it catches a tag pointing at a tree that never built.
- name: Run the server-less tests
run: |
pip install --editable .[test]
pytest -q tests/test_caldav_unit.py tests/test_cdav.py tests/test_vcal.py \
tests/test_utils.py tests/test_lazy_import.py tests/test_http_libraries.py

publish:
name: publish to PyPI
needs: [build, smoke]
# Only real tag pushes publish; a workflow_dispatch run stops after the
# checks above, which is what makes it usable as a rehearsal.
if: github.event_name == 'push'
runs-on: ubuntu-latest
environment:
name: pypi
url: https://pypi.org/p/caldav
permissions:
# Required for trusted publishing (and for the attestations the publish
# action generates).
id-token: write
steps:
- uses: actions/download-artifact@v4
with:
name: dist
path: dist
- uses: pypa/gh-action-pypi-publish@release/v1
with:
verbose: true

github-release:
# Some people watch the github releases page rather than PyPI, and
# forgetting this entry is on the RELEASE-HOWTO's list of mistakes.
name: create the github release
needs: [build, publish]
runs-on: ubuntu-latest
permissions:
contents: write
steps:
- uses: actions/checkout@v5
with:
fetch-depth: 0
- uses: actions/download-artifact@v4
with:
name: dist
path: dist
# The annotated tag message is the release notes - it is written by hand
# at tagging time and there is no point in writing them twice.
- name: Create the release from the tag message
env:
GH_TOKEN: ${{ github.token }}
TAG: ${{ github.ref_name }}
PRERELEASE: ${{ needs.build.outputs.prerelease }}
run: |
git tag -l --format='%(contents)' "$TAG" > /tmp/notes.md
gh release create "$TAG" dist/* \
--title "$TAG" \
--notes-file /tmp/notes.md \
$([ "$PRERELEASE" = "true" ] && echo --prerelease || echo --latest)
5 changes: 5 additions & 0 deletions .lycheeignore
Original file line number Diff line number Diff line change
Expand Up @@ -61,3 +61,8 @@ https://dav\.qq\.com/.*
# pushes the code before the tag, so this view 404s until `v3.3.0` is pushed -
# delete this entry once it is.
https://github\.com/python-caldav/caldav/compare/v3\.2\.1\.\.\.v3\.3\.0

# Management pages behind a login - RELEASE-HOWTO links them for the
# maintainer's benefit, lychee only sees a login page or a 404.
https://pypi\.org/manage/project/caldav/.*
https://github\.com/python-caldav/caldav/settings/.*
Loading
Loading