Skip to main content

gheasy

Install

pip install gheasy

Generate a CI workflow

uv_ci creates a complete Python CI workflow:

from gheasy.workflow import uv_ci
print(uv_ci("ci", lint_cmd=None).to_yaml())
name: ci
on:
  push:
    branches:
      - main
  pull_request:
    branches:
      - main
jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - name: Checkout
        uses: actions/checkout@v4
      - name: Setup uv
        uses: astral-sh/setup-uv@v5
      - name: Install dependencies
        run: uv sync --frozen
      - name: Test
        run: uv run pytest

Inspect a local repository

GitRepo.at accepts the repository root or any path inside it. brief returns a compact record for prompts and status lines. info returns the branches, remotes, changes, tags, stashes, active operation, filter health, and recovery points.

from gheasy.repo import GitRepo
repo = GitRepo.at('.')
status = repo.info()
status['branch'], status['clean'], status['ahead'], status['behind']
('claude/ramabana-branching-review-uqj6v2', False, 0, 0)

Preview before changing history

divergence compares the current branch with its upstream. It rehearses fast-forward, merge, rebase, and reset, then reports conflicts and a recommendation. merge_preview and rebase_preview inspect a specific integration without changing refs, the index, or the working tree.

preview = repo.divergence(fetch=True)
preview['relation'], preview['recommended'], preview['options']

repo.rebase_preview('origin/main')
{'current': 'claude/ramabana-branching-review-uqj6v2',
 'onto': 'origin/main',
 'clean': False,
 'dirty': ['README.md',
  'gheasy/core.py',
  'gheasy/repo.py',
  'gheasy/workflow.py',
  'nbs/00_core.ipynb',
  'nbs/02_repo.ipynb',
  'nbs/index.ipynb',
  'uv.lock'],
 'untracked': 8,
 'merge_base': '80bb02422f0bbe2ec6dcef2e74c641b19879ac16',
 'commits': [{'oid': 'bc8bbb72c1003c9adcca87f3070cb8bdb8efb000',
   'short': 'bc8bbb7',
   'parents': ['ea046eeb5df51e5f0fac69276a1985bcbfa4f23e'],
   'author': 'Claude',
   'email': 'noreply@anthropic.com',
   'timestamp': 1787690153,
   'decoration': 'HEAD -> claude/ramabana-branching-review-uqj6v2, origin/claude/ramabana-branching-review-uqj6v2, origin/claude/ramabana-branching-notebook-nbnzye',
   'signature': 'N',
   'subject': 'Release 0.0.8'},
  {'oid': 'ea046eeb5df51e5f0fac69276a1985bcbfa4f23e',
   'short': 'ea046ee',
   'parents': ['80bb02422f0bbe2ec6dcef2e74c641b19879ac16'],
   'author': 'Claude',
   'email': 'noreply@anthropic.com',
   'timestamp': 1787690023,
   'decoration': '',
   'signature': 'N',
   'subject': 'Add gheasy.repo: git repository operations as a literate notebook'}],
 'conflicts': [],
 'conflict_likely': False,
 'already_based': True,
 'review': {'base': 'origin/main',
  'head': 'claude/ramabana-branching-review-uqj6v2',
  'base_oid': '80bb02422f0bbe2ec6dcef2e74c641b19879ac16',
  'head_oid': 'bc8bbb72c1003c9adcca87f3070cb8bdb8efb000',
  'diff_base_oid': '80bb02422f0bbe2ec6dcef2e74c641b19879ac16',
  'merge_base': '80bb02422f0bbe2ec6dcef2e74c641b19879ac16',
  'mode': 'review',
  'base_only': 0,
  'head_only': 2,
  'files': [{'path': 'CHANGELOG.md',
    'old_path': '',
    'status': 'M',
    'similarity': 0,
    'additions': 8,
    'deletions': 0,
    'binary': False},
   {'path': 'gheasy/__init__.py',
    'old_path': '',
    'status': 'M',
    'similarity': 0,
    'additions': 1,
    'deletions': 1,
    'binary': False},
   {'path': 'gheasy/_modidx.py',
    'old_path': '',
    'status': 'M',
    'similarity': 0,
    'additions': 152,
    'deletions': 0,
    'binary': False},
   {'path': 'gheasy/core.py',
    'old_path': '',
    'status': 'M',
    'similarity': 0,
    'additions': 9,
    'deletions': 7,
    'binary': False},
   {'path': 'gheasy/repo.py',
    'old_path': '',
    'status': 'A',
    'similarity': 0,
    'additions': 1659,
    'deletions': 0,
    'binary': False},
   {'path': 'nbs/00_core.ipynb',
    'old_path': '',
    'status': 'M',
    'similarity': 0,
    'additions': 9,
    'deletions': 7,
    'binary': False},
   {'path': 'nbs/02_repo.ipynb',
    'old_path': '',
    'status': 'A',
    'similarity': 0,
    'additions': 4298,
    'deletions': 0,
    'binary': False},
   {'path': 'uv.lock',
    'old_path': '',
    'status': 'M',
    'similarity': 0,
    'additions': 1931,
    'deletions': 2551,
    'binary': False}],
  'snapshot_alternative': None,
  'commits': [{'oid': 'bc8bbb72c1003c9adcca87f3070cb8bdb8efb000',
    'short': 'bc8bbb7',
    'parents': ['ea046eeb5df51e5f0fac69276a1985bcbfa4f23e'],
    'author': 'Claude',
    'email': 'noreply@anthropic.com',
    'timestamp': 1787690153,
    'decoration': 'HEAD -> claude/ramabana-branching-review-uqj6v2, origin/claude/ramabana-branching-review-uqj6v2, origin/claude/ramabana-branching-notebook-nbnzye',
    'signature': 'N',
    'subject': 'Release 0.0.8'},
   {'oid': 'ea046eeb5df51e5f0fac69276a1985bcbfa4f23e',
    'short': 'ea046ee',
    'parents': ['80bb02422f0bbe2ec6dcef2e74c641b19879ac16'],
    'author': 'Claude',
    'email': 'noreply@anthropic.com',
    'timestamp': 1787690023,
    'decoration': '',
    'signature': 'N',
    'subject': 'Add gheasy.repo: git repository operations as a literate notebook'}],
  'summary': {'files': 8, 'additions': 8067, 'deletions': 2566, 'binary': 0},
  'can_apply': False}}

