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
23 changes: 14 additions & 9 deletions .claude/skills/release/SKILL.md
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
---
name: release
description: Cut a release of genlayer-py. Bumps version, updates CHANGELOG, tags, pushes — CI then publishes to PyPI and creates the GitHub Release. Use when a human asks "release v0.18.x" or "ship a new version".
description: Cut a release or release candidate of genlayer-py. Bumps version, updates CHANGELOG, tags, pushes — CI then publishes to PyPI and creates the GitHub Release.
---

# Release skill — genlayer-py

This repo follows a branch-per-major release model. There is no auto-bump on push. A release happens when a human (or you on their behalf) runs `scripts/release.sh` on the target stable branch.
This repo follows a branch-per-release-line model. There is no auto-bump on push. A final release is cut from its stable branch; an RC is cut from the matching `*-dev` integration branch.

## When to use this skill

Expand All @@ -18,26 +18,29 @@ If they ask "publish to PyPI directly" — refuse and point at this flow. The re

## What this repo's release model expects

- Branches are named after the major they ship: `v0.18` (current stable). When `v0.19` opens, the previous `v0.18` stays read-only for back-ports.
- Branches are named after the release line they ship: `v0.18` (stable) and `v0.19-dev` (integration). When `v0.19` becomes stable, the previous `v0.18` stays available for back-ports.
- Tags live within those branches: `v0.18.1`, `v0.18.2`, ...
- **Semver-zero rule**: this package is still on a 0.x line, so the MINOR component is the breaking-change boundary. `0.18 → 0.19` IS a major bump. `scripts/release.sh` refuses both `minor` and `major` keywords without `--allow-major` while we're on 0.x.
- A major (= minor on 0.x) bump means cutting a new branch (`v0.19`) — not tagging on top of the current one.
- `CHANGELOG.md` is updated in the release commit (python-semantic-release with explicit version).
- `publish.yml` fires on the tag push and does the PyPI publish + GitHub Release.
- `CHANGELOG.md` is updated in the release commit by python-semantic-release; an explicit requested version must match the version computed from release history and conventional commits.
- Final tags are cut from `vX.Y`; RC tags such as `v0.19.0-rc.1` are cut from `vX.Y-dev`.
- `publish.yml` verifies that the tag is the current owning branch head, publishes to PyPI, and marks RC GitHub Releases as prereleases.

## Steps

1. **Confirm intent with the user.**
- Which version? If unspecified, ask whether it's patch or explicit.
- If they say "minor" or "major" while we're on 0.x, surface that this means cutting a new branch — confirm before proceeding.

2. **Switch to the target branch + sync.**
2. **Switch to the owning branch + sync.**
```bash
git checkout v0.18
git pull --ff-only origin v0.18
```
If the working tree isn't clean, stop and surface what's there.

For `v0.19.0-rc.1`, use `v0.19-dev` instead. The script rejects final versions on a dev branch and prereleases on a stable branch.

3. **Verify the head is shippable.**
- Latest CI green:
```bash
Expand All @@ -50,9 +53,10 @@ If they ask "publish to PyPI directly" — refuse and point at this flow. The re

4. **Run the release script.**
```bash
scripts/release.sh <X.Y.Z> # or patch
scripts/release.sh <X.Y.Z> # final on vX.Y
scripts/release.sh --allow-major <X.Y.Z-rc.N> # first RC of a new 0.x line
```
It bumps `pyproject.toml`, updates `CHANGELOG.md`, commits `chore(release): vX.Y.Z`, tags `vX.Y.Z`, and pushes both the branch commit and the tag. It will NOT publish to PyPI — CI handles that.
First run the same command with `--dry-run`; it exercises all read-only preflight and version-policy checks. The real command bumps `pyproject.toml`, updates `CHANGELOG.md`, commits `chore(release): X.Y.Z`, tags `vX.Y.Z`, and pushes both the branch commit and the tag. It will NOT publish to PyPI — CI handles that.

5. **Watch the publish workflow.**
```bash
Expand All @@ -68,8 +72,9 @@ If they ask "publish to PyPI directly" — refuse and point at this flow. The re

## Things to refuse

- **Minor or major bump on 0.x without `--allow-major`**. Those are major bumps in semver-zero and belong on a new branch.
- **Minor or major bump on 0.x without `--allow-major`**. Those are major bumps in semver-zero and belong on a new stable/dev branch pair.
- **Releasing from `main`** — `main` is retired.
- **A final tag from `*-dev`, or an RC tag from the stable branch** — the tag must belong to the exact owning branch.
- **Hand-editing `pyproject.toml` to bump the version** — the script keeps pyproject, the CHANGELOG entry, the commit message, and the tag in lockstep.
- **Publishing a tag where `publish.yml` failed** — fix the underlying issue, re-cut the release (delete the bad tag locally and on origin, re-run the script).

