Skip to content
Merged
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
26 changes: 10 additions & 16 deletions .github/workflows/ci-checks.yml
Original file line number Diff line number Diff line change
Expand Up @@ -20,19 +20,13 @@ jobs:
with:
python-version-file: '.python-version'

- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install -r ci-requirements.txt

- name: Lint code
run: ruff check src

- name: Run type checks
run: |
mypy src
basedpyright src

# add this when tests are ready
# - name: Run tests
# run: pytest
- name: Setup uv
uses: astral-sh/setup-uv@v6
with:
enable-cache: true

- name: Setup Nox
uses: wntrblm/[email protected]

- name: Run Nox sessions
run: nox
86 changes: 41 additions & 45 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
# HOW THIS WORKFLOW WORKS
# This workflow automates creating a GitHub Release and publishing your Python package to PyPI.
# This workflow automates creating a GitHub Release and publishing the Python package to PyPI.

# --- Manual Steps ---
# 1. Edit `pyproject.toml` to set the new version (e.g., `version = "1.2.3"` or `version = "1.2.4-rc1"` for pre-releases).
Expand All @@ -8,42 +8,44 @@
# 4. Use the justfile: `just release` to trigger the process.

# --- Automation ---
# The workflow then automatically:
# - runs .github/scripts/tag_release.py to create a new tag based on the version in `pyproject.toml`.
# - Pushes the new tag to github which triggers this workflow.
# - Pushes the new tag to github which triggers this workflow file.
# - Checks that the tag matches the version in `pyproject.toml`.
# - Builds the sdist and wheel.
# - Publishes the package to PyPI.
# - Creates a new GitHub Release based on the tag with logic for marking pre-releases.
# - Publishes the package to PyPI using trusted publishing.
# - Reads the release notes from your CHANGELOG.md.
# - Creates a new GitHub Release, marking it as a pre-release if necessary.


name: Create Release and Publish to PyPI

on:
push:
tags:
- "v*" # Runs on any tag starting with "v", e.g., v1.2.3
- "v*" # Runs on any tag starting with "v", e.g., v1.2.3 or v1.2.3-rc1
workflow_dispatch:
inputs:
tag_name:
description: 'Tag name to create release for (e.g., v1.2.3)'
description: 'Tag to create release for (e.g., v1.2.3). Must start with "v".'
required: true
type: string

jobs:
build-and-publish:
name: Build and Publish
runs-on: ubuntu-latest
# These permissions are required for the actions below.
permissions:
id-token: write # Required for Trusted Publishing with PyPI (OIDC).
contents: write # Required to create the GitHub Release.

steps:
# If manually triggered, checkout the specific tag. Otherwise, it checks out the tag that triggered the workflow.
# Fetch all history for all tags so the changelog-reader can find previous tags.
- name: Check out code
uses: actions/checkout@v4
with:
# If manually triggered, checkout the specific tag
ref: ${{ github.event_name == 'workflow_dispatch' && inputs.tag_name || github.ref }}
ref: ${{ github.event.inputs.tag_name || github.ref }}
fetch-depth: 0

- name: Set up Python
uses: actions/setup-python@v5
Expand All @@ -53,51 +55,45 @@ jobs:
- name: Install required python packages
run: python -m pip install --upgrade build tomli

- name: Set tag name
id: tag
# github.ref_name is the name of the tag that triggered this workflow,
# if it was triggered by a tag push and not manually.
# inputs.tag_name is used when the workflow is manually triggered.
# Use github.ref_name which reliably gives the tag name (e.g., "v1.2.3")
# Create a step output named 'version' that contains the tag name without the 'v'
- name: Verify tag matches pyproject.toml version
run: |
if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then
TAG_NAME="${{ inputs.tag_name }}"
else
TAG_NAME="${{ github.ref_name }}"
fi
echo "tag_name=$TAG_NAME" >> $GITHUB_OUTPUT
echo "Using tag: $TAG_NAME"

- name: Verify tag matches version
run: |
TAG_VERSION=${{ steps.tag.outputs.tag_name }}
TAG_NAME="${{ github.ref_name }}"
PYPROJECT_VERSION=$(python -c "import tomli; print(tomli.load(open('pyproject.toml', 'rb'))['project']['version'])")
if [ "v$PYPROJECT_VERSION" != "$TAG_VERSION" ]; then
echo "Error: Tag $TAG_VERSION does not match pyproject.toml version v$PYPROJECT_VERSION"

