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
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,10 @@ build/
*.egg-info/
*.egg

# Build stamp generated by scripts/build_info.py before a build (release
# standard); it records the commit and must never be committed.
src/monarch_cli/_build_info.json

# Testing
.pytest_cache/
.coverage
Expand Down
23 changes: 23 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,29 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

### Added

- `monarch update` replaces the installed copy using the method it was
installed with (source checkout, `uv tool`, `pipx`, `pip`, or `mise`).
`monarch update --check` reports whether an update is available without
changing anything (exit 0 up to date, 1 update available, 2 unknown). A
source checkout pulls `origin/main` and reinstalls; it refuses a dirty tree
or a branch other than `main`.
- `--version` and `-v` now include the commit and date: `monarch 0.2.1
(1a2b3c4 2026-06-01)`, with `-dirty` for a build from an uncommitted tree.
Built wheels and sdists carry a `_build_info.json` stamp so an installed copy
prints the same line as the checkout it came from.

### Changed

- Release tooling follows the release standard: `make bump VERSION=X.Y.Z`
updates the version metadata and changelog and commits `chore: release
vX.Y.Z`; `scripts/check-changelog.sh` refuses an empty Unreleased section;
`scripts/release.sh` requires a clean `main` at `HEAD == origin/main` and
refuses to tag when the build stamp does not match `HEAD`.
- Smoke-test/CI builds (`make smoke-install`) stamp the build and verify the
exact `--version` line of both console entry points.

## [0.2.1] - 2026-09-21

### Added
Expand Down
9 changes: 8 additions & 1 deletion Makefile
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
.PHONY: setup verify format format-check lint lint-fix typecheck test test-live test-live-mutation smoke-install release release-dry prepublish update-capabilities-fixture
.PHONY: setup verify format format-check lint lint-fix typecheck test test-live test-live-mutation smoke-install release release-dry prepublish bump update-capabilities-fixture

# Python version used by the isolated wheel smoke-install environment.
SMOKE_PYTHON ?= 3.13
Expand Down Expand Up @@ -46,6 +46,13 @@ test-live:
test-live-mutation:
MONARCH_LIVE_MUTATION_TESTS=1 uv run pytest tests/live/ -m live_mutation -v

# Bump the version (src/monarch_cli/__init__.py, pyproject.toml, uv.lock), move
# the Unreleased changelog entries into a dated section, and commit
# "chore: release vX.Y.Z". Refuses a dirty tree, a branch other than main, an
# existing tag, or an empty Unreleased section. See RELEASING.md.
bump:
./scripts/bump.sh "$(VERSION)"

# Create GitHub release with tag and changelog
release:
./scripts/release.sh
Expand Down
40 changes: 32 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,32 +25,56 @@ net worth.
- Side-effect-free `monarch capabilities` manifest for agent/integration
discovery of commands, options, safety policy, and contract versions

## Installation
## Install

### pip
Requires Python 3.12 or newer.

### pip, uv, or pipx

```bash
pip install monarch-cli
uv tool install monarch-cli # recommended: isolated, on your PATH
pipx install monarch-cli
```

### uv (recommended)
Run it without installing:

```bash
uv tool install monarch-cli
uvx monarch-cli --help
```

### pipx
### From source

```bash
pipx install monarch-cli
git clone https://github.com/crcatala/monarch-cli.git
cd monarch-cli
uv tool install --editable . # puts `monarch` on PATH, running this clone's code
monarch --version # monarch X.Y.Z (sha7 YYYY-MM-DD): the commit you are on
```

Verify the installation:
Code changes take effect on the next run; reinstall (`uv tool install
--editable . --reinstall`) after dependencies change. For a full development
environment instead, see [Development](#development) (`make setup` and
`uv run monarch`).

Verify the install with:

```bash
monarch --version
monarch --help
```

### Updating

```bash
monarch update --check # exit 0: up to date, 1: update available, 2: unknown
monarch update # updates the way it was installed
```

A source checkout updates with `git pull --ff-only` plus a reinstall, and
refuses a dirty tree or a branch other than `main`. Package-manager installs
use the matching `uv tool`, `pipx`, `pip`, or `mise` upgrade command.

