Skip to content
Open
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
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"
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
)"
7 changes: 6 additions & 1 deletion .github/workflows/tests.yml
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
name: Tests

on:
push:
branches:
- 'v*-dev'
workflow_call:
pull_request:
types: [opened, reopened, synchronize, ready_for_review]
Expand All @@ -24,13 +27,15 @@ jobs:
run: uv python install ${{ matrix.python-version }}

- name: Setup | Install dependencies
run: uv sync --python ${{ matrix.python-version }}
run: uv sync --extra sim --python ${{ matrix.python-version }}

- name: Action | Run gltest cli tests
run: uv run gltest tests/gltest_cli/

- name: Action | Run gltest tests
run: uv run gltest tests/gltest/
- name: Run glsim tests
run: uv run gltest tests/glsim/

# Unit tests only — integration tests here download the GenVM SDK.
- name: Action | Run direct runner unit tests
Expand Down
4 changes: 2 additions & 2 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -100,7 +100,7 @@ GenLayer contracts are Python classes:
```python
import genlayer as gl

class MyContract(gl.Contract):
class MyContract(gl.contract.Contract):
def __init__(self, initial_value: int):
self.value = initial_value

Expand Down Expand Up @@ -168,4 +168,4 @@ Multi-file contracts use a `runner.json` manifest:
2. Contracts are discovered automatically from the contracts directory
3. Transaction receipts include consensus information and triggered transactions
4. LLM-based contract methods use `gl.eq_principle_prompt_non_comparative()`
5. Version is managed automatically via semantic release (no manual updates)
5. Version is managed automatically via semantic release (no manual updates)
20 changes: 6 additions & 14 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,19 +29,11 @@ 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
```
See [docs/BRANCHING.md](docs/BRANCHING.md) for the current release-train model.
In short: independently releasable work may target the stable branch directly;
multi-feature or cross-repo train work uses the active `*-dev` integration
branch and is promoted to the matching stable branch when ready. `main` is only
the default/static GitHub branch.

## Releases

Expand Down Expand Up @@ -161,7 +153,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. The release automation will handle all version updates automatically when PRs are merged to the main branch.
**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.

### Improving Documentation

Expand Down
56 changes: 53 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -218,6 +218,56 @@ tx_receipt = contract.update_storage(args=["new_value"]).transact(
assert tx_execution_succeeded(tx_receipt)
```

### Fee Profiling

Generate a frontend-ready fee profile from the deploys and write transactions
executed during a gltest session:

```bash
gltest --fee-profile artifacts/fee-profile.json
gltest --fee-profile artifacts/fee-profile.json --fee-profile-headroom 1.5
```

`--fee-profile` writes JSON that can be used as developer fee suggestions by
transaction-kit, genlayer-js, genlayer-py, or CLI-based submission flows. The optional
`--fee-profile-headroom` multiplier defaults to `1.25`. Fee and time-unit
values are multiplied by headroom, rounded up, and emitted as decimal strings.
When the same method is observed in multiple tests, the profile records the
maximum observed value for each field across all of those branches.
`rotationsPerRound` is recorded exactly because it is a posture choice rather
than a consumed fee amount.

```json
{
"version": 1,
"network": "localnet",
"measuredAt": "2026-06-10T12:00:00Z",
"deploy": {
"leaderTimeunitsAllocation": "125",
"validatorTimeunitsAllocation": "250",
"executionBudgetPerRound": "625000",
"totalMessageFees": "0",
"rotationsPerRound": "0"
},
"methods": {
"create_bet": {
"leaderTimeunitsAllocation": "125",
"validatorTimeunitsAllocation": "250",
"executionBudgetPerRound": "312500",
"totalMessageFees": "12500",
"rotationsPerRound": "0"
}
}
}
```

Fee profiling is currently measurable on Studio-based networks whose receipts
include consumed fee data. Testnet receipts do not expose consumed fees yet, and
direct/sim mode does not go through these receipt paths. Time-unit allocations
are recorded from the submitted fee distribution when the backend receipt
includes it. Live price caps and `feeValue` are intentionally omitted so the SDK
can quote them from the current network policy at transaction time.

### Assertions

```python
Expand Down Expand Up @@ -367,9 +417,9 @@ print(f"Unique states: {analysis.unique_states}")
## Example Contract

```python
from genlayer import *
import genlayer as gl

class Storage(gl.Contract):
class Storage(gl.contract.Contract):
storage: str

def __init__(self, initial_storage: str):
Expand Down Expand Up @@ -400,7 +450,7 @@ For more examples, see the [contracts directory](tests/examples/contracts).

## Troubleshooting

**Contract not found**: Ensure contracts are in `contracts/` or specify `--contracts-dir`. Contracts must inherit from `gl.Contract`.
**Contract not found**: Ensure contracts are in `contracts/` or specify `--contracts-dir`. Contracts must inherit from `gl.contract.Contract`.

**Transaction timeouts** (Studio mode): Increase `wait_interval` and `wait_retries` in `.transact()`.

Expand Down
Loading
Loading