if [ "v$PYPROJECT_VERSION" != "$TAG_NAME" ]; then
echo "Error: Tag '$TAG_NAME' does not match pyproject.toml version 'v$PYPROJECT_VERSION'"
exit 1
fi
fi

echo "Tag and pyproject.toml version match: $TAG_NAME"
echo "version=${TAG_NAME#v}" >> $GITHUB_OUTPUT

- name: Build package
run: python -m build

- name: Publish to PyPI
# This action uses Trusted Publishing, which is configured in your PyPI project settings.
# It avoids the need for storing API tokens as secrets.
uses: pypa/gh-action-pypi-publish@release/v1

- name: Extract changelog for release
id: changelog
run: |
VERSION=${{ steps.tag.outputs.tag_name }}
VERSION=${VERSION#v} # Strip leading "v"
awk "/## \[${VERSION//./\\.}\]/,/^## \[/" CHANGELOG.md | head -n -1 > body.md
- name: Get Changelog Entry
id: changelog_reader
uses: mindsers/changelog-reader-action@v2
with:
validation_level: warn
version: ${{ steps.version_check.outputs.version }}
path: ./CHANGELOG.md

- name: Create GitHub Release
uses: softprops/action-gh-release@v2
uses: ncipollo/release-action@v1
with:
tag_name: ${{ steps.tag.outputs.tag_name }}
# The release title will be "Release vX.X.X" or "Pre-release vX.X.X-rc1"
# depending on whether a hyphen is present in the tag name.
release_name: ${{ startsWith(steps.tag.outputs.tag_name, 'v') && contains(steps.tag.outputs.tag_name, '-') && format('Pre-release {0}', steps.tag.outputs.tag_name) || format('Release {0}', steps.tag.outputs.tag_name) }}
# Marks the release as a "pre-release" on GitHub if the tag contains a hyphen (e.g., "-rc1").
prerelease: ${{ contains(steps.tag.outputs.tag_name, '-') }}
body_path: body.md
# Use the tag name that triggered the workflow
tag: ${{ github.ref_name }}
# The release title will be, e.g., "Release v1.2.3"
name: Release ${{ github.ref_name }}
# The body of the release is the changelog entry from the previous step
body: ${{ steps.changelog_reader.outputs.changes }}
# Automatically mark as pre-release if the tag contains a hyphen (e.g., v1.2.3-rc1)
prerelease: ${{ contains(github.ref_name, '-') }}
# This allows the action to update a release if it already exists
allowUpdates: true
9 changes: 8 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,14 @@ build/
dist/
wheels/
*.egg-info
*_cache/

# Textual stuff
snapshot_report.html

# Virtual environments
.venv

# tooling stuff
*_cache/
.nox/
.crush
10 changes: 10 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,15 @@
# Textual-Color-O-Matic Changelog

## [1.0.1] 2025-07-29

- Dropped the required Textual version back down to 3.7.1 (last 3.x.x release) to maintain compatibility with Textual 3.x.x.
- Made some changes to the demo to make the library compatible with Textual 3.x.x
- Added `/tests` directory with unit tests for the ColorOMatic, a [pytest] section in `pyproject.toml`, and added `just test` command to the justfile.
- Added Nox testing and `noxfile.py` to run tests in different Python versions and across different versions of Textual.
- Added pytest, pytest-asyncio, and pytest-textual-snapshot to dev dependencies.
- Deleted `ci-requirements.txt` as it is no longer needed with the new Nox setup.
- Changed `ci-checks.yml` to run Nox instead of individual commands for MyPy, Ruff, Pytest, etc.

## [1.0.0] 2025-07-28

### Usage / API changes
Expand Down
7 changes: 0 additions & 7 deletions ci-requirements.txt

This file was deleted.

20 changes: 17 additions & 3 deletions justfile
Original file line number Diff line number Diff line change
Expand Up @@ -35,9 +35,19 @@ typecheck:
format:
@uv run black src

test:
@uv run pytest tests -vvv

test-update:
@uv run pytest tests -vvv --snapshot-update

# Runs ruff, mypy, and black
all-checks: lint typecheck format
echo "All pre-commit checks passed. You're good to PR to main."
all-checks: lint typecheck format test
echo "All checks passed."

# Run the Nox testing suite for comprehensive testing
nox:
nox

# Remove build/dist directories and pyc files
clean:
Expand All @@ -48,14 +58,18 @@ clean:
clean-caches:
rm -rf .mypy_cache
rm -rf .ruff_cache
rm -rf .nox

# Remove the virtual environment and lock file
del-env:
rm -rf .venv
rm -rf uv.lock

nuke: clean clean-caches del-env
@echo "All build artifacts and caches have been removed."

# Removes all environment and build stuff
reset: clean clean-caches del-env install
reset: nuke install
@echo "Environment reset."

# Release the kraken
Expand Down
90 changes: 90 additions & 0 deletions noxfile.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
"""Config file for Nox sessions
By Edward Jazzhands - 2025

NOTE ABOUT NOX CONFIG:
If you are doing dev work in some kind of niche environment such as a Docker
container or on a server, you might not have symlinks available to you.
In that case, you can set `nox.options.reuse_existing_virtualenvs = True

Setting `nox.options.reuse_existing_virtualenvs` to True will make Nox
reuse environments between runs, preventing however many GB of data from
being written to your drive every time you run it. (Note: saves environments
between runs of Nox, not between sessions of the same run).

If you do not need to reuse existing virtual environments, you can set
`nox.options.reuse_existing_virtualenvs = False` and `DELETE_VENV_ON_EXIT = True`
to delete the virtual environments after each session. This will ensure that
you do not have any leftover virtual environments taking up space on your drive.
Nox would just delete them when starting a new session anyway.
"""

import nox
import pathlib
import shutil

# PYTHON_VERSIONS = ["3.9", "3.10", "3.11", "3.12"]
PYTHON_VERSIONS = ["3.9"]
MAJOR_TEXTUAL_VERSIONS = [3, 4, 5]

##############
# NOX CONFIG #
##############

nox.options.reuse_existing_virtualenvs = True
nox.options.stop_on_first_error = True
DELETE_VENV_ON_EXIT = False

if nox.options.reuse_existing_virtualenvs and DELETE_VENV_ON_EXIT:
raise ValueError(
"You cannot set both `nox.options.reuse_existing_virtualenvs`"
"and `DELETE_VENV_ON_EXIT` to True (Technically this would not cause "
"an error, but it would be pointless)."
)

################
# NOX SESSIONS #
################

@nox.session(
venv_backend="uv",
python=PYTHON_VERSIONS,
)
@nox.parametrize("ver", MAJOR_TEXTUAL_VERSIONS)
def tests(session: nox.Session, ver: int) -> None:

session.run_install(
"uv",
"sync",
"--quiet",
"--reinstall",
f"--python={session.virtualenv.location}",
env={"UV_PROJECT_ENVIRONMENT": session.virtualenv.location},
external=True,
)

session.run_install(
"uv", "pip", "install",
f"textual<{ver + 1}.0.0",
external=True,
)
session.run("uv", "pip", "show", "textual")
# EXPLANATION: The `ver + 1` is a trick to make UV
# only download the last version of each major revision of Textual.
# If the current version is 3, we're saying `install textual<4.0.0`.
# This will make UV grab the highest version of Textual 3.x.x, which is 3.7.1.
# The last `uv pip show textual` is just for logging purposes.

# These are all assuming you have corresponding
# sections in your pyproject.toml for configuring each tool:
session.run("ruff", "check", "src")
session.run("mypy", "src")
session.run("basedpyright", "src")
session.run("pytest", "tests", "-vvv")

# This code here will make Nox delete each session after it finishes.
# This might be preferable to allowing it all to accumulate and then deleting
# the folder afterwards (for example if testing would use dozens of GB of data and
# you don't have the disk space to store it all temporarily).
session_path = pathlib.Path(session.virtualenv.location)
if session_path.exists() and session_path.is_dir() and DELETE_VENV_ON_EXIT:
shutil.rmtree(session_path)
Loading