From c20d09e74807cc73aa3c5e23eb65b554e7be3908 Mon Sep 17 00:00:00 2001 From: Chandrasekharan M Date: Tue, 8 Sep 2026 13:29:29 +0530 Subject: [PATCH 1/2] docs: add a skill for bumping the client pins and their specs A client bump here is three coupled edits -- the exact pin, the vendored spec re-copied from the commit that client was generated from, and the provenance sha -- and the coupling is only discoverable by tripping test_specs.py or test_contract.py and working backwards. Records the order as a repo-local skill, including the part that is easy to get wrong: derived_flags.json is refreshed deliberately after reading what moved, a flag missing from it being a flag the CLI has stopped offering. Also pins the release shape: publish before tag, `none` for the version already committed, and pre_release for the rc flow. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01CSm32dsxD56PkKENgav3Mn --- .claude/skills/bump-client-pins/SKILL.md | 98 ++++++++++++++++++++++++ 1 file changed, 98 insertions(+) create mode 100644 .claude/skills/bump-client-pins/SKILL.md diff --git a/.claude/skills/bump-client-pins/SKILL.md b/.claude/skills/bump-client-pins/SKILL.md new file mode 100644 index 0000000..afe45ca --- /dev/null +++ b/.claude/skills/bump-client-pins/SKILL.md @@ -0,0 +1,98 @@ +--- +name: bump-client-pins +description: Bump the exact `unstract-client` / `llmwhisperer-client` pins, re-sync the vendored specs to match, and cut a CLI release. Use whenever a new version of either client is released, when `tests/test_specs.py` or `tests/test_contract.py` fails, when the CLI is missing a flag for an endpoint the API already has, or when someone asks to "bump the client", "update the pins", "refresh the specs", or "release the CLI". Reach for this even when the request sounds like a plain dependency bump — the pins, the vendored specs and `provenance.json` have to move together or the CLI derives flags the pinned client cannot carry. +--- + +# Bumping the client pins + +The CLI derives its flags and help text from two published clients and from +vendored copies of the specs those clients were generated from. That makes a pin +bump three coupled edits, not one: the pin, the spec, and the provenance record. +Move one without the others and the tests say so — which is the point of them. + +## The pieces + +| Thing | Where | +|---|---| +| Exact pins | `pyproject.toml`, `[project].dependencies` | +| Vendored specs | `src/unstract_cli/specs/{docstudio,llmwhisperer}.json` | +| Provenance | `src/unstract_cli/specs/provenance.json` (upstream repo, commit, sha256) | +| Coherence tests | `tests/test_specs.py`, `tests/test_contract.py`, `tests/derived_flags.json` | +| Release | `.github/workflows/release.yml`, `workflow_dispatch` | + +`src/unstract_cli/specs/README.md` explains the vendoring rule in place; read it +if any of the below is unclear. + +## The sequence + +1. **Move the pins** in `pyproject.toml` to the released versions you are + upgrading to. They are exact (`==`) on purpose: the CLI's published surface is + derived from these clients, so a client that moves reshapes the CLI. + +2. **Relock:** `uv lock` then `uv sync --extra dev --python 3.12`. CI installs + from `uv.lock`, not from a fresh resolve, so a lockfile left behind means the + gate tests a dependency set nobody ships. + +3. **Re-sync each vendored spec from the commit the pinned client was generated + from.** The chain is: the client repo's release tag → its `tools/gen_sdk.sh`, + which records the upstream service repo, path and revision the spec was copied + from → the spec file committed in that client at that tag. Copy that file here + byte-for-byte. Copying from anywhere else — upstream `main`, a newer service + commit — is what `tests/test_contract.py` guards: a spec parameter the pinned + client has no argument for cannot become a flag. + +4. **Update `provenance.json`** for each spec you moved: the upstream `commit` + the client recorded, and the `sha256` of the file you just wrote + (`sha256sum src/unstract_cli/specs/`). This is the record that lets the + next person tell a current copy from a stale one. + +5. **Run the tests:** `uv run pytest -q`. + + - `test_specs.py` fails if a vendored file stops matching its pinned sha256, + or if a spec has no provenance entry. It is the cheap check that steps 3 and + 4 actually agree. + - `test_contract.py` fails if a spec parameter the pinned client cannot accept + would have become a flag, and separately if the derived flags stop matching + `tests/derived_flags.json`. + +6. **If `derived_flags.json` fails, read the difference before refreshing it.** + The failure names the flags that moved. A flag missing from the new set is a + flag the CLI has stopped offering; a narrowed choice or changed type is a value + the CLI used to take and now rejects. Once you have decided the change is + intended, refresh it deliberately: + + ```bash + UNSTRACT_CLI_REFRESH_FLAG_SNAPSHOT=1 uv run pytest -q tests/test_contract.py + ``` + + and commit the snapshot in the same PR, so the diff shows what the CLI's + surface gained or lost. + +7. **Lint:** `uv run ruff check . && uv run ruff format --check .` — the release + run repeats exactly this, so a failure here is a failure there. + +## Versioning and release + +Choose the bump by what changed for CLI users: **minor** for new or changed +flags and commands, **patch** for fixes that leave the surface identical. + +Do not bump `__version__` in `src/unstract_cli/__init__.py` in your PR. The +in-repo value names the last released version; `release.yml` reads it, applies +the bump chosen at dispatch and commits the result itself. + +Release by dispatching **Release Tag and Publish Package** on `main`: + +- `version_bump: none` publishes the version already in the repo — what the + first release of a version needs. +- `pre_release: true` publishes `rcN` and deliberately leaves the + committed version alone, counting N up from the rc tags already published for + that target. Dispatch again with it off to promote the same version to stable. +- It publishes to PyPI **before** it tags and releases, because publishing is the + only step that cannot be undone: a failure before it leaves nothing to + unpublish, and one after it is retried by hand against a live artifact. + +## Upstream + +If a client pin is missing an endpoint the service already offers, the fix is in +that client, not here — see the `spec-upgrade` skill in `unstract-python-client` +and `llm-whisperer-python-client`. Bump the pin here once it is released. From 8a5dfdabb1c9129699a6f0bbed867a6f30d1e025 Mon Sep 17 00:00:00 2001 From: Chandrasekharan M Date: Tue, 8 Sep 2026 15:36:19 +0530 Subject: [PATCH 2/2] docs: check all four provenance fields, and hold version_bump across an rc promotion Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01CSm32dsxD56PkKENgav3Mn --- .claude/skills/bump-client-pins/SKILL.md | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/.claude/skills/bump-client-pins/SKILL.md b/.claude/skills/bump-client-pins/SKILL.md index afe45ca..97a32a6 100644 --- a/.claude/skills/bump-client-pins/SKILL.md +++ b/.claude/skills/bump-client-pins/SKILL.md @@ -41,10 +41,13 @@ if any of the below is unclear. commit — is what `tests/test_contract.py` guards: a spec parameter the pinned client has no argument for cannot become a flag. -4. **Update `provenance.json`** for each spec you moved: the upstream `commit` - the client recorded, and the `sha256` of the file you just wrote - (`sha256sum src/unstract_cli/specs/`). This is the record that lets the - next person tell a current copy from a stale one. +4. **Update `provenance.json`** for each spec you moved. Check all four fields + against what that client's `tools/gen_sdk.sh` records — `repo` and `path` as + well as `commit` — because an upstream that moved its spec file leaves those + two stale and the tests cannot see it: they check the `sha256` and the entry + names, nothing about where the file came from. The `sha256` is of the file + you just wrote (`sha256sum src/unstract_cli/specs/`). This record is + what lets the next person tell a current copy from a stale one. 5. **Run the tests:** `uv run pytest -q`. @@ -86,7 +89,10 @@ Release by dispatching **Release Tag and Publish Package** on `main`: first release of a version needs. - `pre_release: true` publishes `rcN` and deliberately leaves the committed version alone, counting N up from the rc tags already published for - that target. Dispatch again with it off to promote the same version to stable. + that target. To promote to stable, dispatch again with it off **and the same + `version_bump`**: the workflow recomputes the target from that input every + time, so a different bump publishes a different version than the one the rc + tested. - It publishes to PyPI **before** it tags and releases, because publishing is the only step that cannot be undone: a failure before it leaves nothing to unpublish, and one after it is retried by hand against a live artifact.