Skip to content
Open
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
37 changes: 18 additions & 19 deletions .github/workflows/release-pypi.yml
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,8 @@ jobs:
# REFER: GITHUB_REF is the branch ref or tag that triggered the workflow run.
# https://docs.github.com/en/actions/learn-github-actions/variables#default-environment-variables
- name: Probe version tag (not workflow_dispatch)
if: github.event_name != 'workflow_dispatch'
shell: bash
run: |
VERSION_TAG="${GITHUB_REF#refs/tags/}"

Expand All @@ -100,21 +102,19 @@ jobs:
fi

echo "VERSION_TAG=${VERSION_TAG}" >> "${GITHUB_ENV}"
shell: bash
if: github.event_name != 'workflow_dispatch'

- name: Checkout repository (not workflow_dispatch)
uses: actions/checkout@v4
if: github.event_name != 'workflow_dispatch'
uses: actions/checkout@v4

- name: Checkout repository (is workflow_dispatch)
if: github.event_name == 'workflow_dispatch'
uses: actions/checkout@v4
with:
ref: ${{ github.event.repository.default_branch }}
# Grab everything, because we might need to switch branches
# in the next step.
fetch-depth: 0
if: github.event_name == 'workflow_dispatch'

# Check the default branch HEAD for a version tag.
# - If it's not found, try the pre-release branch.
Expand Down Expand Up @@ -198,8 +198,8 @@ jobs:
# ~/.kit/sh/sh-git-nubs/bin/git-nubs.sh
# git_latest_version_tag
- name: Fail if VERSION_TAG undiscoverable
run: exit 1
if: env.VERSION_TAG == ''
run: exit 1

# Double-check tag is version-formatted, esp. if 'workflow_dispatch'
# (which got whatever was tagged on HEAD).
Expand All @@ -221,6 +221,7 @@ jobs:
shell: bash

- name: Set pipx index URL
shell: bash
run: |
test_pypi_url="https://test.pypi.org/simple"
# REFER: https://packaging.python.org/en/latest/guides/using-testpypi/
Expand All @@ -246,7 +247,6 @@ jobs:
&& index_url_pipx="" \
|| index_url_pipx="--index-url ${test_pypi_url} ${pipx_pip_args}"
echo INDEX_PIPX=${index_url_pipx} >> "${GITHUB_ENV}"
shell: bash

# MAYBE/2023-11-11: Prefer `fromJSON(shell-bool)` vs. `== 'true|false'`?
- name: Announcement — ${{
Expand All @@ -261,8 +261,8 @@ jobs:

# github.repository is, e.g.,"doblabs/easy-as-pypi".
- name: Probe package name
run: echo "PACKAGE_NAME=$(basename ${{ github.repository }})" >> "${GITHUB_ENV}"
shell: bash
run: echo "PACKAGE_NAME=$(basename ${{ github.repository }})" >> "${GITHUB_ENV}"

# # Local, and GHA: 1.2.0.
# - name: pipx --version
Expand All @@ -271,22 +271,22 @@ jobs:

# On GHA: pipx: error: unrecognized arguments: easy-as-pypi==1.1.1a20
- name: Check if previously released
shell: bash
run: |
IS_RELEASED=false
echo "pip install ${INDEX_PIP} ${PACKAGE_NAME}==${VERSION}"
pip install ${INDEX_PIP} ${PACKAGE_NAME}==${VERSION} \
&& IS_RELEASED=true
echo "IS_RELEASED=${IS_RELEASED}" >> "${GITHUB_ENV}"
shell: bash

# Determine if previously released (and skip re-releasing if so).
- name: View release
if: env.IS_RELEASED == 'true'
run: |
echo "gh release view ${VERSION_TAG}"
gh release view ${VERSION_TAG}
env:
GH_TOKEN: ${{ github.token }}
if: env.IS_RELEASED == 'true'

# Here's how you might publish using Poetry, but this approach
# requires a PyPI token.
Expand Down Expand Up @@ -363,20 +363,17 @@ jobs:

wrangle-ci-tags:
name: Wrangle (read) CI tags
uses: ./.github/workflows/ci-tags-wrangle.yml
needs: [prepare-publish]
if: ${{ ! cancelled()
&& needs.prepare-publish.result == 'success'
&& needs.prepare-publish.outputs.IS_RELEASED == 'false' }}
uses: ./.github/workflows/ci-tags-wrangle.yml
with:
prerelease: ${{ needs.prepare-publish.outputs.PRERELEASE == 'true' }}

# ***

poetry-publish:

runs-on: ubuntu-latest

needs: [prepare-publish, wrangle-ci-tags]

# Easiest approach would be to skip if IS_RELEASED, e.g.,
Expand All @@ -390,6 +387,8 @@ jobs:
&& needs.prepare-publish.outputs.IS_RELEASED == 'false'
&& needs.wrangle-ci-tags.outputs.old-inhibit-release-pypi == 'false' }}