Mutate with a way back

Guarded mutations record a safepoint before they run. The result includes an undo token. Pass that token to undo to restore the earlier branch, commit, and uncommitted work.

result = repo.checkout('feature')
print(result['summary'])
repo.undo(result['undo'])

# Apply the strategy selected after reviewing `divergence`.
repo.sync(preview['recommended'])

Review branches and commits

review compares branch work from the merge base. review_file loads one changed file on demand. commit_review and commit_file provide the same view for one commit.

review = repo.review('main', 'claude/ramabana-branching-review-uqj6v2')
review['summary'], review['commits']

file_review = repo.review_file('main', 'claude/ramabana-branching-review-uqj6v2', review['files'][0]['path'])
file_review['left'], file_review['right'], file_review['patch']
('# Release notes\n\n<!-- do not remove -->\n\n## 0.0.7\nghapi is async, so sync=True for now\n\n\n\n## 0.0.6\ngheasy new setup python requires to >=3.11\n\n\n\n## 0.0.5\npins python to 3.13, git workflows optional\n\n\n\n## 0.0.4\nnbdev pyproject to hatchling bugfix + cli addition\n\n\n\n## 0.0.3\nskills\n\n\n\n## 0.0.2\ngheasy makes git lfs, worfklows easy\n\n\n\n## 0.0.1\ninitial release\n\n\n',
 "# Release notes\n\n<!-- do not remove -->\n\n## 0.0.8\n\nNew `gheasy.repo`: git repository operations, moved here from `ramabana.git`. One gateway that\nserialises every git process per repository, a safepoint before every mutation, and previews that\nrehearse a merge or a rebase without touching the worktree. `gheasy.core`'s own git calls now go\nthrough the same gateway.\n\n\n## 0.0.7\nghapi is async, so sync=True for now\n\n\n\n## 0.0.6\ngheasy new setup python requires to >=3.11\n\n\n\n## 0.0.5\npins python to 3.13, git workflows optional\n\n\n\n## 0.0.4\nnbdev pyproject to hatchling bugfix + cli addition\n\n\n\n## 0.0.3\nskills\n\n\n\n## 0.0.2\ngheasy makes git lfs, worfklows easy\n\n\n\n## 0.0.1\ninitial release\n\n\n",
 "diff --git a/CHANGELOG.md b/CHANGELOG.md\nindex 54062f3..eb0aabf 100644\n--- a/CHANGELOG.md\n+++ b/CHANGELOG.md\n@@ -2,6 +2,14 @@\n \n <!-- do not remove -->\n \n+## 0.0.8\n+\n+New `gheasy.repo`: git repository operations, moved here from `ramabana.git`. One gateway that\n+serialises every git process per repository, a safepoint before every mutation, and previews that\n+rehearse a merge or a rebase without touching the worktree. `gheasy.core`'s own git calls now go\n+through the same gateway.\n+\n+\n ## 0.0.7\n ghapi is async, so sync=True for now\n \n")

