diff --git a/.claude/commands/ready-release.md b/.claude/commands/ready-release.md index 20aee13c..45f1c4f7 100644 --- a/.claude/commands/ready-release.md +++ b/.claude/commands/ready-release.md @@ -1,21 +1,13 @@ -Walk me through readying a gxformat2 release. Run each step, check for problems, and pause if anything looks wrong. +Walk me through readying a gxformat2 release. The full process is documented in `docs/developing.rst` — read that first and defer to it for details. This command drives the interactive parts. ## Steps -1. **Check git status** - ensure working tree is clean, no missing files. +1. **Confirm target version** — read `__version__` in `gxformat2/__init__.py`, show me the current `.devN`, and ask me to confirm the target release version. -2. **Verify version** - read `gxformat2/__init__.py` and confirm `__version__` is a `.devN` variant of the intended release. Show me the current version and ask me to confirm the target release version. +2. **Populate changelog** — run `make add-history`, show me the new `HISTORY.rst` entries for review, pause so I can edit if needed, then commit the result (`make check-release` requires a clean tree). -3. **Setup venv** - check `.venv` exists. If not, run `make setup-venv`. Confirm dev-requirements are installed. +3. **Pre-release checks** — run `make check-release`. If it fails, stop and report. Do not try to work around failures; fix the underlying issue. -4. **Check UPSTREAM remote** - the Makefile defaults `UPSTREAM` to `galaxyproject`. Check if `$UPSTREAM` is set in the environment; if not, check if a git remote named `galaxyproject` exists. If it doesn't, check for `origin` or `upstream` remotes pointing to `galaxyproject/gxformat2` and offer to create a `galaxyproject` alias via `git remote add galaxyproject `. This must be resolved before `make release` can push. - -5. **Add history** - run `make add-history` to pull contributions into HISTORY.rst under the .dev0 entry. Show me the new HISTORY.rst additions for review. - -6. **Lint** - run `make clean && make lint`. Report any failures. - -7. **Review** - show me a summary of outstanding uncommitted changes (if any) and ask if I want to commit them before proceeding. - -8. **Release** - after I confirm, run `make release` which does: commit-version, new-version, release-artifacts, push-release. This tags, bumps to next dev version, and pushes upstream. +4. **Release** — after I confirm, run `make release`. Stop after each step and report status before moving to the next. diff --git a/Makefile b/Makefile index 56a9040a..117ee6b3 100644 --- a/Makefile +++ b/Makefile @@ -15,8 +15,9 @@ BUILD_SCRIPTS_DIR=scripts DEV_RELEASE?=0 VERSION?=$(shell DEV_RELEASE=$(DEV_RELEASE) python $(BUILD_SCRIPTS_DIR)/print_version_for_release.py $(SOURCE_DIR) $(DEV_RELEASE)) DOC_URL?=https://gxformat2.readthedocs.org -PROJECT_URL?=https://github.com/jmchilton/gxformat2 PROJECT_NAME?=gxformat2 +UPSTREAM_REPO?=$(UPSTREAM)/$(PROJECT_NAME) +PROJECT_URL?=https://github.com/$(UPSTREAM_REPO) TEST_DIR?=tests DOCS_DIR?=docs @@ -45,7 +46,7 @@ clean-test: ## remove test and coverage artifacts setup-venv: ## setup a development virutalenv in current directory if command -v uv > /dev/null 2>&1; then \ - uv sync --group test --group lint --group mypy; \ + uv sync; \ else \ if [ ! -d $(VENV) ]; then \ python3 -m venv $(VENV); \ @@ -53,11 +54,12 @@ setup-venv: ## setup a development virutalenv in current directory $(IN_VENV) pip install -r requirements.txt && pip install -r dev-requirements.txt; \ fi -setup-git-hook-lint: ## setup precommit hook for linting project - cp $(BUILD_SCRIPTS_DIR)/pre-commit-lint .git/hooks/pre-commit - -setup-git-hook-lint-and-test: ## setup precommit hook for linting and testing project - cp $(BUILD_SCRIPTS_DIR)/pre-commit-lint-and-test .git/hooks/pre-commit +setup-pre-commit: ## install pre-commit hook (uses .pre-commit-config.yaml if present, else the .sample) + @if [ -f .pre-commit-config.yaml ]; then \ + $(IN_VENV) pre-commit install; \ + else \ + $(IN_VENV) pre-commit install --config .pre-commit-config.yaml.sample; \ + fi lint: ## check style with ruff, flake8, black, and mypy uv run --group lint isort --check --diff . @@ -114,6 +116,26 @@ commit-version: ## Update version and history, commit. new-version: ## Mint a new version $(IN_VENV) DEV_RELEASE=$(DEV_RELEASE) python $(BUILD_SCRIPTS_DIR)/new_version.py $(SOURCE_DIR) $(VERSION) +check-release: ## pre-release checklist: venv, clean tree, history entries, lint, lint-docs + @echo "==> checking $(VENV) exists" + @test -d $(VENV) || \ + (echo "ERROR: $(VENV) does not exist; run 'make setup-venv'"; exit 1) + @echo "==> checking working tree is clean" + @test -z "$$(git status --porcelain)" || \ + (echo "ERROR: working tree has uncommitted changes or untracked files"; git status --short; exit 1) + @echo "==> checking UPSTREAM remote '$(UPSTREAM)' points to $(UPSTREAM_REPO)" + @git remote get-url $(UPSTREAM) 2>/dev/null | grep -q "$(UPSTREAM_REPO)" || \ + (echo "ERROR: remote '$(UPSTREAM)' missing or not pointing to $(UPSTREAM_REPO)."; \ + echo " Fix: git remote add $(UPSTREAM) git@github.com:$(UPSTREAM_REPO).git"; \ + echo " Or fork: make check-release UPSTREAM= UPSTREAM_REPO=/"; exit 1) + @echo "==> checking HISTORY.rst has entries under current .devN header" + @$(IN_VENV) python $(BUILD_SCRIPTS_DIR)/check_history_entries.py + @echo "==> make clean && make lint && make lint-docs" + @$(MAKE) clean + @$(MAKE) lint + @$(MAKE) lint-docs + @echo "==> check-release OK" + release-local: commit-version new-version push-release: ## Push a tagged release to github diff --git a/docs/developing.rst b/docs/developing.rst index 72649f29..68bd47aa 100644 --- a/docs/developing.rst +++ b/docs/developing.rst @@ -1,41 +1,91 @@ -================== -Release Checklist -================== +========== +Developing +========== -This page describes the process of releasing new versions of gxformat2. +This page describes how to set up a development environment for gxformat2 +and how to cut a release. -This release checklist is based on the `Pocoo Release Management Workflow -`_. +Development Setup +================= -This assumes ``~/.pypirc`` file exists with the following fields (variations) -are fine. +Create a virtualenv with dev dependencies installed:: + + make setup-venv + +This runs ``uv sync``. The ``test``, ``lint``, ``mypy``, and ``docs`` +dependency groups are marked as ``default-groups`` in ``pyproject.toml`` +so they install automatically. + +Optional pre-commit hooks are configured via `pre-commit +`_. A sample configuration ships as +``.pre-commit-config.yaml.sample``. Install the hook with:: + + make setup-pre-commit + +This uses ``.pre-commit-config.yaml`` if you have one (copy the sample and +customize), and otherwise falls back to the committed +``.pre-commit-config.yaml.sample``. + +Running Tests and Linters +========================= :: - [pypi] - username: - password: - - [test] - repository:https://testpypi.python.org/pypi - username: - password: - - -* Review ``git status`` for missing files. -* Verify the latest Travis CI builds pass. -* ``make open-docs`` and review changelog. -* Ensure the target release is set correctly in ``galaxy/__init__.py`` ( - ``version`` will be a ``devN`` variant of target release). -* ``make clean && make lint && make test`` -* ``make release`` - - This process will push packages to test PyPI, allow review, publish - to production PyPI, tag the git repository, push the tag upstream, - and modify the Homebrew recipe. If changes are needed, such as manual - changes to the homebrew recipe, this can be broken down into steps - such as: - - * ``make release-local`` - * ``make push-release`` - * ``make release-brew`` + make test # pytest + make lint # ruff, flake8, black, mypy, schema build dry-run + make lint-docs # rebuild Sphinx HTML and check for warnings + +Set ``GXFORMAT2_TEST_IWC_DIRECTORY`` to a local clone of the IWC repository +to enable integration tests that exercise real-world workflows. + +Releases +======== + +gxformat2 publishes to PyPI via GitHub Actions trusted publishing — no +``~/.pypirc`` or PyPI credentials are needed on the release machine. Pushing +a version tag to ``galaxyproject/gxformat2`` triggers the publish workflow. + +Pre-release checklist +--------------------- + +Run:: + + make check-release + +This verifies: + +* ``.venv`` exists (or ``uv`` is available). +* Working tree is clean (no uncommitted or untracked files). +* The ``UPSTREAM`` git remote exists and points to ``UPSTREAM_REPO`` + (defaults: ``galaxyproject`` / ``galaxyproject/gxformat2``). +* ``HISTORY.rst`` has entries under the current ``.devN`` section. +* ``make clean``, ``make lint``, and ``make lint-docs`` all pass. + +Fork maintainers can override the defaults:: + + make check-release UPSTREAM=myfork UPSTREAM_REPO=me/gxformat2 + +Cutting the release +------------------- + +1. Populate the changelog from merged PRs:: + + make add-history + + Review the diff to ``HISTORY.rst``, adjust as needed, and commit it + (``make check-release`` in the next step requires a clean working tree). + +2. Confirm the target version in ``gxformat2/__init__.py`` (``__version__`` + is the ``.devN`` variant of the release you intend to cut). + +3. ``make check-release``. + +4. ``make release`` — this runs ``commit-version``, ``new-version``, and + ``push-release``, which commits the version bump, tags the release, + bumps ``__version__`` to the next ``.dev0``, and pushes ``main`` plus + the tag to ``UPSTREAM``. The pushed tag triggers the PyPI publish + workflow on GitHub. + +If you need finer control the release can be broken into its pieces: +``make release-local`` (commit, tag, next-dev) followed by +``make push-release`` (push main and tags). diff --git a/pyproject.toml b/pyproject.toml index 3e53d302..9059c2a6 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -79,6 +79,9 @@ docs = [ "sphinxcontrib-mermaid", ] +[tool.uv] +default-groups = ["test", "lint", "mypy", "docs"] + [tool.ruff] exclude = [ "gxformat2/schema/v19_09.py", diff --git a/scripts/check_history_entries.py b/scripts/check_history_entries.py new file mode 100644 index 00000000..e95c43c2 --- /dev/null +++ b/scripts/check_history_entries.py @@ -0,0 +1,26 @@ +"""Fail if HISTORY.rst's current .devN section has no bullet entries.""" + +import re +import sys +from pathlib import Path + +HISTORY = Path(__file__).resolve().parent.parent / "HISTORY.rst" + + +def main() -> int: + text = HISTORY.read_text() + m = re.search(r"-{3,}\n(\d+\.\d+\.\d+\.dev\d+)\n-{3,}\n(.*?)(?=\n-{3,}\n|\Z)", text, re.DOTALL) + if not m: + print(f"ERROR: no .devN section found in {HISTORY}", file=sys.stderr) + return 1 + version, body = m.group(1), m.group(2) + bullets = [line for line in body.splitlines() if line.lstrip().startswith("*")] + if not bullets: + print(f"ERROR: {version} section in HISTORY.rst has no entries; run 'make add-history'", file=sys.stderr) + return 1 + print(f"OK: {version} has {len(bullets)} entr{'y' if len(bullets) == 1 else 'ies'}") + return 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/scripts/pre-commit-lint b/scripts/pre-commit-lint deleted file mode 100755 index 53769a25..00000000 --- a/scripts/pre-commit-lint +++ /dev/null @@ -1,3 +0,0 @@ -#!/bin/sh - -make lint diff --git a/scripts/pre-commit-lint-and-test b/scripts/pre-commit-lint-and-test deleted file mode 100644 index db1a6071..00000000 --- a/scripts/pre-commit-lint-and-test +++ /dev/null @@ -1,4 +0,0 @@ -#!/bin/sh - -make lint -make quick-test