mirror of
https://github.com/Xe138/windmill-git-sync.git
synced 2026-04-02 09:37:24 -04:00
Compare commits
3 Commits
v0.1.0-alp
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
| 5ad72fe986 | |||
| f276beae3b | |||
| ecc598788a |
178
app/sync.py
178
app/sync.py
@@ -3,6 +3,7 @@
|
|||||||
Core sync logic for pulling Windmill workspace and pushing to Git.
|
Core sync logic for pulling Windmill workspace and pushing to Git.
|
||||||
"""
|
"""
|
||||||
import os
|
import os
|
||||||
|
import shutil
|
||||||
import subprocess
|
import subprocess
|
||||||
import logging
|
import logging
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
@@ -59,17 +60,18 @@ def run_wmill_sync(config: Dict[str, Any]) -> bool:
|
|||||||
|
|
||||||
logger.info(f"Syncing Windmill workspace '{workspace}' from {WINDMILL_BASE_URL}")
|
logger.info(f"Syncing Windmill workspace '{workspace}' from {WINDMILL_BASE_URL}")
|
||||||
|
|
||||||
env = os.environ.copy()
|
|
||||||
env['WM_BASE_URL'] = WINDMILL_BASE_URL
|
|
||||||
env['WM_TOKEN'] = windmill_token
|
|
||||||
env['WM_WORKSPACE'] = workspace
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
# Run wmill sync in the workspace directory
|
# Run wmill sync in the workspace directory with explicit flags
|
||||||
|
# Note: When using --base-url, --token and --workspace are required
|
||||||
result = subprocess.run(
|
result = subprocess.run(
|
||||||
['wmill', 'sync', 'pull', '--yes'],
|
[
|
||||||
|
'wmill', 'sync', 'pull',
|
||||||
|
'--base-url', WINDMILL_BASE_URL,
|
||||||
|
'--token', windmill_token,
|
||||||
|
'--workspace', workspace,
|
||||||
|
'--yes'
|
||||||
|
],
|
||||||
cwd=WORKSPACE_DIR,
|
cwd=WORKSPACE_DIR,
|
||||||
env=env,
|
|
||||||
capture_output=True,
|
capture_output=True,
|
||||||
text=True,
|
text=True,
|
||||||
check=True
|
check=True
|
||||||
@@ -85,9 +87,115 @@ def run_wmill_sync(config: Dict[str, Any]) -> bool:
|
|||||||
raise RuntimeError(f"Failed to sync from Windmill: {e.stderr}")
|
raise RuntimeError(f"Failed to sync from Windmill: {e.stderr}")
|
||||||
|
|
||||||
|
|
||||||
|
def is_workspace_empty() -> bool:
|
||||||
|
"""
|
||||||
|
Check if workspace directory is empty or not initialized as a git repo.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
bool: True if directory is empty or not a git repository
|
||||||
|
"""
|
||||||
|
git_dir = WORKSPACE_DIR / '.git'
|
||||||
|
|
||||||
|
# Check if .git directory exists
|
||||||
|
if not git_dir.exists():
|
||||||
|
# Check if workspace is completely empty or has no meaningful content
|
||||||
|
workspace_contents = list(WORKSPACE_DIR.iterdir())
|
||||||
|
return len(workspace_contents) == 0
|
||||||
|
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
def clone_remote_repository(config: Dict[str, Any]) -> Repo:
|
||||||
|
"""
|
||||||
|
Clone remote repository to workspace directory.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
config: Configuration dictionary containing git_remote_url, git_token, git_branch
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Repo: GitPython repository object
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
RuntimeError: If clone fails
|
||||||
|
"""
|
||||||
|
git_remote_url = config['git_remote_url']
|
||||||
|
git_token = config['git_token']
|
||||||
|
git_branch = config.get('git_branch', 'main')
|
||||||
|
git_user_name = config.get('git_user_name', 'Windmill Git Sync')
|
||||||
|
git_user_email = config.get('git_user_email', 'windmill@example.com')
|
||||||
|
|
||||||
|
authenticated_url = get_authenticated_url(git_remote_url, git_token)
|
||||||
|
|
||||||
|
logger.info(f"Cloning remote repository from {git_remote_url}")
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Clone the repository
|
||||||
|
repo = Repo.clone_from(authenticated_url, WORKSPACE_DIR)
|
||||||
|
|
||||||
|
# Configure user
|
||||||
|
repo.config_writer().set_value("user", "name", git_user_name).release()
|
||||||
|
repo.config_writer().set_value("user", "email", git_user_email).release()
|
||||||
|
|
||||||
|
# Check if the specified branch exists
|
||||||
|
try:
|
||||||
|
repo.git.checkout(git_branch)
|
||||||
|
logger.info(f"Checked out existing branch '{git_branch}'")
|
||||||
|
except GitCommandError:
|
||||||
|
# Branch doesn't exist, create it as orphan
|
||||||
|
logger.info(f"Branch '{git_branch}' doesn't exist, creating new branch")
|
||||||
|
repo.git.checkout('--orphan', git_branch)
|
||||||
|
# Remove all files from staging (orphan branch starts with staged files)
|
||||||
|
try:
|
||||||
|
repo.git.rm('-rf', '.')
|
||||||
|
except GitCommandError:
|
||||||
|
# If rm fails (no files to remove), that's fine
|
||||||
|
pass
|
||||||
|
|
||||||
|
logger.info("Repository cloned successfully")
|
||||||
|
return repo
|
||||||
|
|
||||||
|
except GitCommandError as e:
|
||||||
|
logger.error(f"Failed to clone repository: {str(e)}")
|
||||||
|
raise RuntimeError(f"Failed to clone remote repository: {str(e)}")
|
||||||
|
|
||||||
|
|
||||||
|
def sync_local_with_remote(repo: Repo, config: Dict[str, Any]) -> None:
|
||||||
|
"""
|
||||||
|
Sync local repository with remote (fetch and hard reset).
|
||||||
|
|
||||||
|
Args:
|
||||||
|
repo: GitPython Repo object
|
||||||
|
config: Configuration dictionary containing git_branch
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
RuntimeError: If sync fails
|
||||||
|
"""
|
||||||
|
git_branch = config.get('git_branch', 'main')
|
||||||
|
|
||||||
|
logger.info(f"Syncing local repository with remote branch '{git_branch}'")
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Fetch from remote
|
||||||
|
origin = repo.remote('origin')
|
||||||
|
origin.fetch()
|
||||||
|
logger.info("Fetched from remote")
|
||||||
|
|
||||||
|
# Reset local branch to match remote
|
||||||
|
try:
|
||||||
|
repo.git.reset('--hard', f'origin/{git_branch}')
|
||||||
|
logger.info(f"Reset local branch to match origin/{git_branch}")
|
||||||
|
except GitCommandError as e:
|
||||||
|
# Branch might not exist on remote yet, which is fine
|
||||||
|
logger.info(f"Branch '{git_branch}' doesn't exist on remote yet, will be created on push")
|
||||||
|
|
||||||
|
except GitCommandError as e:
|
||||||
|
logger.error(f"Failed to sync with remote: {str(e)}")
|
||||||
|
raise RuntimeError(f"Failed to sync local repository with remote: {str(e)}")
|
||||||
|
|
||||||
|
|
||||||
def init_or_update_git_repo(config: Dict[str, Any]) -> Repo:
|
def init_or_update_git_repo(config: Dict[str, Any]) -> Repo:
|
||||||
"""
|
"""
|
||||||
Initialize Git repository or open existing one.
|
Initialize Git repository, clone from remote if needed, or open existing one.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
config: Configuration dictionary containing optional git_user_name and git_user_email
|
config: Configuration dictionary containing optional git_user_name and git_user_email
|
||||||
@@ -100,18 +208,34 @@ def init_or_update_git_repo(config: Dict[str, Any]) -> Repo:
|
|||||||
|
|
||||||
git_dir = WORKSPACE_DIR / '.git'
|
git_dir = WORKSPACE_DIR / '.git'
|
||||||
|
|
||||||
|
# Check if workspace is empty/uninitialized
|
||||||
|
if is_workspace_empty():
|
||||||
|
# Try to clone from remote
|
||||||
|
logger.info("Workspace is empty, attempting to clone from remote")
|
||||||
|
return clone_remote_repository(config)
|
||||||
|
|
||||||
|
# Repository already exists
|
||||||
if git_dir.exists():
|
if git_dir.exists():
|
||||||
logger.info("Opening existing Git repository")
|
logger.info("Opening existing Git repository")
|
||||||
repo = Repo(WORKSPACE_DIR)
|
repo = Repo(WORKSPACE_DIR)
|
||||||
|
|
||||||
|
# Sync with remote before continuing
|
||||||
|
sync_local_with_remote(repo, config)
|
||||||
|
|
||||||
|
return repo
|
||||||
else:
|
else:
|
||||||
logger.info("Initializing new Git repository")
|
# Workspace has files but no git repo - delete contents and clone
|
||||||
repo = Repo.init(WORKSPACE_DIR)
|
logger.info("Workspace has files but no git repository, cleaning and cloning from remote")
|
||||||
|
|
||||||
# Configure user
|
# Delete all contents in workspace
|
||||||
repo.config_writer().set_value("user", "name", git_user_name).release()
|
for item in WORKSPACE_DIR.iterdir():
|
||||||
repo.config_writer().set_value("user", "email", git_user_email).release()
|
if item.is_dir():
|
||||||
|
shutil.rmtree(item)
|
||||||
|
else:
|
||||||
|
item.unlink()
|
||||||
|
|
||||||
return repo
|
logger.info("Workspace cleaned, attempting to clone from remote")
|
||||||
|
return clone_remote_repository(config)
|
||||||
|
|
||||||
|
|
||||||
def commit_and_push_changes(repo: Repo, config: Dict[str, Any]) -> bool:
|
def commit_and_push_changes(repo: Repo, config: Dict[str, Any]) -> bool:
|
||||||
@@ -159,9 +283,21 @@ def commit_and_push_changes(repo: Repo, config: Dict[str, Any]) -> bool:
|
|||||||
|
|
||||||
# Push to remote
|
# Push to remote
|
||||||
logger.info(f"Pushing to {git_remote_url} (branch: {git_branch})")
|
logger.info(f"Pushing to {git_remote_url} (branch: {git_branch})")
|
||||||
origin.push(refspec=f'HEAD:{git_branch}', force=False)
|
push_info = origin.push(refspec=f'HEAD:{git_branch}', force=False)
|
||||||
logger.info("Push completed successfully")
|
|
||||||
|
|
||||||
|
# Check push results for errors
|
||||||
|
if push_info:
|
||||||
|
for info in push_info:
|
||||||
|
if info.flags & info.ERROR:
|
||||||
|
error_msg = f"Push failed: {info.summary}"
|
||||||
|
logger.error(error_msg)
|
||||||
|
raise RuntimeError(error_msg)
|
||||||
|
elif info.flags & info.REJECTED:
|
||||||
|
error_msg = f"Push rejected (non-fast-forward): {info.summary}"
|
||||||
|
logger.error(error_msg)
|
||||||
|
raise RuntimeError(error_msg)
|
||||||
|
|
||||||
|
logger.info("Push completed successfully")
|
||||||
return True
|
return True
|
||||||
|
|
||||||
except GitCommandError as e:
|
except GitCommandError as e:
|
||||||
@@ -192,12 +328,12 @@ def sync_windmill_to_git(config: Dict[str, Any]) -> Dict[str, Any]:
|
|||||||
|
|
||||||
workspace = config.get('workspace', 'admins')
|
workspace = config.get('workspace', 'admins')
|
||||||
|
|
||||||
# Pull from Windmill
|
# Initialize/update Git repo (must happen BEFORE wmill sync to clone if needed)
|
||||||
run_wmill_sync(config)
|
|
||||||
|
|
||||||
# Initialize/update Git repo
|
|
||||||
repo = init_or_update_git_repo(config)
|
repo = init_or_update_git_repo(config)
|
||||||
|
|
||||||
|
# Pull from Windmill (overwrites files with Windmill workspace content)
|
||||||
|
run_wmill_sync(config)
|
||||||
|
|
||||||
# Commit and push changes
|
# Commit and push changes
|
||||||
has_changes = commit_and_push_changes(repo, config)
|
has_changes = commit_and_push_changes(repo, config)
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user