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
3 changes: 1 addition & 2 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -1,10 +1,9 @@
name: CI

on:
push:
branches: [main]
pull_request:
workflow_dispatch:
workflow_call: # the release workflow runs CI before publishing

permissions:
contents: read
Expand Down
93 changes: 81 additions & 12 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
@@ -1,30 +1,99 @@
name: Release

# Rolling releases: every merge to main that changes the package is tested, versioned from its
# commits, tagged, published to PyPI and announced as a GitHub release.

on:
release:
types: [published]
push:
branches: [main]
workflow_dispatch:
inputs:
force:
description: "Release even if no package files changed"
type: boolean
default: false

permissions:
contents: read

concurrency:
group: release
cancel-in-progress: false

jobs:
ci:
uses: ./.github/workflows/ci.yml

version:
needs: ci
runs-on: ubuntu-latest
outputs:
version: ${{ steps.next.outputs.version }}
steps:
- uses: actions/checkout@v5
with:
fetch-depth: 0
persist-credentials: false
- id: next
run: |
version=$(python3 scripts/next_version.py ${{ inputs.force && '--force' || '' }})
echo "version=$version" >> "$GITHUB_OUTPUT"
if [ -n "$version" ]; then echo "Next release: v$version"; else echo "No package changes; nothing to release."; fi

