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

Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

gheasy-0.0.9.tar.gz (53.4 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

gheasy-0.0.9-py3-none-any.whl (54.8 kB view details)

Uploaded Python 3

File details

Details for the file gheasy-0.0.9.tar.gz.

File metadata

  • Download URL: gheasy-0.0.9.tar.gz
  • Upload date:
  • Size: 53.4 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/7.0.0 CPython/3.13.1

File hashes

Hashes for gheasy-0.0.9.tar.gz
Algorithm Hash digest
SHA256 28927a798cdd2547aa637ad2fed8d610d2d5fac9e556bdedddf287842cfe404f
MD5 5d97cb40871387515d07fe18677ec82c
BLAKE2b-256 d7d4a47bda353869dbd5c13fcd55049a444e9a1b68438c927d4d3aec792952d0

See more details on using hashes here.

File details

Details for the file gheasy-0.0.9-py3-none-any.whl.

File metadata

  • Download URL: gheasy-0.0.9-py3-none-any.whl
  • Upload date:
  • Size: 54.8 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/7.0.0 CPython/3.13.1

File hashes

Hashes for gheasy-0.0.9-py3-none-any.whl
Algorithm Hash digest
SHA256 e426a658601fc9f6ab320bdbb1325995cb246e32394df627288bb33951a21d50
MD5 1b875a1b2300e00f4b496b599ae765ef
BLAKE2b-256 6b250903913e1bdd75e373194c1a54a15dee6badc75345d97f2d59c14e225d81

See more details on using hashes here.

Release history Release notifications | RSS feed

0.0.10

2 files

This release

0.0.9 This release

2 files

0.0.7

2 files

0.0.6

2 files

0.0.5

2 files

0.0.4

2 files

0.0.3

2 files

0.0.2

2 files

0.0.1

2 files

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