runs-on: ubuntu-latest

# CXREF: PyPI recommends using an environment ('release')
# to keep non-admin organization members from publishing.
# - BWARE: The environment configuration's branch protection rules
Expand Down Expand Up @@ -460,9 +459,6 @@ jobs:
# https://github.com/pypa/gh-action-pypi-publish

- name: Publish package distributions to test.PyPI
uses: pypa/gh-action-pypi-publish@release/v1
with:
repository-url: https://test.pypi.org/legacy/
# Only publish pre-release packages to test.PyPI.
#
# - It's not prudent to publish normal packages to test.PyPI,
Expand All @@ -481,12 +477,15 @@ jobs:
# - So for either type or release cascade, whether 'alpha'
# or 'patch', we always publish to the same package index.
if: env.PRERELEASE == 'true'
uses: pypa/gh-action-pypi-publish@release/v1
with:
repository-url: https://test.pypi.org/legacy/

- name: Publish package distributions to PyPI
uses: pypa/gh-action-pypi-publish@release/v1
# Unlike test.PyPI, only publish normal releases to PyPI proper,
# never alphas.
if: env.PRERELEASE == 'false'
uses: pypa/gh-action-pypi-publish@release/v1

# ***

Expand All @@ -512,9 +511,9 @@ jobs:
# immediately, but it takes a bit for pip to find it (~12 sec).
# - SAVVY/2023-10-08: Huh, 17s not always enough?
- name: Sleep for 13 seconds
run: sleep 13s
shell: bash
if: needs.prepare-publish.outputs.IS_RELEASED == 'false'
shell: bash
run: sleep 13s

# ***

Expand Down
9 changes: 3 additions & 6 deletions .github/workflows/release-smoke-test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -33,12 +33,9 @@ on:
# ***

env:
# Lest: "The `python-version` input is not set. The version
# of Python currently in `PATH` will be used." and
# "Cache paths are empty. Please check the previous
# logs and make sure that the python version is specified"
# TRACK: https://github.com/actions/python-versions/releases
# USYNC: workflows/ (PYTHON_VERSION), tox.ini (basepython), Makefile (VENV_PYVER).
# USYNC: When you update Python versions, update the following:
# workflows/ (PYTHON_VERSION), tox.ini (basepython), Makefile (VENV_PYVER)
# - REFER: Track versions at https://github.com/actions/python-versions/releases
# BWARE: Trailing zeroes disappear unless quoted.
PYTHON_VERSION: "3.11"

Expand Down
147 changes: 125 additions & 22 deletions README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -49,39 +49,142 @@ Overview

Boilerplate for modern, bathroom-tub-included Python projects.

The boilerplate itself is installable and includes minimalist Click CLI.
This project is installable and includes a minimalist
`Click <https://palletsprojects.com/p/click/>`__ CLI::

But most of the gold is buried within:
pip install easy-as-pypi

easy-as-pypi

But this project is so much more!

This is *living* boilerplate.

- Use this project to start a new Python project.

- Use this project to manage your CI checks.

- Use this project to automate your releases.

- And keep your project up-to-date with the latest "boilerplate" by running::

bin/update-faithful

- Because boilerplate is *never* static!

Here are the selling points, because we're all here to make gold:

- Modern Poetry and ``pyproject.toml`` setup.

- Supports cascading editable installs (install current project in
editable mode, as well as any dependencies you might have source
for locally; boilerplate manages alternative ``pyproject.toml``
automatically).
- Much of any project's ``pyproject.toml`` is already prescribed, like,
90% of your projects' ``pyproject.toml`` is the same between all your
projects.

