Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
5d8d1ce
fix: serialize fee payloads for studio estimates (#88)
MuncleUscles Jun 8, 2026
61dd2c0
fix: include execution budget floor in fee estimates (#89)
MuncleUscles Jun 9, 2026
bc04db8
feat(fees)!: estimation correctness, wait-for-decided semantics, v0.6…
MuncleUscles Jun 10, 2026
dd25ef7
ci: keep main forwarded to active dev branch (#92)
MuncleUscles Jun 10, 2026
ab386b4
docs: add branching guide (#94)
MuncleUscles Jun 11, 2026
6bfa86a
fix(fees)!: move wildcard callKey sentinel to keccak256(empty), deplo…
MuncleUscles Jun 11, 2026
ec7cab9
fix: bump package version to v0.19 (#95)
MuncleUscles Jun 16, 2026
30c262c
feat(vesting): vesting staking actions + ABI (#97)
MuncleUscles Jul 7, 2026
ef36623
feat(calldata)!: migrate method-call key "method" -> "" (v0.6 genvm A…
MuncleUscles Jul 23, 2026
3106bc8
fix: resolve v0.19-dev bug-hunt findings (#99)
MuncleUscles Jul 26, 2026
3583471
fix: include pending transactions in default nonce lookup (#100)
MuncleUscles Jul 26, 2026
2a689c0
feat(staking): support the two-step operator rotation (#104)
kirilaa Aug 18, 2026
39d6a37
chore(genvm): update runner hashes and rename accepted to decided ⬆️ …
kp2pml30 Aug 24, 2026
85a4782
fix: align Python SDK consumers with the resolution-kernel train (#109)
kirilaa Aug 31, 2026
b88e492
fix(fees): add exact quote and appeal parity (#110)
MuncleUscles Sep 2, 2026
34bff46
feat: prepare v0.19 Studio preview and RC release (#113)
MuncleUscles Sep 3, 2026
31179d8
chore(release): 0.19.0-rc.1
Sep 3, 2026
4556920
fix: align localnet chain id with Studio (#115)
MuncleUscles Sep 3, 2026
4ea0055
chore(release): 0.19.0-rc.2
Sep 3, 2026
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
107 changes: 107 additions & 0 deletions .github/scripts/validate-branch-policy.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
#!/usr/bin/env bash
set -euo pipefail

failed=0

error() {
echo "::error::$*"
failed=1
}

warning() {
echo "::warning::$*"
}

active_branch_file="support/ci/ACTIVE_DEV_BRANCH"
if [[ ! -f "${active_branch_file}" ]]; then
error "${active_branch_file} is required."
active_branch=""
else
active_branch="$(tr -d '[:space:]' < "${active_branch_file}")"
fi

if [[ -z "${active_branch}" ]]; then
error "${active_branch_file} must not be empty."
elif [[ "${active_branch}" == "main" ]]; then
error "${active_branch_file} must point to a dev branch, not main."
elif [[ "${active_branch}" != *-dev ]]; then
warning "${active_branch_file} should normally point to a -dev branch; got ${active_branch}."
fi

release_branch="${active_branch%-dev}"
default_branch="${GITHUB_DEFAULT_BRANCH:-}"
event_name="${GITHUB_EVENT_NAME:-local}"
base_ref="${GITHUB_BASE_REF:-}"
head_ref="${GITHUB_HEAD_REF:-}"
ref_name="${GITHUB_REF_NAME:-}"
actor="${GITHUB_ACTOR:-}"

if [[ -n "${default_branch}" && "${default_branch}" != "main" ]]; then
warning "Repository default branch should be main after branch-policy rollout; currently ${default_branch}."
fi

if [[ -n "${base_ref}" && "${base_ref}" == "main" ]]; then
warning "PR targets main; retarget-main-prs should move it to ${active_branch}."
fi

if [[ -n "${base_ref}" && -n "${active_branch}" ]]; then
if [[ "${base_ref}" == "${release_branch}" && "${head_ref}" != "${active_branch}" && "${ALLOW_DIRECT_RELEASE_PR:-false}" != "true" ]]; then
error "PRs into ${release_branch} must come from ${active_branch}. Merge feature work into ${active_branch}, then promote ${active_branch} -> ${release_branch}."
fi
fi

if [[ "${event_name}" == "push" && "${ref_name}" == "main" ]]; then
case "${actor}" in
github-actions[bot]|ci-core-e2e-runner[bot])
;;
*)
error "main should only move by automation from ${active_branch}; direct push actor was ${actor:-unknown}."
;;
esac
fi

if [[ ! -f ".github/workflows/fast-forward-main.yaml" ]]; then
error ".github/workflows/fast-forward-main.yaml is required."
fi

if [[ ! -f ".github/workflows/retarget-main-prs.yaml" ]]; then
error ".github/workflows/retarget-main-prs.yaml is required."
fi

if [[ -f ".github/workflows/release-from-main.yml" ]]; then
error ".github/workflows/release-from-main.yml is forbidden. Releases must be tag/version-branch driven."
fi

if [[ -f "release.config.js" ]]; then
error "release.config.js is forbidden in versioned tooling branches; semantic-release-on-main must not be restored."
fi

if [[ -f ".github/workflows/release-from-tag.yml" ]]; then
if ! grep -Fq 'v*.*.*' .github/workflows/release-from-tag.yml; then
error "release-from-tag.yml must trigger only from version tags matching v*.*.*."
fi
if ! grep -Fq 'refs/remotes/origin/${version_branch}' .github/workflows/release-from-tag.yml || \
! grep -Fq 'tag_commit' .github/workflows/release-from-tag.yml || \
! grep -Fq 'branch_head' .github/workflows/release-from-tag.yml; then
error "release-from-tag.yml must verify the tag commit is the current matching version branch head."
fi
fi

if [[ -f ".github/workflows/manual-docker-release.yml" ]]; then
if ! grep -Fq 'expected_branch=' .github/workflows/manual-docker-release.yml; then
error "manual-docker-release.yml must derive and enforce the expected version branch from the tag."
fi
if ! grep -Fq './.github/workflows/release-from-tag.yml' .github/workflows/manual-docker-release.yml; then
error "manual-docker-release.yml must delegate image promotion to release-from-tag.yml."
fi
fi

if [[ "${failed}" -ne 0 ]]; then
exit 1
fi

if [[ -n "${base_ref}" ]]; then
echo "Branch policy ok for PR ${head_ref} -> ${base_ref}; active dev branch is ${active_branch}."
else
echo "Branch policy ok for ${event_name} on ${ref_name:-detached ref}; active dev branch is ${active_branch}."
fi
24 changes: 24 additions & 0 deletions .github/workflows/branch-policy.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
name: Branch Policy

on:
pull_request:
types: [opened, synchronize, reopened, edited, ready_for_review]
push:
branches:
- "**"
workflow_dispatch:

permissions:
contents: read

jobs:
branch-policy:
name: Validate branch policy
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4

- name: Validate branch policy
env:
GITHUB_DEFAULT_BRANCH: ${{ github.event.repository.default_branch }}
run: ./.github/scripts/validate-branch-policy.sh
57 changes: 57 additions & 0 deletions .github/workflows/fast-forward-main.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
name: Fast-forward main

# main is the static/default branch for GitHub UX and tools that assume a
# stable default branch. It is not the integration target. On each push to the
# configured active dev branch, fast-forward main to that commit.

on:
push:
branches: ["**"]
workflow_dispatch:

permissions:
contents: write

concurrency:
group: fast-forward-main-${{ github.repository }}
cancel-in-progress: false

defaults:
run:
shell: bash

jobs:
fast-forward:
if: github.ref_type == 'branch'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0

- name: Fast-forward main to active dev branch
run: |
set -euo pipefail

active_branch="$(tr -d '[:space:]' < support/ci/ACTIVE_DEV_BRANCH)"
if [[ -z "${active_branch}" || "${active_branch}" == "main" ]]; then
echo "::error::support/ci/ACTIVE_DEV_BRANCH must name a non-main dev branch"
exit 1
fi

if [[ "${GITHUB_REF_NAME}" != "${active_branch}" ]]; then
echo "Push was to ${GITHUB_REF_NAME}; active dev branch is ${active_branch}. Nothing to do."
exit 0
fi

if git ls-remote --exit-code --heads origin main >/dev/null 2>&1; then
git fetch origin main
if ! git merge-base --is-ancestor origin/main HEAD; then
echo "::error::main has diverged from ${active_branch}; refusing non-fast-forward update"
exit 1
fi
else
echo "main does not exist yet; creating it at ${GITHUB_SHA}."
fi

git push origin "HEAD:refs/heads/main"
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[@]}"
53 changes: 53 additions & 0 deletions .github/workflows/retarget-main-prs.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
name: Retarget main PRs

# main is a static/default alias of the active dev branch. Contributions should
# target the active dev branch directly; PRs opened against main are retargeted
# automatically so required checks and release-train rules run in the right
# branch context.
#
# pull_request_target is used for the write-scoped token. This workflow never
# checks out or executes PR head code; it reads only trusted base-branch files.

on:
pull_request_target:
types: [opened, reopened, synchronize, edited, ready_for_review]

permissions:
contents: read
pull-requests: write
issues: write

defaults:
run:
shell: bash

jobs:
retarget:
if: github.event.pull_request.base.ref == 'main'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
ref: ${{ github.event.pull_request.base.ref }}

- name: Retarget PR to active dev branch
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
PR_NUMBER: ${{ github.event.pull_request.number }}
run: |
set -euo pipefail

active_branch="$(tr -d '[:space:]' < support/ci/ACTIVE_DEV_BRANCH)"
if [[ -z "${active_branch}" || "${active_branch}" == "main" ]]; then
echo "::error::support/ci/ACTIVE_DEV_BRANCH must name a non-main dev branch"
exit 1
fi

gh pr edit "${PR_NUMBER}" --repo "${GITHUB_REPOSITORY}" --base "${active_branch}"

gh pr comment "${PR_NUMBER}" --repo "${GITHUB_REPOSITORY}" --body "$(cat <<EOF
This PR targeted \`main\`, which is only the default/static branch.

I retargeted it to \`${active_branch}\`, the active development branch. Pushes to \`${active_branch}\` automatically fast-forward \`main\`.
EOF
)"
4 changes: 4 additions & 0 deletions .github/workflows/smoke.yml
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,10 @@ on:
jobs:
smoke:
name: Testnet Smoke Tests
# The v0.19 line intentionally targets the resolution-kernel train and
# does not support the currently deployed pre-train testnets. Its live
# coverage comes from the cross-repository E2E stack until they upgrade.
if: github.event_name != 'pull_request' || github.base_ref != 'v0.19-dev'
runs-on: ubuntu-latest
# Bradbury/Asimov RPC reads occasionally stall for 2-3 minutes on
# single calls (testnet capacity); with 17 testnet-flagged tests
Expand Down
Loading
Loading