Skip to main content

An octocat poorly disguised as a database

Github Issues as a Data Store

The gh-store package provides a data store implementation that uses GitHub Issues as a backend. The primary intended use case is for "github native" applications which are constrained to Github Actions as the only available runtime, and free-tier github resources.

The data storage pattern presented here is inspired by https://github.com/utterance/utterances

Key Features

  • Store and version JSON objects using GitHub Issues
  • Atomic updates through a comment-based event system
  • Point-in-time snapshots for static site generation
  • Built-in GitHub Actions integration

Installation

pip install gh-store  # Requires Python 3.12+

Prerequisites

  • GitHub repository with Issues enabled
  • GitHub token with repo scope
  • For GitHub Actions: issues write permission

Basic Usage

from gh_store.core.store import GitHubStore

store = GitHubStore(
    token="github-token",
    repo="username/repository"
)

# Create object
store.create("metrics", {
    "count": 0,
    "last_updated": "2025-01-16T00:00:00Z"
})

# Update object
store.update("metrics", {"count": 1})

# Get current state
obj = store.get("metrics")
print(f"Current count: {obj.data['count']}")

System Architecture

gh-store uses GitHub Issues as a versioned data store. Here's how the components work together:

1. Object Storage Model

Each stored object is represented by a GitHub Issue:

Issue #123
├── Labels: ["stored-object", "UID:metrics"]
├── Body: Current object state (JSON)
└── Comments: Update history
    ├── Comment 1: Update {"count": 1}
    ├── Comment 2: Update {"field": "value"}
    └── Each comment includes the 👍 reaction when processed

Key components:

  • Base Label ("stored-object"): Identifies issues managed by gh-store
  • UID Label ("UID:{object-id}"): Uniquely identifies each stored object
  • Issue Body: Contains the current state as JSON
  • Comments: Store update history
  • Reactions: Track processed updates (👍)

2. Update Process

When updating an object:

  1. New update is added as a comment with JSON changes
  2. Issue is reopened to trigger processing
  3. GitHub Actions workflow processes updates:
    • Gets all unprocessed comments (no 👍 reaction)
    • Applies updates in chronological order
    • Adds 👍 reaction to mark comments as processed
    • Updates issue body with new state
    • Closes issue when complete

3. Core Components

  • GitHubStore: Main interface for CRUD operations
  • IssueHandler: Manages GitHub Issue operations
  • CommentHandler: Processes update comments

GitHub Actions Integration

Process Updates

# .github/workflows/process_update.yml
name: Process Updates

on:
  issues:
    types: [reopened]

jobs:
  process:
    runs-on: ubuntu-latest
    if: contains(github.event.issue.labels.*.name, 'stored-object')
    permissions:
      issues: write
    steps:
      - uses: actions/checkout@v4
      - name: Process Updates
        run: |
          gh-store process-updates \
            --issue ${{ github.event.issue.number }} \
            --token ${{ secrets.GITHUB_TOKEN }} \
            --repo ${{ github.repository }}

Create Snapshots

# .github/workflows/snapshot.yml
name: Snapshot

on:
  schedule:
    - cron: '0 0 * * *'  # Daily
  workflow_dispatch:

jobs:
  snapshot:
    runs-on: ubuntu-latest
    permissions:
      contents: write
    steps:
      - uses: actions/checkout@v4
      - name: Create Snapshot
        run: |
          gh-store snapshot \
            --token ${{ secrets.GITHUB_TOKEN }} \
            --repo ${{ github.repository }} \
            --output data/store-snapshot.json

CLI Commands

# Process updates for an issue
gh-store process-updates \
  --issue <issue-number> \
  --token <github-token> \
  --repo <owner/repo>

# Create snapshot
gh-store snapshot \
  --token <github-token> \
  --repo <owner/repo> \
  --output <path>

# Update existing snapshot
gh-store update-snapshot \
  --token <github-token> \
  --repo <owner/repo> \
  --snapshot-path <path>

Configuration

A default configuration is automatically created at ~/.config/gh-store/config.yml when first using the tool. You can customize this file or specify a different config location:

store = GitHubStore(
    token="github-token",
    repo="username/repository",
    config_path=Path("custom_config.yml")
)

Default configuration:

# gh_store/default_config.yml