## Upgrading

Version-specific migration guides, including breaking-change notes, are in
Expand Down Expand Up @@ -344,7 +368,7 @@ make verify

See [CONTRIBUTING.md](CONTRIBUTING.md) for development workflow and
[docs/testing.md](docs/testing.md) for live-test safeguards. Release
instructions are in [docs/RELEASING.md](docs/RELEASING.md).
instructions are in [RELEASING.md](RELEASING.md).

## Contributing

Expand Down
178 changes: 178 additions & 0 deletions RELEASING.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,178 @@
# Releasing

Releases are cut by hand from a clean `main`. `make release` creates the tag
and the GitHub Release; the PyPI upload is a separate, last step from the
tagged commit, so a failed upload never leaves a half-made tag.

TestPyPI is a rehearsal for the PyPI upload. Publishing versions are immutable
on both indexes.

`scripts/prep-release.sh`, `scripts/check-changelog.sh`, and
`scripts/build_info.py` are canonical release-standard files; do not edit them
in place. Project specifics live in `scripts/release.sh`,
`scripts/smoke_install.sh`, `scripts/bump.sh`, and this file.

## Prerequisites

- Push access to `main`; `gh auth status` succeeds.
- PyPI and TestPyPI API tokens. `twine` reads `~/.pypirc`:

```ini
[testpypi]
username = __token__
password = pypi-YOUR_TEST_PYPI_TOKEN

[pypi]
username = __token__
password = pypi-YOUR_PYPI_TOKEN
```

`chmod 600 ~/.pypirc`. Tokens are secrets: never paste the file into a
shell, issue tracker, or chat; rotate a token immediately if it is exposed.

## Before releasing

1. `git checkout main && git pull --ff-only`
2. `bash scripts/prep-release.sh`. Paste the output into an LLM, review the
result, and put the user-facing entries under `## [Unreleased]` in
`CHANGELOG.md` (Keep a Changelog groups: Added, Changed, Fixed, Removed,
Security). Commit the changelog to `main`.

## Release

```bash
make bump VERSION=X.Y.Z # version metadata + changelog section,
# commit "chore: release vX.Y.Z"
git push # the release commit must be on origin/main before tagging
make prepublish # verify, stamp, build, isolated install, --version check
make release-dry # preview the tag and GitHub Release
make release # tag vX.Y.Z (annotated "Release X.Y.Z"), push,
# GitHub Release with the changelog and dist/ attached
uv run twine upload --repository testpypi dist/* # optional rehearsal
uv run twine upload dist/* # last: publish the tagged build
```

### `make bump VERSION=X.Y.Z`

Writes the version in every place it is declared and commits the release:

| File | Field | Why it matters |
|------|-------|----------------|
| `src/monarch_cli/__init__.py` | `__version__` | Source of the git tag (`v<version>`) |
| `pyproject.toml` | `[project].version` | Version embedded in the built wheel/sdist |
| `uv.lock` | `monarch-cli` package version | Keeps the locked install consistent (`uv lock`) |
| `CHANGELOG.md` | Unreleased → `## [X.Y.Z] - YYYY-MM-DD` | GitHub Release notes |

It refuses a dirty tree, a branch other than `main`, an existing tag, or an
empty Unreleased section (`bash scripts/check-changelog.sh`). `make prepublish`
and `scripts/release.sh` re-check that the source and `pyproject.toml`
versions agree.

No capabilities-fixture regeneration is needed for a version bump: the
canonical capabilities fixture (`tests/fixtures/capabilities.manifest.json`)
stores a fixed placeholder CLI version and is decoupled from `__version__`.
Only after an *intentional* capabilities manifest change (a new command,
option, effect, or schema reference) run `make update-capabilities-fixture` and
review the diff. See `docs/capabilities.md`.

### `make prepublish`

The one command that proves the release is publishable. It runs, in order:

1. `format-check`, `lint`, `typecheck`, `test` (`verify`) — includes the
version-parity and capabilities-fixture contract tests.
2. `smoke-install` — stamps `src/monarch_cli/_build_info.json` from `HEAD`,
**wipes `dist/`**, rebuilds the wheel and sdist with `uv build`, checks that
both artifacts ship the stamp, installs the wheel into a throwaway
environment outside the source tree, and checks that `monarch` and
`monarch-cli` print exactly
`monarch X.Y.Z (sha7 YYYY-MM-DD)` (with `-dirty` only when the stamp's tree
was dirty).
3. `twine check dist/*` — validates distribution metadata.
4. `readme_renderer` — confirms the README renders on PyPI.

> **`dist/` is not a cache.** `make prepublish`, `make smoke-install`, and
> `make release` each wipe and rebuild `dist/`, so the artifacts always match
> the current commit. Do **not** run a bare `uv build` and then upload — it
> *adds* files without removing old ones, leaving a mixed-version `dist/` that
> PyPI rejects.

### `make release-dry` and `make release`

`make release-dry` previews every action and the changelog that will be used.
It warns (rather than refuses) when the branch is not `main`, `HEAD` is not
`origin/main`, or the version's tag already exists, so it can be run on an
unreleased branch. It never creates a tag or a release.

`make release` verifies, in order:

- `__init__.py` and `pyproject.toml` versions match;
- the working tree is clean and the branch is `main`;
- `HEAD` equals `origin/main` (the release commit was pushed);
- the tag `vX.Y.Z` does not already exist;
- a `## [X.Y.Z]` changelog section exists;
- `smoke-install` rebuilt `dist/` and the stamp in
`src/monarch_cli/_build_info.json` is `HEAD`;
- `dist/monarch_cli-X.Y.Z-*.whl` exists.

It then prompts for confirmation, creates and pushes the annotated tag
`vX.Y.Z` (`Release X.Y.Z`), and creates the GitHub Release with the changelog
as notes and `dist/*` as assets. It never publishes to PyPI.

### Publish last

Upload from the same `dist/` built from the tagged commit:

```bash
uv run twine upload dist/*
```

Optionally rehearse on TestPyPI first:

```bash
uv run twine upload --repository testpypi dist/*
python -m venv /tmp/test-monarch
source /tmp/test-monarch/bin/activate
pip install --index-url https://test.pypi.org/simple/ \
--extra-index-url https://pypi.org/simple/ monarch-cli==X.Y.Z
monarch --version # monarch X.Y.Z (sha7 YYYY-MM-DD)
deactivate && rm -rf /tmp/test-monarch
```

TestPyPI does not host the runtime dependencies, so the production index
fallback is required. TestPyPI versions are immutable too: to re-test, bump to
a new version (for example `X.Y.Zrc2`).

## When a step fails

- **Before `git push`**: nothing is on the remote yet. Fix and repeat;
`git reset --hard origin/main` drops a local bump commit.
- **After `git push`, before `make release`**: the bump commit is on `main` but
untagged. Fix the cause and continue from `make prepublish`; do not bump
again.
- **`tag vX.Y.Z already exists`**: either delete the mistaken tag
(`git tag -d vX.Y.Z && git push origin :refs/tags/vX.Y.Z`) or bump to the
next version.
- **`HEAD is not origin/main`**: push the release commit first.
- **`No changelog entry found for version X.Y.Z`**: add the section (or use
`make bump`) and commit it.
- **`dist/ was built from ... not HEAD`**: rebuild with `make prepublish`
before tagging.
- **GitHub Release**: the tag is pushed. Create it with
`gh release create vX.Y.Z dist/* --title vX.Y.Z --notes-file <section>`.
- **Upload `400 Bad Request` / `HTTPError`**: twine hides the reason. Re-run
with `--verbose`; the usual cause is `File already exists`, meaning the
version is already published. Never delete or re-upload a published version;
bump instead. Files already on PyPI can be skipped with
`uv publish --check-url https://pypi.org/simple/` (or just re-run twine).
- **Token not working**: no extra whitespace in `~/.pypirc`, the token starts
with `pypi-`, the username is exactly `__token__`, and the token is not
expired or revoked.

## Verify

```bash
monarch --version # monarch X.Y.Z (sha7 YYYY-MM-DD)
monarch update --check # 0 up to date, 1 update available, 2 unknown
uvx monarch-cli@latest --version
```
Loading
Loading