diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 0213a8d..0cd37c0 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -43,6 +43,9 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v7 + # hatch-vcs reads the version from the tags, so fetch them. + with: + fetch-depth: 0 - uses: astral-sh/setup-uv@v9.0.0 - uses: extractions/setup-just@v4 - run: just build diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml index 44cb43d..d0ef687 100644 --- a/.github/workflows/release.yaml +++ b/.github/workflows/release.yaml @@ -15,13 +15,16 @@ jobs: fetch-depth: 0 - uses: astral-sh/setup-uv@v9.0.0 - uses: extractions/setup-just@v4 - - name: assert tag matches pyproject version + # The tag is the only source of the version, so this only guards against a + # build that did not pick the tag up (shallow clone, dirty tree, two tags on + # one commit). + - name: assert build version matches tag env: TAG: ${{ github.ref_name }} run: | ver="$(just version)" if [ "$TAG" != "v$ver" ]; then - echo "::error::tag '${TAG}' does not match pyproject version 'v${ver}'" + echo "::error::tag '${TAG}' does not match build version 'v${ver}'" exit 1 fi - name: assert tag is on main @@ -34,4 +37,26 @@ jobs: fi - run: just build - run: just smoke + - uses: actions/upload-artifact@v4 + with: + name: dist + path: dist/ - uses: pypa/gh-action-pypi-publish@release/v1 + + # Runs only after PyPI accepts the upload, so a failed publish leaves no release. + github-release: + needs: publish + runs-on: ubuntu-latest + permissions: + contents: write + steps: + - uses: actions/checkout@v7 + - uses: actions/download-artifact@v4 + with: + name: dist + path: dist/ + - name: create GitHub release + env: + GH_TOKEN: ${{ github.token }} + TAG: ${{ github.ref_name }} + run: gh release create "$TAG" dist/* --title "$TAG" --generate-notes diff --git a/.gitignore b/.gitignore index 438ee50..b631e2c 100644 --- a/.gitignore +++ b/.gitignore @@ -9,6 +9,9 @@ dist/ .venv/ venv/ +# Written at build time by hatch-vcs +/src/posit_cli/_version.py + # Tooling .pytest_cache/ .ruff_cache/ diff --git a/CLAUDE.md b/CLAUDE.md index 1a58781..b38b5b4 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -37,4 +37,7 @@ posit-sdk) and the OAuth-release gotcha. `RSConnectClient`), which has no stability contract. `tests/test_rsconnect_contract.py` guards the surface we depend on; pin the rsconnect version and re-verify on bumps. +- The git tag sets the version, through `hatch-vcs`. `pyproject.toml` has no `version` field. + Do not add one. + See `RELEASE.md` for how to cut a release to PyPI. diff --git a/Justfile b/Justfile index 413a557..c9860f2 100644 --- a/Justfile +++ b/Justfile @@ -29,9 +29,9 @@ smoke: install: build uv pip install "$(ls -t dist/*.whl | head -1)" -# Print the current version +# Print the version that a build gets from the current git state version: - @uv version --short + @uvx hatch version # Remove build/test artifacts clean: diff --git a/README.md b/README.md index 6afc859..b92fd2a 100644 --- a/README.md +++ b/README.md @@ -12,21 +12,27 @@ This project is in early-stage development and so far only supports Posit Connec ## Installation -`posit-cli` isn't on PyPI yet. Install the latest version straight from GitHub -with [`uv`](https://docs.astral.sh/uv/): +Install [`posit-cli` from PyPI](https://pypi.org/project/posit-cli/) with +[`uv`](https://docs.astral.sh/uv/): + +```console +uv tool install posit-cli +``` + +To get unreleased changes, install from GitHub instead: ```console uv tool install git+https://github.com/posit-dev/posit-cli.git ``` -If you authenticate to GitHub over SSH, use the `git+ssh://` form instead (uv -requires the `git@` username): +If you authenticate to GitHub over SSH, use the `git+ssh://` form (uv requires +the `git@` username): ```console uv tool install git+ssh://git@github.com/posit-dev/posit-cli.git ``` -Either way this puts the `posit` executable on your `PATH`. To upgrade later, run +Each of these puts the `posit` executable on your `PATH`. To upgrade later, run `uv tool upgrade posit-cli`. See uv's [Git authentication docs](https://docs.astral.sh/uv/concepts/authentication/git/) for tokens and other hosts. diff --git a/RELEASE.md b/RELEASE.md index 668ca10..c7fb52c 100644 --- a/RELEASE.md +++ b/RELEASE.md @@ -1,17 +1,29 @@ # Releasing +The git tag is the only source of the version. `pyproject.toml` has no `version` field — +`hatch-vcs` reads the tag at build time. Do not add one. + Follow these steps to publish a new version to PyPI: -1. Bump `version` in `pyproject.toml`. -2. Run `uv lock` if the bump changed any dependency. CI fails the build if `uv.lock` has drifted. -3. Commit the change and merge it to `main`. -4. Tag the merged commit on `main` as `vX.Y.Z`. The tag must match the `pyproject.toml` version - exactly. -5. Push the tag. This triggers `.github/workflows/release.yaml`. +1. Make sure `main` holds everything you want in the release. +2. Tag the commit on `main` as `vX.Y.Z`. +3. Push the tag. This triggers `.github/workflows/release.yaml`. + +```console +git checkout main && git pull +git tag v0.2.0 +git push origin v0.2.0 +``` + +The release workflow confirms that the build picked the tag up and that the tag is on `main`, +builds the wheel and sdist, smoke-tests the wheel, then publishes to PyPI through GitHub's OIDC +trusted-publisher flow. No PyPI token is stored in this repo. After PyPI accepts the upload, a +second job creates a GitHub release for the tag, with generated notes and the built artifacts +attached. -The release workflow checks the tag against `pyproject.toml` and against `main`, builds the -wheel and sdist, smoke-tests the wheel, then publishes to PyPI through GitHub's OIDC -trusted-publisher flow. No PyPI token is stored in this repo. +Run `just version` to see the version that a build gets from the current git state. A commit +after the last tag gets a development version (for example `0.2.1.dev3+g1a2b3c4`), which PyPI +rejects. Only a clean, tagged commit produces a release version. ## One-time setup for a new repo diff --git a/pyproject.toml b/pyproject.toml index 0583691..b8076fc 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,7 @@ [project] name = "posit-cli" -version = "0.1.0" +# The version comes from the git tag, through hatch-vcs. Never hard-code it here. +dynamic = ["version"] description = "A single, friendly command-line interface for Posit Connect, in the spirit of gh." readme = "README.md" requires-python = ">=3.8" @@ -28,9 +29,17 @@ test = ["pytest>=7"] lint = ["ruff>=0.6"] [build-system] -requires = ["hatchling"] +requires = ["hatchling", "hatch-vcs"] build-backend = "hatchling.build" +[tool.hatch.version] +source = "vcs" + +# Write the resolved version into the package. An sdist has no git metadata, so a +# wheel built from an sdist reads this file instead of asking git. +[tool.hatch.build.hooks.vcs] +version-file = "src/posit_cli/_version.py" + [tool.hatch.build.targets.wheel] packages = ["src/posit_cli"] diff --git a/skills/posit-cli/SKILL.md b/skills/posit-cli/SKILL.md index 3e772bc..b5084b7 100644 --- a/skills/posit-cli/SKILL.md +++ b/skills/posit-cli/SKILL.md @@ -208,15 +208,21 @@ These are rsconnect commands — run `posit connect --help` for detail ## Installing the CLI -`posit-cli` isn't on PyPI yet; install from GitHub with [`uv`](https://docs.astral.sh/uv/): +Install from PyPI with [`uv`](https://docs.astral.sh/uv/): ```console -uv tool install git+https://github.com/posit-dev/posit-cli.git # `posit` onto your PATH -uv tool upgrade posit-cli # later, to update +uv tool install posit-cli # `posit` onto your PATH +uv tool upgrade posit-cli # later, to update ``` -If GitHub is set up for SSH auth, use the `git+ssh://` form instead (uv requires -the `git@` username): +For unreleased changes, install from GitHub instead: + +```console +uv tool install git+https://github.com/posit-dev/posit-cli.git +``` + +If GitHub is set up for SSH auth, use the `git+ssh://` form (uv requires the +`git@` username): ```console uv tool install git+ssh://git@github.com/posit-dev/posit-cli.git diff --git a/uv.lock b/uv.lock index ea7ae22..a541b51 100644 --- a/uv.lock +++ b/uv.lock @@ -729,7 +729,6 @@ wheels = [ [[package]] name = "posit-cli" -version = "0.1.0" source = { editable = "." } dependencies = [ { name = "click", version = "8.1.8", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, @@ -759,7 +758,7 @@ requires-dist = [ { name = "rsconnect-python", specifier = ">=1.30,<2" }, { name = "ruff", marker = "extra == 'lint'", specifier = ">=0.6" }, ] -provides-extras = ["test", "lint"] +provides-extras = ["lint", "test"] [[package]] name = "pycparser"