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
94 changes: 94 additions & 0 deletions .claude/skills/release/SKILL.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
---
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".
---

# 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.

## When to use this skill

User asks anything like:
- "release v0.18.1"
- "ship a patch"
- "tag the latest fix as a release"

If they ask "publish to PyPI directly" — refuse and point at this flow. The repo doesn't have an unprotected PyPI push path; the tag is the only release entry point.

## 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.
- 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.

## 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.**
```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.

3. **Verify the head is shippable.**
- Latest CI green:
```bash
gh run list --branch v0.18 --commit "$(git rev-parse HEAD)" --limit 1
```
- Inspect commits since the previous tag for surprises:
```bash
git log "$(git describe --tags --abbrev=0)..HEAD" --oneline
```

4. **Run the release script.**
```bash
scripts/release.sh <X.Y.Z> # or patch
```
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.

5. **Watch the publish workflow.**
```bash
gh run watch
```
If `publish.yml` fails (typical: tag/pyproject mismatch, expired `PYPI_API_TOKEN`, build failure), report verbatim and stop. Don't retry blindly.

6. **Confirm on PyPI.**
```bash
pip index versions genlayer-py
```
The latest version should match. Report back with the version and the GitHub Release URL.

## 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.
- **Releasing from `main`** — `main` is retired.
- **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).

## Roll-back

If a release shipped but is broken:

1. **Don't yank from PyPI** unless someone with elevated permissions has assessed the impact — PyPI yank is reversible but signals "skip this" to installers and you'll want a follow-up patch up first.
2. **Ship a follow-up patch** via the same flow (`scripts/release.sh patch`).
3. After the fixed version is live, optionally yank the bad version:
```bash
pip install pkginfo twine
# use pypi.org web UI to yank — there's no CLI in current PyPI flow
```

## Why no auto-bump?

Previously `push: main` triggered `python-semantic-release`, which would auto-bump and tag whenever a `feat:`/`fix:` commit landed. Two failure modes that fix-on-merge can't address:
- Conflated decisions — "merge this PR" silently meant "ship to PyPI".
- Major bumps that slip through (`BREAKING CHANGE` in a PR body produces a 0.X → 0.X+1 bump while on 0.x, which is a major).

Manual + scripted puts a checkpoint between the two without losing the bump-tag automation.
1 change: 1 addition & 0 deletions .github/e2e-track
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
v0.5
131 changes: 46 additions & 85 deletions .github/workflows/publish.yml
Original file line number Diff line number Diff line change
@@ -1,115 +1,76 @@
name: Continuous Delivery

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
# — 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:
branches:
- main
tags:
- "v*"

jobs:
run-tests:
name: Run Tests
uses: ./.github/workflows/tests.yml

release-and-upload:
name: Release and Upload Artifacts
runs-on: ubuntu-latest
publish-to-pypi:
name: Publish Package to PyPI
needs: run-tests
environment: Publish

concurrency:
group: ${{ github.workflow }}-release-${{ github.ref_name }}
cancel-in-progress: false

permissions:
contents: write

outputs:
released: ${{ steps.release.outputs.released }}
commit_sha: ${{ steps.get-commit.outputs.commit_sha }}
runs-on: ubuntu-latest

steps:
- name: Setup | Get CI Bot Token
uses: actions/create-github-app-token@v3
id: ci_bot_token
with:
client-id: ${{ vars.PUBLISH_CI_APP_CLIENT_ID }}
private-key: ${{ secrets.PUBLISH_CI_APP_KEY }}

- name: Setup | Checkout Repository
- name: Checkout tag
uses: actions/checkout@v4
with:
ref: ${{ github.ref_name }}
fetch-depth: 0
token: ${{ steps.ci_bot_token.outputs.token }}

- name: Check | Verify Upstream Unchanged
shell: bash
run: |
chmod +x scripts/verify-upstream.sh
./scripts/verify-upstream.sh ${{ github.sha }}

- name: Setup | Initialize Git User
run: |
git config --global user.email "github-actions[bot]@genlayerlabs.com"
git config --global user.name "github-actions[bot]"

- name: Setup | Install uv
- name: Install uv
uses: astral-sh/setup-uv@v5

- name: Setup | Install Python
- name: Install Python
run: uv python install 3.12

- name: Action | Semantic Version Release
id: release
env:
GH_TOKEN: ${{ steps.ci_bot_token.outputs.token }}
- name: Verify tag matches pyproject.toml version
run: |
chmod +x scripts/semantic-version-release.sh
./scripts/semantic-version-release.sh releaserc.toml

- name: Get | Current Commit SHA
id: get-commit
run: echo "commit_sha=$(git rev-parse HEAD)" >> "$GITHUB_OUTPUT"
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
exit 1
fi
echo "Tag $GITHUB_REF_NAME matches pyproject.toml $PKG_VERSION."

- name: Build | Clean Previous Builds
if: steps.release.outputs.released == 'true'
run: |
rm -rf -- dist build *.egg-info
- name: Clean previous builds
run: rm -rf -- dist build *.egg-info

- name: Build | Create Distribution Package
if: steps.release.outputs.released == 'true'
- name: Build distribution
run: uv build

- name: Upload | Distribution Artifacts
if: steps.release.outputs.released == 'true'
uses: actions/upload-artifact@v4
with:
name: distribution-artifacts
path: dist
if-no-files-found: error

publish-to-pypi:
name: Publish Package to PyPI
needs: release-and-upload
runs-on: ubuntu-latest
if: ${{ needs.release-and-upload.outputs.released == 'true' }}

steps:
- name: Setup | Install uv
uses: astral-sh/setup-uv@v5

- name: Download | Distribution Artifacts
uses: actions/download-artifact@v4
with:
name: distribution-artifacts
path: dist

- name: Publish | Upload to PyPI
- name: Publish to PyPI
run: |
if [ -z "${{ secrets.PYPI_API_TOKEN }}" ]; then
echo "Missing PyPI API token"; exit 1;
fi
uv publish dist/*
env:
UV_PUBLISH_TOKEN: ${{ secrets.PYPI_API_TOKEN }}

- name: Create GitHub Release
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
NOTES="$(awk -v ver="$GITHUB_REF_NAME" '
$0 ~ "^## \\[?" substr(ver, 2) {capture=1; next}
capture && /^## / {exit}
capture {print}
' CHANGELOG.md)"
if [ -z "$NOTES" ]; then
NOTES="Release $GITHUB_REF_NAME"
fi
gh release create "$GITHUB_REF_NAME" \
--title "$GITHUB_REF_NAME" \
--notes "$NOTES"
22 changes: 22 additions & 0 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,28 @@ Have ideas for new features or use cases? We're eager to hear them! But first:
- Ensure you have the SDK installed to explore existing use cases.
- After familiarizing yourself with the SDK, contribute your unique use case and share your ideas in our [Discord channel](https://discord.gg/8Jm4v89VAu).

## Branch model

This repo uses a branch-per-major release model. There is no `main`.

- **`v0.18`** — current stable major (semver-zero, so 0.18 IS the major; 0.19 would be a major bump that gets its own branch). PRs for bug fixes / non-breaking features target this branch.
- **`v<next>-dev`** — when next-major (i.e. next-minor on 0.x) work is in progress, this branch is open for breaking changes. PRs introducing them target this branch, not `v0.18`.
- **Older majors** stay on the repo for back-ports and security patches. Default branch on github.com is whichever major is current stable.

When you fork or clone, the default branch is `v0.18` today. If you have a `main` branch from a previous checkout, delete it locally:

```sh
git checkout v0.18
git branch -D main
git remote prune origin
```

## 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.

**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`.

### Bug fixing and Feature development

#### 1. Set yourself up to start coding
Expand Down
Loading
Loading