Expand Down
31 changes: 22 additions & 9 deletions .github/workflows/publish.yml
Original file line number Diff line number Diff line change
@@ -1,17 +1,19 @@
name: Publish Package to PyPI

# Tag-driven publish. The release is cut by a human (or Claude via the
# release skill) running scripts/release.sh on the target stable branch
# release skill) running scripts/release.sh on the owning version branch
# — that script bumps pyproject.toml, updates CHANGELOG.md, commits,
# tags vX.Y.Z, and pushes both the branch commit and the tag. This
# workflow fires on the tag push, runs tests, sanity-checks the tag
# matches pyproject.toml, builds, and publishes to PyPI. It never
# bumps or tags by itself.
on:
workflow_dispatch:
push:
tags:
- "v*"
- "v*.*.*"

permissions:
contents: write

jobs:
run-tests:
Expand All @@ -33,16 +35,22 @@ jobs:
- name: Install Python
run: uv python install 3.12

- name: Verify tag matches pyproject.toml version
- name: Verify tag, package version, and owning branch
run: |
TAG_VERSION="${GITHUB_REF_NAME#v}"
PKG_VERSION="$(grep -E '^version = ' pyproject.toml | head -1 | sed -E 's/version = "([^"]+)"/\1/')"
if [ "$TAG_VERSION" != "$PKG_VERSION" ]; then
echo "Tag ($TAG_VERSION) and pyproject.toml ($PKG_VERSION) disagree — refusing to publish." >&2
echo "Re-cut the release via scripts/release.sh so the tag and the committed version match." >&2
NORMALIZED_VERSION="$(python scripts/release_version.py verify-tag "$TAG_VERSION" "$PKG_VERSION")"
EXPECTED_BRANCH="$(python scripts/release_version.py branch "$TAG_VERSION")"
git fetch --no-tags origin \
"refs/heads/$EXPECTED_BRANCH:refs/remotes/origin/$EXPECTED_BRANCH"
TAG_COMMIT="$(git rev-parse "${GITHUB_REF_NAME}^{commit}")"
BRANCH_HEAD="$(git rev-parse "origin/$EXPECTED_BRANCH")"
if [ "$TAG_COMMIT" != "$BRANCH_HEAD" ]; then
echo "Tag $GITHUB_REF_NAME points to $TAG_COMMIT, but $EXPECTED_BRANCH is at $BRANCH_HEAD." >&2
echo "Re-cut the release from the current owning branch head via scripts/release.sh." >&2
exit 1
fi
echo "Tag $GITHUB_REF_NAME matches pyproject.toml $PKG_VERSION."
echo "Tag $GITHUB_REF_NAME matches package $NORMALIZED_VERSION and $EXPECTED_BRANCH@$BRANCH_HEAD."

- name: Clean previous builds
run: rm -rf -- dist build *.egg-info
Expand All @@ -63,6 +71,10 @@ jobs:
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
RELEASE_FLAGS=()
if [ "$(python scripts/release_version.py is-prerelease "$GITHUB_REF_NAME")" = "true" ]; then
RELEASE_FLAGS+=(--prerelease)
fi
NOTES="$(awk -v ver="$GITHUB_REF_NAME" '
$0 ~ "^## \\[?" substr(ver, 2) {capture=1; next}
capture && /^## / {exit}
Expand All @@ -73,4 +85,5 @@ jobs:
fi
gh release create "$GITHUB_REF_NAME" \
--title "$GITHUB_REF_NAME" \
--notes "$NOTES"
--notes "$NOTES" \
"${RELEASE_FLAGS[@]}"
4 changes: 2 additions & 2 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ the default/static GitHub branch.

## Releases

Releases are deliberate, not automatic. `scripts/release.sh` bumps the version, updates `CHANGELOG.md`, commits, tags, and pushes; CI takes over from the tag push and publishes to PyPI. See `.claude/skills/release/SKILL.md` for the full flow.
Releases are deliberate, not automatic. `scripts/release.sh` bumps the version, updates `CHANGELOG.md`, commits, tags, and pushes; CI takes over from the tag push and publishes to PyPI. Release candidates are cut from the active `*-dev` branch (for example, `v0.19.0-rc.1` from `v0.19-dev`), while final versions are cut from the matching stable branch. See `.claude/skills/release/SKILL.md` for the full flow.

**Semver-zero rule**: this package is on a 0.x line, so the MINOR component is the breaking-change boundary. `0.18 → 0.19` is a major bump and needs a new branch — the script refuses `minor`/`major` keywords without `--allow-major`.

Expand Down Expand Up @@ -162,7 +162,7 @@ The project uses automated semantic versioning based on commit messages:
| `feat!:`, `fix!:`, or `BREAKING CHANGE:` | **Major** version bump | 1.0.0 → 2.0.0 |
| `docs:`, `style:`, `refactor:`, `test:`, `chore:`, `build:`, `ci:` | **No** version bump | Version stays the same |

**Important**: Never manually edit version numbers in `pyproject.toml` or other files. Releases are cut from the stable branch using the release automation described above.
**Important**: Never manually edit version numbers in `pyproject.toml` or other files. Final releases are cut from the stable branch and release candidates from its matching `*-dev` branch using the release automation described above.

## Logging Configuration

Expand Down
12 changes: 12 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,18 @@ SDK releases follow their corresponding GenLayer protocol release. This
release targets the current resolution-kernel train; use the matching older SDK
release when connecting to an older deployment.

Use the dedicated preview preset for the release-candidate Studio deployment:

```python
from genlayer_py import create_client
from genlayer_py.chains import studio_devnet

client = create_client(chain=studio_devnet)
```

`studio_devnet` targets `https://studio-dev.genlayer.com/api` (chain ID 61997).
The existing `studionet` preset remains pinned to the stable hosted Studio.

Here’s how to initialize the client and connect to the GenLayer Simulator:

### Reading a Transaction
Expand Down
7 changes: 5 additions & 2 deletions docs/BRANCHING.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,8 +41,11 @@ When an integration train is ready, open a promotion PR from the integration
branch to the matching stable branch, for example `v0.19-dev` to `v0.19`.

That promotion PR is the release-readiness gate and must pass required
cross-repo `E2E Tests`. The actual package release is cut from the stable branch
using a version tag after the stable branch is ready.
cross-repo `E2E Tests`. The final package release is cut from the stable branch
using a version tag after the stable branch is ready. A release candidate may
be cut earlier from the matching integration branch; RC tags use
`vX.Y.Z-rc.N`, publish as PyPI prereleases, and never substitute for the
promotion PR's final release gate.

## `main`

Expand Down
20 changes: 6 additions & 14 deletions docs/api-references/api.md
Original file line number Diff line number Diff line change
Expand Up @@ -185,9 +185,8 @@ client.get_contract_schema_for_code(contract_code: AnyStr)

Appeals a consensus transaction to trigger a new round of validation.
Returns the original transaction_id (appeals operate on the same tx).
Deployed Consensus fills missing decision/value inputs from its
authoritative quote. Current Studio requires an explicit value and does
not accept ``expected_decision_id``.
Missing decision/value inputs are filled from the authoritative quote
on both Studio and deployed Consensus.

```python
client.appeal_transaction(transaction_id: HexStr, account: Optional = None, value: Optional = None, expected_decision_id: Optional = None)
Expand Down Expand Up @@ -225,9 +224,8 @@ client.top_up_fees(transaction_id: HexStr, distribution: FeesDistributionInput,

Deposits appeal funding and submits an appeal.

On deployed Consensus, omitted decision/value inputs are resolved from
the authoritative appeal quote. Current Studio requires an explicit
value and does not accept ``expected_decision_id``.
Omitted decision/value inputs are resolved from the authoritative
appeal quote on both Studio and deployed Consensus.

```python
client.top_up_and_submit_appeal(transaction_id: HexStr, distribution: FeesDistributionInput, account: Optional = None, value: Optional = None, expected_decision_id: Optional = None)
Expand All @@ -247,9 +245,7 @@ client.top_up_and_submit_appeal(transaction_id: HexStr, distribution: FeesDistri

### can_appeal

Checks whether the exact active decision can be appealed on a network.

This decision-bound read is not available on current Studio.
Checks whether the exact active decision can be appealed.

```python
client.can_appeal(transaction_id: HexStr, expected_decision_id: Optional = None)
Expand All @@ -266,9 +262,7 @@ client.can_appeal(transaction_id: HexStr, expected_decision_id: Optional = None)

### get_appeal_quote

Returns a network's latest decision id, appeal charges, and deadline.

Current Studio has no decision-bound quote surface.
Returns the latest decision id, appeal charges, and deadline.

```python
client.get_appeal_quote(transaction_id: HexStr)
Expand All @@ -286,8 +280,6 @@ client.get_appeal_quote(transaction_id: HexStr)

Returns the full appeal payment (bond plus induced-work funding).

Current Studio has no decision-bound quote surface.

```python
client.get_appeal_charge(transaction_id: HexStr)
```
Expand Down
12 changes: 12 additions & 0 deletions docs/api-references/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,18 @@ SDK releases follow their corresponding GenLayer protocol release. This
release targets the current resolution-kernel train; use the matching older SDK
release when connecting to an older deployment.

Use the dedicated preview preset for the release-candidate Studio deployment:

```python
from genlayer_py import create_client
from genlayer_py.chains import studio_devnet

client = create_client(chain=studio_devnet)
```

`studio_devnet` targets `https://studio-dev.genlayer.com/api` (chain ID 61997).
The existing `studionet` preset remains pinned to the stable hosted Studio.

Here’s how to initialize the client and connect to the GenLayer Simulator:

### Reading a Transaction
Expand Down
6 changes: 3 additions & 3 deletions genlayer_py/accounts/actions.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
from __future__ import annotations

from typing import TYPE_CHECKING
from genlayer_py.chains import localnet
from genlayer_py.chains.utils import is_studio_chain
from hexbytes import HexBytes
from web3.types import Nonce, BlockIdentifier, ENS
from genlayer_py.exceptions import GenLayerError
Expand All @@ -18,8 +18,8 @@
def fund_account(
self: GenLayerClient, address: Union[Address, ChecksumAddress, ENS], amount: int
) -> HexBytes:
if self.chain.id != localnet.id:
raise GenLayerError("Client is not connected to the localhost")
if not is_studio_chain(self.chain):
raise GenLayerError("Account funding is only supported on Studio networks")
try:
response = self.provider.make_request(
method="sim_fundAccount",
Expand Down
9 changes: 8 additions & 1 deletion genlayer_py/chains/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,5 +2,12 @@
from .testnet_asimov import testnet_asimov
from .testnet_bradbury import testnet_bradbury
from .studionet import studionet
from .studio_devnet import studio_devnet

__all__ = ["localnet", "testnet_asimov", "testnet_bradbury", "studionet"]
__all__ = [
"localnet",
"testnet_asimov",
"testnet_bradbury",
"studionet",
"studio_devnet",
]
7 changes: 2 additions & 5 deletions genlayer_py/chains/actions.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,8 @@
from __future__ import annotations

from genlayer_py.exceptions import GenLayerError
from .localnet import localnet
from .studionet import studionet
from .testnet_asimov import testnet_asimov
from .utils import is_studio_chain

from typing import TYPE_CHECKING

Expand All @@ -23,12 +22,10 @@ def initialize_consensus_smart_contract(
and bool(self.chain.consensus_main_contract.get("address"))
and bool(self.chain.consensus_main_contract.get("abi"))
)
is_local_or_studio_chain = self.chain.id in (localnet.id, studionet.id)

if (
not force_reset
and has_static_consensus_contract
and not is_local_or_studio_chain
and not is_studio_chain(self.chain)
):
return

Expand Down
32 changes: 32 additions & 0 deletions genlayer_py/chains/studio_devnet.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
from genlayer_py.types import GenLayerChain, NativeCurrency

from .studionet import (
CONSENSUS_DATA_CONTRACT,
CONSENSUS_MAIN_CONTRACT,
)


STUDIO_DEVNET_JSON_RPC_URL = "https://studio-dev.genlayer.com/api"
STUDIO_DEVNET_EXPLORER_URL = "https://explorer-studio-dev.genlayer.com"

studio_devnet: GenLayerChain = GenLayerChain(
id=61997,
name="GenLayer Studio Devnet",
rpc_urls={"default": {"http": [STUDIO_DEVNET_JSON_RPC_URL]}},
native_currency=NativeCurrency(name="GEN Token", symbol="GEN", decimals=18),
block_explorers={
"default": {
"name": "GenLayer Explorer",
"url": STUDIO_DEVNET_EXPLORER_URL,
}
},
testnet=True,
consensus_main_contract=dict(CONSENSUS_MAIN_CONTRACT),
consensus_data_contract=dict(CONSENSUS_DATA_CONTRACT),
fee_manager_contract=None,
rounds_storage_contract=None,
appeals_contract=None,
staking_contract=None,
default_number_of_initial_validators=5,
default_consensus_max_rotations=3,
)
12 changes: 12 additions & 0 deletions genlayer_py/chains/utils.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
from __future__ import annotations

from genlayer_py.types import GenLayerChain


STUDIO_CHAIN_IDS = frozenset({61997, 61999})


def is_studio_chain(chain: GenLayerChain) -> bool:
"""Return whether *chain* uses the Studio simulator RPC surface."""

return chain.id in STUDIO_CHAIN_IDS
Loading
Loading