- So this project *generates* ``pyproject.toml``.

It uses a simple template, located at ``.pyproject.project.tmpl``,
and applies it to a base ``pyproject.toml`` template
(named ``.pyproject.tmpl``).

- Use ``.pyproject.project.tmpl`` to add the few bits that are unique
to your project, then run ``bin/update-faithful`` to generate
``pyproject.toml`` for your project.

- Most of ``pyproject.toml`` is already boilerplate — think of
``pytest`` options, ``black`` options, ``flake8`` options,
``isort`` options, as well as ``poetry install --with``
dependencies, like those for testing, or creating docs,
etc. — these are usually the same for all of your projects!
So why repeat yourself?

As such, we consider ``pyproject.toml`` to be essentially
boilerplate — it's half boilerplate, half project-specific,
and generated when you run ``bin/update-faithful``.

- *Editable* installs.

- Run ``make develop`` to install your project in editable mode,
as well as any dependencies that you have sources for locally.

- This project (the boilerplate) will manage an alternative
``.pyproject-editable/pyproject.toml`` automatically.

- Edit ``Makefile`` to tell it which projects are *editable*
(specifically the ``EDITABLE_PJS`` environ).

- Pre-release installs.

- Publish pre-release builds to https://test.pypi.org

- This project (the boilerplate) manages an alternative
``.pyproject-prerelease/pyproject.toml`` and ``poetry.lock``,
as well as using the appropriate ``pip install --pre`` options,
so you can test changes to your stack before releasing to PyPI,

- All the lints.

- Run ``make lint`` to run all the lints: ``black``, ``flake8``,
``isort``, ``pydocstyle``, ``doc8``, ``linkcheck``,
``poetry check``, and ``twine check``.

- All the tests.

- Run ``tox`` to test against all supported Python versions.

- All the other dev tasks.

- Run ``make develop`` to create an editable virtualenv (using local sources).

- Then you can run your app locally, against local sources,
by calling ``make test``, or ``pytest``.

- You can also use ``pyenv`` and modify the Python version in
``Makefile`` to test against different Python versions, e.g.,::

pyenv install -s 3.12

sed -i 's/^VENV_PYVER.*/VENV_PYVER ?= 3.12/' Makefile

make develop

workon

make test

- Run ``make install`` to create a release virtualenv (using PyPI sources),
so you can test what end-users will experience (though not the same as
publishing to PyPI and running ``pip install``, but close).

- Run ``make docs`` to generate docs for *ReadTheDocs*.

- Run ``make coverage`` to, ya know, run coverage.

- Also Babel support, e.g., run ``make babel-compile`` to localize user
messages.

- All the lints: ``black``, ``flake8``, ``isort``, ``pydocstyle``,
``doc8``, ``linkcheck``, ``poetry check``, and ``twine check``.
- All the CI.

- Test against all active Python versions and lint using ``tox``.
- Look under ``.github/workflows`` for what some might consider an
over-engineered GitHub Actions workflow.

- Run tasks, tests, and setup virtualenvs quickly using ``make``
commands in your active virtualenv.
But that's really where's there gold:

- Generate docs for *ReadTheDocs*.
- When you push a branch, checks run.

- Localize user messages using ``Babel``.
- When you push a version tag, a release happens.

- Easily install to shared or isolated virtualenvs.
- After checks run, and after a release is published,
a "smoke test" runs: Both ``pip`` and ``pipx`` are
called to verify your package is viable.

- GitHub Actions linting, testing, and coverage upload.
- And lemme tell you, Poetry might work, publishing
to PyPI might work, but that still doesn't mean
the release works. The smoke test lets you know
it works for certain.

Most of the files are designed to be hard linked from the derived
projects themselves, as they won't need to be customized (such as
``Makefile``).
- And if you maintain multiple projects, the CI job
will dispatch and kick-off the release of the next
downstream project.

- Then when the boilerplate changes, you can just commit the
changes in the derived project, call them "dependency updates"
or something, and not have to worry about merging changes manually
(and running ``meld`` or something).
Point being, this is the Python "boilerplate" to end all boilerplate.

Loading