store:
  # Base label for all stored objects
  base_label: "stored-object"
  
  # Prefix for unique identifier labels
  uid_prefix: "UID:"
  
  # Reaction settings
  # Limited to: ["+1", "-1", "laugh", "confused", "heart", "hooray", "rocket", "eyes"]
  reactions:
    processed: "+1"
    initial_state: "rocket"
  
  # Retry settings for GitHub API calls
  retries:
    max_attempts: 3
    backoff_factor: 2
    
  # Rate limiting
  rate_limit:
    max_requests_per_hour: 1000
    
  # Logging
  log:
    level: "INFO"
    format: "{time} | {level} | {message}"

Object History

Each object maintains a complete history from its initial state through all updates:

# Get object history
history = store.issue_handler.get_object_history("metrics")

# History includes initial state and all updates
for entry in history:
    print(f"[{entry['timestamp']}] {entry['type']}")
    print(f"Data: {entry['data']}")

The history includes:

  • Initial state with timestamp and data
  • All updates in chronological order
  • Each entry's comment ID for reference

History is tracked through:

  • Initial state comment marked with 🚀
  • Update comments marked with 👍 when processed
  • All changes preserved in chronological order

Limitations

  • Not suitable for high volume or high velocity (GitHub API limits)
    • Unique objects per store is limited only by the number of unique issues and labels
    • Per experimentation by SO users, github supports at least 10k+ unique labels within a single repo
    • Even if this is theoretically unbounded, it is inadvisable to use this system if you plan to store more than 10k+ items
    • As concrete examples of undocumented github api limitations:
      • There is no limit to the number of repos a single user may star, but above 7k stars the native github frontend breaks
      • There is no limit to the number of stars that can be added to a single star list, but above 3k stars the number of pages exceeds 100, and newly added stars after the 3000th member of the list will not be retrievable via the star list endpoint.
  • Objects limited to Issue size (~65KB)
    • Github supports 10MB attachments to issues/comments, so limited future blob support is feasible
  • Updates processed asynchronously via GitHub Actions
  • Sensitive data that should not be publicly visible

Development

# Install dev dependencies
pip install -e ".[dev]"

# Run tests
pytest

# Type checking & linting
mypy .
ruff check .

License

MIT License - see LICENSE

Release files for gh-store 0.11.3

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

Source distribution (sdist)

Source distribution for gh-store 0.11.3
File Size Uploaded
gh_store-0.11.3.tar.gz 399.0 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for gh-store 0.11.3
File Interpreter ABI Platform
gh_store-0.11.3-py3-none-any.whl Python 3 none any Details

Total release size: 428.3 kB

Release files / gh_store-0.11.3.tar.gz

Download URL gh_store-0.11.3.tar.gz
Size 399.0 kB
Tags Source
SHA-256 checksum
How to use checksums
db66651e0923bbd075956bbf0d637baed783d61d4d14ea50579825bd25d93005
BLAKE2b-256 checksum
How to use checksums
3d57caeb71ffea319f3d4f2e59ad353574ea4c15a8a1f6e1d320176d70725d88
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/6.1.0 CPython/3.12.9

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.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Apr 14, 2025.

Transparency log

Release files / gh_store-0.11.3-py3-none-any.whl

Download URL gh_store-0.11.3-py3-none-any.whl
Size 29.3 kB
Tags Python 3
SHA-256 checksum
How to use checksums
7797b324cfc5b721fdba33908ac49290255fddd141e935059fbfe98e0f81eb9c
BLAKE2b-256 checksum
How to use checksums
727c05670a0d20698775e8d8f8a9676ffce081a676d128b627daf550c86c0f41
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/6.1.0 CPython/3.12.9

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.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Apr 14, 2025.

Transparency log

Release history Release notifications | RSS feed

This release

0.11.3 This release

2 release files

0.11.2

2 release files

0.11.1

2 release files

0.10.4

2 release files

0.10.3

2 release files

0.10.2

2 release files

0.10.1

2 release files

0.10.0

2 release files

0.9.0

2 release files

0.8.0

2 release files

0.7.0

2 release files

0.6.2

2 release files

0.5.1

2 release files

0.5.0

2 release files

0.4.3

2 release files

0.4.2

2 release files

0.4.1

2 release files

0.4.0

2 release files

0.3.0

2 release files

0.2.0

2 release files

0.1.0

2 release 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