Build a library pipeline

This workflow runs lint, then tests, then publishes a release to PyPI:

from gheasy.workflow import Workflow

wfb = Workflow("ci")
wfb.on.push(branches=["main"]).pull_request()
wfb.uv_lint_job()
wfb.uv_test_job(needs="lint")
wfb.uv_pypi_job(needs="test")
print(wfb.build().to_yaml())
name: ci
on:
  push:
    branches:
      - main
  pull_request:
jobs:
  lint:
    runs-on: ubuntu-latest
    steps:
      - name: Checkout
        uses: actions/checkout@v4
      - name: Setup uv
        uses: astral-sh/setup-uv@v5
      - name: Install dependencies
        run: uv sync --frozen
      - name: Lint
        run: uv run ruff check . && uv run ruff format --check .
  test:
    needs: lint
    runs-on: ubuntu-latest
    steps:
      - name: Checkout
        uses: actions/checkout@v4
      - name: Setup uv
        uses: astral-sh/setup-uv@v5
      - name: Install dependencies
        run: uv sync --frozen
      - name: Test
        run: uv run pytest
  publish:
    needs: test
    runs-on: ubuntu-latest
    if: github.event_name == 'release'
    permissions:
      id-token: write
    steps:
      - name: Checkout
        uses: actions/checkout@v4
      - name: Setup uv
        uses: astral-sh/setup-uv@v5
      - name: Build
        run: uv build
      - name: Publish
        uses: pypa/gh-action-pypi-publish@release/v1

Build an application pipeline

Use the workflow builder directly when a deployment needs custom steps. This example tests every push and deploys main to Fly.io:

wfb = Workflow("deploy")
wfb.on.push(branches=["main"])

wfb.uv_test_job()

wfb.job("deploy").needs("test").runs_on("ubuntu-latest")\
    .checkout().end_step()\
    .setup_uv().end_step()\
    .step("Deploy to Fly.io").run("fly deploy --remote-only").end_job()

print(wfb.build().to_yaml())
name: deploy
on:
  push:
    branches:
      - main
jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - name: Checkout
        uses: actions/checkout@v4
      - name: Setup uv
        uses: astral-sh/setup-uv@v5
      - name: Install dependencies
        run: uv sync --frozen
      - name: Test
        run: uv run pytest
  deploy:
    needs: test
    runs-on: ubuntu-latest
    steps:
      - name: Checkout
        uses: actions/checkout@v4
      - name: Setup uv
        uses: astral-sh/setup-uv@v5
      - name: Deploy to Fly.io
        run: fly deploy --remote-only

Configure LFS and secrets

Projects can configure Git LFS and copy local environment values to GitHub secrets or variables:

from gheasy.core import gh_lfs, gh_secrets_from_file, gh_push_env, GheasyConfig

# 1. Track binary/media files in git-lfs
gh_lfs(['*.mp3', '*.ogg', '*.wav', '*.png', '*.jpg', '*.webp', '*.xml', '*.db'])

# 2. Push .env values to GitHub — None-default keys become secrets, string-default become variables
cfg = GheasyConfig(app='vedicreader', env_schema={
    # variables (have sane defaults)
    'MODE': 'dev', 'PORT': '5001', 'DOMAIN': 'http://localhost:5001',
    'WANT_GOOGLE': 'true', 'WANT_GIT': 'false',
    'NEED_BACKUP': 'false', 'RC_TYPE': 's3', 'RC_PROVIDER': 'Cloudflare',
    # secrets (no default — must be set)
    'JWT_SCRT': None, 'RESEND_API_KEY': None,
    'GOOGLE_CLI': None, 'GOOGLE_SCRT': None,
    'CF_ACCESS_KEY_ID': None, 'CF_SCRT_ACCESS_KEY': None, 'CF_ENDPOINT': None,
})
cfg.save()

# reads local .env, routes each key: None-schema → gh secret set, string-schema → gh variable set
import os
gh_push_env(dict(os.environ))

