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
35 changes: 13 additions & 22 deletions .claude/CLAUDE.md
Original file line number Diff line number Diff line change
@@ -1,28 +1,19 @@
# genlayer-testing-suite

## Releases — CRITICAL
## Releases

This repo uses `python-semantic-release` (config: `releaserc.toml`, CI: `.github/workflows/publish.yml`).
This repo uses a branch-per-major release model. There is no `main`. Releases are deliberate, not automatic.

**NEVER do any of the following:**
- Manually edit version strings in `pyproject.toml` or `glsim/__init__.py`
- Manually create git tags (`git tag vX.Y.Z`)
- Manually run `semantic-release` locally
- Commit messages like `chore: bump version to X.Y.Z`
See `.claude/skills/release/SKILL.md` for the full release flow. Short version:

**How releases work:**
1. Push conventional commits to `main` (`feat:` → minor, `fix:` → patch)
2. CI runs `semantic-release version` which auto-bumps versions, creates commit + tag, pushes
3. CI builds and publishes to PyPI via twine
- Branches are per-major: `v0.29` (current stable), `v<next>-dev` when next-major work is in progress.
- Releases go through `scripts/release.sh` on the target branch. The script bumps `pyproject.toml` + `glsim/__init__.py`, updates `CHANGELOG.md` via python-semantic-release, commits, tags `vX.Y.Z`, and pushes.
- `publish.yml` fires on the tag push and ships to PyPI.
- **Semver-zero rule**: this package is on 0.x, so minor IS the breaking-change boundary. `0.29 → 0.30` is a major bump and needs a new branch — `scripts/release.sh` refuses `minor`/`major` keywords without `--allow-major`.

**When user says "release":**
- Verify all changes are committed and pushed to `main` with proper conventional commit prefixes
- That's it. CI handles the rest. Do NOT touch versions or tags.
- If CI fails, inspect the workflow logs (`gh run view`) — don't try to manually publish

**Version files managed by semantic-release:**
- `pyproject.toml:project.version`
- `glsim/__init__.py:__version__`
- Invoke the release skill. It will confirm version + branch, run pre-flight checks, then call `scripts/release.sh`.
- If CI on the tag fails, inspect the workflow logs (`gh run view`) — fix the issue, delete the bad tag, re-run the script.

## Architecture

Expand All @@ -33,7 +24,7 @@ This repo uses `python-semantic-release` (config: `releaserc.toml`, CI: `.github

## Conventional Commits

All commits must use conventional format:
- `feat(scope): description` — new feature (minor bump)
- `fix(scope): description` — bug fix (patch bump)
- `chore/docs/refactor/test: description` — no version bump
Commits should still use conventional format because the release script generates the changelog from them:
- `feat(scope): description` — new feature
- `fix(scope): description` — bug fix
- `chore/docs/refactor/test: description` — no changelog entry
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-testing-suite. Bumps version, updates CHANGELOG, tags, pushes — CI then publishes to PyPI and creates the GitHub Release. Use when a human asks "release v0.29.x" or "ship a new version".
---

# Release skill — genlayer-testing-suite

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.29.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.29` (current stable). When `v0.19` opens, the previous `v0.29` stays read-only for back-ports.
- Tags live within those branches: `v0.29.1`, `v0.29.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.29
git pull --ff-only origin v0.29
```
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.29 --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-test
```
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
140 changes: 53 additions & 87 deletions .github/workflows/publish.yml
Original file line number Diff line number Diff line change
@@ -1,111 +1,77 @@
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 (and glsim/__init__.py), 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
runs-on: ubuntu-latest
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 }}

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"

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

- name: Build | Create Distribution Package
if: steps.release.outputs.released == 'true'
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: Clean previous builds
run: rm -rf -- dist build *.egg-info

- 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
run: uv publish dist/*
- 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 @@ -27,6 +27,28 @@ Have ideas for new features or use cases? We're eager to hear them! But first:



## Branch model

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

- **`v0.29`** — current stable major (semver-zero, so 0.29 IS the major; 0.30 would be a major bump that gets its own branch).
- **`v<next>-dev`** — when next-major work is in progress.
- Default branch on github.com is the current stable.

If you have a `main` branch from a previous checkout:

```sh
git checkout v0.29
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 0.x, so minor IS the breaking-change boundary. `0.29 → 0.30` 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