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.10.tar.gz (59.1 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.10-py3-none-any.whl (61.3 kB view details)

Uploaded Python 3

File details

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

File metadata

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

File hashes

Hashes for gheasy-0.0.10.tar.gz
Algorithm Hash digest
SHA256 657dc5fc40c5ea962ad2d6d4b07b1b7eace2cd383d1723ee4de954b09f36361f
MD5 4f9bb325dcf4fe1a64d3776a808f1d6d
BLAKE2b-256 e84da47031eda26dcda6a07b2260e1e9641c21363532e9e6f5741db1b2b10c69

See more details on using hashes here.

File details

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

File metadata

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

File hashes

Hashes for gheasy-0.0.10-py3-none-any.whl
Algorithm Hash digest
SHA256 6bf88f60435f2f25ca48c856e2d4d4d5e74b6133b3a7fcfd6a5a5088a830cf30
MD5 888264e6d5a7065f310399e9063527f2
BLAKE2b-256 483666cf858718dad5ade90bb1df76d900f77aef25027b6c869c5c9fee490f49

See more details on using hashes here.

Release history Release notifications | RSS feed

This release

0.0.10 This release

2 files

0.0.9

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