Without a schema, gh_secrets_from_file sends every value in .env as a secret:

# Push all KEY=VALUE pairs from .env as GitHub secrets
gh_secrets_from_file('.env')

# With dry-run to preview what would be pushed
gh_secrets_from_file('.env', dry_run=True)

Build a cross-package pipeline

Workflow steps can call code from another package. This example uses dockeasy to generate a Dockerfile, then builds and publishes the image when its source files change:

wfb = Workflow("Build caddy-sqlite image")
wfb.on.push(branches=["main"], paths=["dockeasy/proxy.py", "nbs/01_proxy.ipynb"])
wfb.on.workflow_dispatch()

wfb.job("build-and-push").runs_on("ubuntu-latest")\
    .permissions(contents="read", packages="write")\
    .checkout().end_step()\
    .setup_uv().end_step()\
    .step("Install deps").run("uv sync").end_step()\
    .step("Generate Dockerfile").run(
        'uv run python -c "\n'
        'from dockeasy.proxy import caddy_sqlite_dockerfile\n'
        'caddy_sqlite_dockerfile().save(\'Dockerfile\')\n'
        '"'
    ).end_step()\
    .step("Log in to GHCR").uses("docker/login-action@v3")\
        .with_(registry="ghcr.io",
               username="${{ github.actor }}",
               password="${{ secrets.GITHUB_TOKEN }}").end_step()\
    .step("Build and push").uses("docker/build-push-action@v5")\
        .with_(context=".", push=True,
               tags="ghcr.io/vedicreader/caddy-sqlite:latest").end_job()

print(wfb.build().to_yaml())
name: Build caddy-sqlite image
on:
  push:
    branches:
      - main
    paths:
      - dockeasy/proxy.py
      - nbs/01_proxy.ipynb
  workflow_dispatch:
jobs:
  build-and-push:
    runs-on: ubuntu-latest
    permissions:
      contents: read
      packages: write
    steps:
      - name: Checkout
        uses: actions/checkout@v4
      - name: Setup uv
        uses: astral-sh/setup-uv@v5
      - name: Install deps
        run: uv sync
      - name: Generate Dockerfile
        run: "uv run python -c \"\nfrom dockeasy.proxy import caddy_sqlite_dockerfile\ncaddy_sqlite_dockerfile().save('Dockerfile')\n\
          \""
      - name: Log in to GHCR
        uses: docker/login-action@v3
        with:
          registry: ghcr.io
          username: ${{ github.actor }}
          password: ${{ secrets.GITHUB_TOKEN }}
      - name: Build and push
        uses: docker/build-push-action@v5
        with:
          context: .
          push: true
          tags: ghcr.io/vedicreader/caddy-sqlite:latest

Metadata

Release files for gheasy 0.0.11

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for gheasy 0.0.11
File Size Uploaded Provenance Actions
gheasy-0.0.11.tar.gz 59.7 kB Without provenance Details for gheasy-0.0.11.tar.gz

Built distribution (wheel)

Table of built distributions (wheels) for gheasy 0.0.11
File Interpreter ABI Platform Actions
gheasy-0.0.11-py3-none-any.whl Python 3 none any Details for gheasy-0.0.11-py3-none-any.whl

Total release size: 121.6 kB

Release files / gheasy-0.0.11.tar.gz

Download URL gheasy-0.0.11.tar.gz
Size 59.7 kB
Tags Source
SHA-256 checksum
How to use checksums
3ac0545356812f3f2643584aef6b7a312e23475cc2acd1d203bc4bfe13d0c07b
BLAKE2b-256 checksum
How to use checksums
936873cb319948b2d5c829bf24ddd0ae8c8e16c87824b7cae3e97108fd98c662
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/7.0.0 CPython/3.14.6

Release files / gheasy-0.0.11-py3-none-any.whl

Download URL gheasy-0.0.11-py3-none-any.whl
Size 61.9 kB
Tags Python 3
SHA-256 checksum
How to use checksums
454040cded4b19990ce99a3d2b4bf67e97699e9a659f17bb78c50fd9dc640a61
BLAKE2b-256 checksum
How to use checksums
edfe6645322f04229ccf169272229918abe2b7687abb435c49726afa4d32c8d0
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/7.0.0 CPython/3.14.6

Security for gheasy 0.0.11

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

No provenance found

None of the files in this release were uploaded with provenance metadata.

A note on package security

PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.

To learn more about protecting yourself from malicious actors, visit our package security documentation.

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page