Skip to content
Merged
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
33 changes: 23 additions & 10 deletions .github/workflows/ci.yaml
Original file line number Diff line number Diff line change
@@ -1,21 +1,34 @@
name: CI
on: [push, pull_request]

on:
push:
branches: [main]
pull_request:
branches: [main]

jobs:
check:
name: Check
lint:
name: Lint and Format
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: chartboost/ruff-action@v1

- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: "3.11"

- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install ruff

- name: Run Ruff linter
uses: chartboost/ruff-action@v1
with:
args: "check"

format:
name: Format
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: chartboost/ruff-action@v1
- name: Check formatting
uses: chartboost/ruff-action@v1
with:
args: "format --check"
9 changes: 4 additions & 5 deletions .github/workflows/docker-image.yml
Original file line number Diff line number Diff line change
Expand Up @@ -28,14 +28,14 @@ jobs:

- name: Install cosign
if: github.event_name != 'pull_request'
uses: sigstore/cosign-installer@v3.5.0
uses: sigstore/cosign-installer@v3.7.0

- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3.3.0
uses: docker/setup-buildx-action@v3.7.1

- name: Log into registry ${{ env.REGISTRY }}
if: github.event_name != 'pull_request'
uses: docker/login-action@v3.1.0
uses: docker/login-action@v3.3.0
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
Expand All @@ -47,7 +47,6 @@ jobs:
with:
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
tags: |
# set latest tag for default branch
type=raw,value=latest,enable={{is_default_branch}}
type=semver,pattern={{version}}
type=semver,pattern={{major}}.{{minor}}
Expand All @@ -57,7 +56,7 @@ jobs:

- name: Build and push Docker image
id: build-and-push
uses: docker/build-push-action@v5.3.0
uses: docker/build-push-action@v6.9.0
with:
context: .
push: ${{ github.event_name != 'pull_request' }}
Expand Down
4 changes: 2 additions & 2 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
# Base image
FROM python:3.12-slim
FROM python:3.11-slim

# Set work directory
WORKDIR /home/docker
Expand All @@ -11,7 +11,7 @@ RUN apt-get update && \

# Copy the necessary files
COPY github-backup/ ./github-backup/
COPY setup.py .
COPY pyproject.toml .
COPY config.json.example ./github-backup/
COPY backup.sh .

Expand Down
94 changes: 18 additions & 76 deletions github-backup/github-backup.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,24 +6,15 @@
import subprocess
import sys
import time
from typing import Dict, Iterator, Tuple, Union
from collections.abc import Iterator
from urllib.parse import urlparse, urlunparse

import requests
from requests.adapters import HTTPAdapter
from urllib3.util.retry import Retry


def get_json(url: str, session: requests.Session) -> Iterator[Dict]:
"""
Fetch JSON data from a URL using pagination, authentication, and handling rate limits.

Args:
url (str): The base URL to fetch data from.
session (requests.Session): The requests session for making HTTP requests.
token (str): The GitHub personal access token for authentication.

Yields:
dict: The JSON data from each page of the response.
"""
def get_json(url: str, session: requests.Session) -> Iterator[dict]:
while True:
try:
response = session.get(url, timeout=10)
Expand Down Expand Up @@ -60,34 +51,13 @@ def get_json(url: str, session: requests.Session) -> Iterator[Dict]:


def check_name(name: str) -> str:
"""
Check if a name is valid according to a regular expression pattern.

Args:
name (str): The name to be checked.

Raises:
ValueError: If the name is invalid.

Returns:
str: The validated name.
"""
pattern = r"^\w[-\.\w]*$"
if not re.match(pattern, name):
raise ValueError(f"Invalid name '{name}'")
return name


def mkdir(path: str) -> bool:
"""
Create a directory if it doesn't exist.

Args:
path (str): The path to the directory.

Returns:
bool: True if a new directory was created, False otherwise.
"""
try:
os.makedirs(path, mode=0o770)
return True
Expand All @@ -98,41 +68,17 @@ def mkdir(path: str) -> bool:


def prepare_repo_url(repo_url: str, username: str, token: str) -> str:
"""
Prepare the repository URL for cloning with authentication.

Args:
repo_url (str): The original repository URL.
username (str): The GitHub username.
token (str): The GitHub personal access token.

Returns:
str: The prepared repository URL with authentication credentials.
"""
parsed = urlparse(repo_url)
modified = list(parsed)
modified[1] = f"{username}:{token}@{parsed.netloc}"
return urlunparse(modified)


def init_bare_repo(repo_path: str) -> None:
"""
Initialize a bare Git repository at the given path.

Args:
repo_path (str): The path to the repository.
"""
subprocess.run(["git", "init", "--bare", "--quiet"], cwd=repo_path, check=True)


def fetch_repo(repo_path: str, repo_url: str) -> None:
"""
Fetch the remote repository into the bare repository.

Args:
repo_path (str): The path to the bare repository.
repo_url (str): The URL of the remote repository.
"""
subprocess.run(
[
"git",
Expand All @@ -148,20 +94,7 @@ def fetch_repo(repo_path: str, repo_url: str) -> None:
)


def mirror(repo_name: str, repo_url: str, to_path: str, username: str, token: str) -> Tuple[str, str]:
"""
Mirror a GitHub repository to a local directory.

Args:
repo_name (str): The name of the repository.
repo_url (str): The URL of the remote repository.
to_path (str): The path to the directory where the repository will be mirrored.
username (str): The GitHub username.
token (str): The GitHub personal access token.

Returns:
Tuple[str, str]: The owner and name of the mirrored repository.
"""
def mirror(repo_name: str, repo_url: str, to_path: str, username: str, token: str) -> tuple[str, str]:
repo_path = os.path.join(to_path, repo_name)
os.makedirs(repo_path, exist_ok=True)

Expand All @@ -173,21 +106,30 @@ def mirror(repo_name: str, repo_url: str, to_path: str, username: str, token: st
return repo_name, repo_url.split("/")[-2]


def create_session(retries: int = 3, backoff_factor: float = 0.3) -> requests.Session:
session = requests.Session()
retry = Retry(total=retries, backoff_factor=backoff_factor, status_forcelist=[500, 502, 503, 504])
adapter = HTTPAdapter(max_retries=retry)
session.mount("http://", adapter)
session.mount("https://", adapter)
return session


def main():
parser = argparse.ArgumentParser(description="Backup GitHub repositories")
parser.add_argument("config", metavar="CONFIG", help="a configuration file")
args = parser.parse_args()

with open(args.config, "rb") as f:
config = json.loads(f.read())
with open(args.config) as f:
config = json.load(f)

owners: Union[list[str], None] = config.get("owners")
owners: list[str] | None = config.get("owners")
token: str = config["token"]
path: str = os.path.expanduser(config["directory"])
if mkdir(path):
print(f"Created directory {path}", file=sys.stderr)

with requests.Session() as session:
with create_session() as session:
session.headers.update({"Authorization": f"token {token}"})
user: dict = next(get_json("https://api.github.com/user", session))
for page in get_json("https://api.github.com/user/repos", session):
Expand Down
34 changes: 34 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
[project]
name = "github_backup"
version = "1.0.0"
requires-python = ">=3.11"
dependencies = [
"requests~=2.32.3"
]

[project.optional-dependencies]
dev = [
"ruff"
]

[build-system]
requires = ["setuptools"]
build-backend = "setuptools.build_meta"

[tool.setuptools.packages.find]
include = ["github_backup"]

[tool.ruff]
exclude = [".venv", ".vscode", "venv"]
line-length = 120
indent-width = 4
target-version = "py311"

[tool.ruff.lint]
select = ["E", "W", "F", "I", "UP", "B"]

[tool.ruff.format]
quote-style = "double"
indent-style = "space"
skip-magic-trailing-comma = false
line-ending = "auto"
1 change: 0 additions & 1 deletion requirements.txt

This file was deleted.

4 changes: 0 additions & 4 deletions ruff.toml

This file was deleted.

13 changes: 0 additions & 13 deletions setup.py

This file was deleted.