Skip to content

Commit

Permalink
feat: Add persistent SSH agent management library
Browse files Browse the repository at this point in the history
Add a Python library for persistent SSH agent management with automatic key handling, focusing on Windows compatibility and seamless Git integration. Includes core functionality, tests, and documentation.

Signed-off-by: longhao <hal.long@outlook.com>
  • Loading branch information
loonghao committed Dec 22, 2024
1 parent 7c3ce39 commit 380291a
Show file tree
Hide file tree
Showing 11 changed files with 422 additions and 328 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/mr-test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ jobs:
triple: 'x86_64-apple-darwin'
- os: 'windows-2022'
triple: 'x86_64-pc-windows-msvc'
python-version: ["3.10"]
python-version: ["3.7", "3.8", "3.9", "3.10", "3.11", "3.12"]
fail-fast: false
runs-on: ${{ matrix.target.os }}
steps:
Expand Down
100 changes: 33 additions & 67 deletions .github/workflows/python-publish.yml
Original file line number Diff line number Diff line change
Expand Up @@ -7,35 +7,18 @@ on:

jobs:
deploy:
strategy:
max-parallel: 3
matrix:
target:
- os: 'ubuntu-22.04'
triple: 'x86_64-unknown-linux-gnu'
- os: 'macos-12'
triple: 'x86_64-apple-darwin'
- os: 'windows-2022'
triple: 'x86_64-pc-windows-msvc'
python-version: ["3.10"]
fail-fast: false
runs-on: ${{ matrix.target.os }}
runs-on: ubuntu-latest
permissions:
# IMPORTANT: this permission is mandatory for trusted publishing
id-token: write
contents: write

steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
with:
token: "${{ secrets.GITHUB_TOKEN }}"
fetch-depth: 0
ref: main
- name: Get repository name
id: repo-name
uses: MariachiBear/get-repo-name-action@v1.3.0
with:
with-owner: 'true'
string-case: 'uppercase'
- uses: olegtarasov/get-tag@v2.1.4
id: get_tag_name
with:
Expand All @@ -48,53 +31,36 @@ jobs:
run: |
python -m pip install -r requirements-dev.txt
poetry --version
- name: build exe
id: build_exe
run: |
nox -s build-exe -- --release --version ${{ steps.get_tag_name.outputs.version }}
- name: Upload
uses: actions/upload-artifact@v4
poetry build
# Note that we don't need credentials.
# We rely on https://docs.pypi.org/trusted-publishers/.
- name: Upload to PyPI
uses: pypa/gh-action-pypi-publish@release/v1
with:
name: ${{ steps.repo-name.outputs.repository-name }}-${{ steps.get_tag_name.outputs.version }}-${{ matrix.target.triple }}
path: .zip/${{ steps.repo-name.outputs.repository-name }}-${{ steps.get_tag_name.outputs.version }}-${{ matrix.target.triple }}.zip

upload-artifact:
needs: deploy
runs-on: ubuntu-latest
permissions:
# IMPORTANT: this permission is mandatory for trusted publishing
id-token: write
contents: write
steps:
- name: Download
uses: actions/download-artifact@v4
with:
path: artifact
merge-multiple: true
- run: ls -R artifact
- name: Generate changelog
id: changelog
uses: jaywcjlove/changelog-generator@main
with:
token: "${{ secrets.GITHUB_TOKEN }}"
filter-author: (|dependabot|renovate\\[bot\\]|dependabot\\[bot\\]|Renovate Bot)
filter: '[R|r]elease[d]\s+[v|V]\d(\.\d+){0,2}'
template: |
## Bugs
{{fix}}
## Feature
{{feat}}
## Improve
{{refactor,perf,clean}}
## Misc
{{chore,style,ci||🔶 Nothing change}}
## Unknown
{{__unknown__}}
- uses: ncipollo/release-action@v1
with:
artifacts: artifact/*
token: ${{ secrets.PERSONAL_ACCESS_TOKEN }}
body: |
Comparing Changes: ${{ steps.changelog.outputs.compareurl }}
packages-dir: dist
- name: Generate changelog
id: changelog
uses: jaywcjlove/changelog-generator@main
with:
token: ${{ secrets.PERSONAL_ACCESS_TOKEN }}
filter-author: (|dependabot|renovate\\[bot\\]|dependabot\\[bot\\]|Renovate Bot)
filter: '[R|r]elease[d]\s+[v|V]\d(\.\d+){0,2}'
template: |
## Bugs
{{fix}}
## Feature
{{feat}}
## Improve
{{refactor,perf,clean}}
## Misc
{{chore,style,ci||🔶 Nothing change}}
## Unknown
{{__unknown__}}
- uses: ncipollo/release-action@v1
with:
artifacts: "dist/*"
token: ${{ secrets.PERSONAL_ACCESS_TOKEN }}
body: |
Comparing Changes: ${{ steps.changelog.outputs.compareurl }}
${{ steps.changelog.outputs.changelog }}
${{ steps.changelog.outputs.changelog }}
14 changes: 0 additions & 14 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -10,17 +10,3 @@ repos:
- id: check-toml
- id: end-of-file-fixer
- id: trailing-whitespace
- repo: local
hooks:
- id: ruff check
name: Ruff check
entry: nox
args: [-s, ruff_check]
language: system
types: [python]
- id: isort check
name: isort check
entry: nox
args: [ -s, isort_check]
language: system
types: [python]
44 changes: 22 additions & 22 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -63,21 +63,21 @@ import os
def clone_with_gitpython(repo_url: str, local_path: str, branch: str = None) -> Repo:
"""Clone a repository using GitPython with persistent SSH authentication."""
ssh_agent = PersistentSSHAgent()

# Extract hostname and set up SSH
hostname = ssh_agent._extract_hostname(repo_url)
if not hostname or not ssh_agent.setup_ssh(hostname):
raise RuntimeError("Failed to set up SSH authentication")

# Get SSH command and configure environment
ssh_command = ssh_agent.get_git_ssh_command(hostname)
if not ssh_command:
raise RuntimeError("Failed to get SSH command")

# Set up Git environment
env = os.environ.copy()
env['GIT_SSH_COMMAND'] = ssh_command

# Clone with GitPython
return Repo.clone_from(
repo_url,
Expand All @@ -94,7 +94,7 @@ try:
branch='main'
)
print(f"✅ Repository cloned: {repo.working_dir}")

# Perform Git operations
repo.remotes.origin.pull()
repo.remotes.origin.push()
Expand All @@ -109,36 +109,36 @@ def setup_git_operations():
"""Set up environment for Git operations."""
ssh_agent = PersistentSSHAgent()
hostname = "github.com"

if not ssh_agent.setup_ssh(hostname):
raise RuntimeError("SSH setup failed")

ssh_command = ssh_agent.get_git_ssh_command(hostname)
if not ssh_command:
raise RuntimeError("Failed to get SSH command")

os.environ['GIT_SSH_COMMAND'] = ssh_command
return True

# Example: Complex Git operations
def manage_git_workflow(repo_path: str):
if not setup_git_operations():
return False

repo = Repo(repo_path)

# Create and checkout new branch
new_branch = repo.create_head('feature/new-feature')
new_branch.checkout()

# Make changes
with open(os.path.join(repo_path, 'new_file.txt'), 'w') as f:
f.write('New content')

# Stage and commit
repo.index.add(['new_file.txt'])
repo.index.commit('Add new file')

# Push to remote
repo.remotes.origin.push(new_branch)
```
Expand All @@ -154,16 +154,16 @@ from persistent_ssh_agent import PersistentSSHAgent
def setup_ci_ssh():
"""Set up SSH for CI environment."""
ssh_agent = PersistentSSHAgent()

# Set up SSH key from environment
key_path = os.environ.get('SSH_PRIVATE_KEY_PATH')
if not key_path:
raise ValueError("SSH key path not provided")

if ssh_agent._start_ssh_agent(key_path):
print("✅ SSH agent started successfully")
return True

raise RuntimeError("Failed to start SSH agent")
```

Expand All @@ -174,10 +174,10 @@ async def setup_multiple_hosts(hosts: list[str]) -> dict[str, bool]:
"""Set up SSH for multiple hosts concurrently."""
ssh_agent = PersistentSSHAgent()
results = {}

for host in hosts:
results[host] = ssh_agent.setup_ssh(host)

return results

# Usage
Expand Down Expand Up @@ -206,10 +206,10 @@ def safe_git_operation(repo_url: str, local_path: Path) -> Optional[Repo]:
hostname = ssh_agent._extract_hostname(repo_url)
if not hostname:
raise ValueError("Invalid repository URL")

if not ssh_agent.setup_ssh(hostname):
raise RuntimeError("SSH setup failed")

return Repo.clone_from(repo_url, local_path)
except Exception as e:
logger.error(f"Git operation failed: {e}")
Expand All @@ -224,7 +224,7 @@ def safe_git_operation(repo_url: str, local_path: Path) -> Optional[Repo]:
```bash
# Check SSH agent status
ssh-add -l

# Start SSH agent manually
eval $(ssh-agent -s)
```
Expand All @@ -233,7 +233,7 @@ def safe_git_operation(repo_url: str, local_path: Path) -> Optional[Repo]:
```bash
# Fix key permissions
chmod 600 ~/.ssh/id_ed25519

# Test SSH connection
ssh -T git@github.com
```
Expand Down
9 changes: 8 additions & 1 deletion nox_actions/lint.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,4 +14,11 @@ def lint_fix(session: nox.Session) -> None:
session.run("ruff", "check", "--fix")
session.run("isort", ".")
session.run("pre-commit", "run", "--all-files")
session.run("autoflake", "--in-place", "--remove-all-unused-imports", "--remove-unused-variables")
session.run(
"autoflake",
"--in-place",
"--remove-all-unused-imports",
"--remove-unused-variables",
"-r",
PACKAGE_NAME
)
6 changes: 3 additions & 3 deletions noxfile.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,9 @@
sys.path.append(ROOT)

# Import third-party modules
from nox_actions import codetest # noqa: E402
from nox_actions import lint # noqa: E402
from nox_actions import release # noqa: E402
from nox_actions import codetest
from nox_actions import lint
from nox_actions import release


nox.session(lint.lint, name="lint")
Expand Down
2 changes: 1 addition & 1 deletion persistent_ssh_agent/__version__.py
Original file line number Diff line number Diff line change
@@ -1 +1 @@
__version__ = "0.0.1"
__version__ = "0.0.1"
Loading

0 comments on commit 380291a

Please sign in to comment.