build:
needs: version
if: needs.version.outputs.version != ''
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v5
with:
persist-credentials: false
- uses: astral-sh/setup-uv@v6
- name: Build
env:
SETUPTOOLS_SCM_PRETEND_VERSION: ${{ needs.version.outputs.version }}
run: uv build
- run: uvx twine check --strict dist/*
- uses: actions/upload-artifact@v4
with:
name: dist
path: dist/

publish:
name: Publish to PyPI
needs: [version, build]
runs-on: ubuntu-latest
environment:
name: pypi
url: https://pypi.org/project/opencode-python-sdk/
url: https://pypi.org/project/opencode-python-sdk/${{ needs.version.outputs.version }}/
permissions:
id-token: write # trusted publishing
id-token: write # trusted publishing, no stored token
attestations: write
steps:
- uses: actions/checkout@v5
- uses: astral-sh/setup-uv@v6
- name: Check the tag matches the package version
run: |
version=$(uv run --no-project python -c "import tomllib; print(tomllib.load(open('pyproject.toml','rb'))['project']['version'])")
test "v$version" = "${GITHUB_REF_NAME}" || { echo "Tag ${GITHUB_REF_NAME} does not match version $version"; exit 1; }
- run: uv build
- uses: actions/download-artifact@v4
with:
name: dist
path: dist/
- uses: pypa/gh-action-pypi-publish@release/v1
with:
attestations: true

github-release:
needs: [version, publish]
runs-on: ubuntu-latest
permissions:
contents: write
steps:
- uses: actions/download-artifact@v4
with:
name: dist
path: dist/
- name: Tag and create release
env:
GH_TOKEN: ${{ github.token }}
VERSION: ${{ needs.version.outputs.version }}
run: |
gh release create "v$VERSION" dist/* \
--repo "$GITHUB_REPOSITORY" \
--target "$GITHUB_SHA" \
--title "v$VERSION" \
--generate-notes
19 changes: 5 additions & 14 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,18 +1,9 @@
# Changelog

All notable changes to this project are documented here. The format follows
[Keep a Changelog](https://keepachangelog.com/en/1.1.0/) and the project uses
[semantic versioning](https://semver.org/).
Releases are published automatically on every merge to `main`. Release notes for each version are on the
[GitHub releases page](https://github.com/shrijayan/opencode-python-sdk/releases).

## [0.1.0] - Unreleased
## 0.1.0

First release, generated from OpenCode 2.1.2.

### Added

- `Opencode` and `AsyncOpencode` clients covering all 142 operations of the OpenCode 2.1 server API.
- Typed pydantic models for every schema and 96 event types, with forward-compatible enums and union fallbacks.
- Typed exceptions for every error the server declares, plus status-based base classes.
- Cursor pagination, server-sent event streams, retries with backoff, and per-request options.
- `OpencodeServer` and `AsyncOpencodeServer` to run `opencode serve` as a child process.
- Contract extractor and code generator, with CI checks that generated code is current.
First release, generated from OpenCode 2.1.2: sync and async clients for all 142 operations, typed models and
events, typed errors, pagination, event streams, retries, and the `OpencodeServer` launcher.
22 changes: 20 additions & 2 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -53,10 +53,28 @@ cd ../..
uv run python -m codegen
```

Then review the diff of `api.md`, update `API_VERSION` notes in the README version table, and add a changelog
entry.
Then review the diff of `api.md` and update the README version table if the OpenCode minor version changed.

## Changing generated code

Change the generator in `codegen/`, run `uv run python -m codegen`, and commit both the generator change and the
regenerated files. Never edit generated files directly; CI will reject the change.

## Releases

Releases are automatic. Every merge to `main` that changes `src/`, `spec/` or `pyproject.toml` runs CI, picks the
next version from the merged commit subjects, publishes to PyPI and creates a GitHub release with generated notes.
Docs, tests and CI changes alone do not release.

Use [Conventional Commits](https://www.conventionalcommits.org/) subjects; with rebase merging, every commit lands
on `main` as written:

| Subject | Bump |
| --- | --- |
| `fix: handle empty cursor` | patch |
| `feat: add session.run helper` | minor |
| `feat!: rename Opencode.server` or a `BREAKING CHANGE:` footer | major (minor while below 1.0) |
| anything else (`chore:`, `refactor:`, ...) | patch |

The version comes from git tags (`hatch-vcs`); there is no version number to edit. To release without package
changes, run the Release workflow manually with **force**.
3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -259,7 +259,8 @@ retry; credentials are never logged.
| 0.1.x | 2.1.x |

`opencode.API_VERSION` is the server version the client was generated from. The package follows semantic
versioning; until 1.0, minor versions may contain breaking changes, noted in [CHANGELOG.md](CHANGELOG.md).
versioning and releases automatically on every merge to `main`; until 1.0, minor versions may contain breaking
changes. See the [releases page](https://github.com/shrijayan/opencode-python-sdk/releases).

## How it is built

Expand Down
10 changes: 8 additions & 2 deletions pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[project]
name = "opencode-python-sdk"
version = "0.1.0"
dynamic = ["version"]
description = "Typed Python client for the OpenCode 2.x server API"
readme = "README.md"
license = "MIT"
Expand Down Expand Up @@ -51,9 +51,14 @@ dev = [
]

[build-system]
requires = ["hatchling>=1.27"]
requires = ["hatchling>=1.27", "hatch-vcs>=0.5"]
build-backend = "hatchling.build"

[tool.hatch.version]
# The version comes from the latest `vX.Y.Z` git tag, which the release workflow creates on every merge to main.
source = "vcs"
raw-options = { local_scheme = "no-local-version", fallback_version = "0.0.0" }

[tool.hatch.build.targets.wheel]
packages = ["src/opencode"]

Expand Down Expand Up @@ -112,6 +117,7 @@ ignore = [
"src/opencode/resources/**" = ["TC"]
"src/opencode/_errors.py" = ["TC"]
"codegen/**" = ["T20"]
"scripts/**" = ["T20"]
"examples/**" = ["T20"]
"tests/**" = ["E501", "TC"]

Expand Down
66 changes: 66 additions & 0 deletions scripts/next_version.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
"""Prints the next release version, or nothing when there is nothing to release.

Every merge to main that changes the package is released (rolling releases). The bump follows
Conventional Commits in the commit subjects since the last ``vX.Y.Z`` tag:

- ``feat!:`` / ``fix!:`` or a ``BREAKING CHANGE:`` footer: major (minor while below 1.0)
- ``feat:``: minor
- anything else: patch

Commits that only touch docs, tests or CI do not trigger a release. With no tag yet, the first
release is 0.1.0.

Usage: python scripts/next_version.py [--force]
"""

from __future__ import annotations

import re
import subprocess
import sys

RELEASE_PATHS = ("src/", "pyproject.toml", "spec/")
BREAKING = re.compile(r"^[a-z]+(\([^)]*\))?!:", re.IGNORECASE)
FEATURE = re.compile(r"^feat(\([^)]*\))?:", re.IGNORECASE)


def git(*args: str) -> str:
return subprocess.run(["git", *args], check=True, capture_output=True, text=True).stdout.strip()


def latest_tag() -> str | None:
tags = git("tag", "--list", "v[0-9]*.[0-9]*.[0-9]*", "--sort=-v:refname").splitlines()
return tags[0] if tags else None


def main() -> int:
force = "--force" in sys.argv
tag = latest_tag()
if tag is None:
print("0.1.0")
return 0

revision = f"{tag}..HEAD"
changed = git("diff", "--name-only", revision).splitlines()
if not force and not any(path.startswith(RELEASE_PATHS) for path in changed):
return 0 # nothing user-facing changed

subjects = git("log", "--format=%s", revision).splitlines()
bodies = git("log", "--format=%b", revision)
major, minor, patch = (int(part) for part in tag.removeprefix("v").split("."))

if any(BREAKING.match(subject) for subject in subjects) or "BREAKING CHANGE:" in bodies:
if major == 0:
minor, patch = minor + 1, 0
else:
major, minor, patch = major + 1, 0, 0
elif any(FEATURE.match(subject) for subject in subjects):
minor, patch = minor + 1, 0
else:
patch += 1
print(f"{major}.{minor}.{patch}")
return 0


if __name__ == "__main__":
raise SystemExit(main())
9 changes: 8 additions & 1 deletion src/opencode/_version.py
Original file line number Diff line number Diff line change
@@ -1 +1,8 @@
__version__ = "0.1.0"
from __future__ import annotations

from importlib.metadata import PackageNotFoundError, version

try:
__version__ = version("opencode-python-sdk")
except PackageNotFoundError: # running from a source tree that is not installed
__version__ = "0.0.0"
1 change: 0 additions & 1 deletion uv.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading