salla-gitpuller 0.1.2__tar.gz → 1.0.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {salla_gitpuller-0.1.2 → salla_gitpuller-1.0.0}/PKG-INFO +1 -1
- salla_gitpuller-1.0.0/gitpuller/__init__.py +19 -0
- salla_gitpuller-1.0.0/gitpuller/alert_manager.py +63 -0
- salla_gitpuller-1.0.0/gitpuller/gitpull.py +202 -0
- salla_gitpuller-1.0.0/gitpuller/slack_notifier.py +60 -0
- salla_gitpuller-1.0.0/gitpuller/state_manager.py +113 -0
- {salla_gitpuller-0.1.2 → salla_gitpuller-1.0.0}/pyproject.toml +1 -1
- {salla_gitpuller-0.1.2 → salla_gitpuller-1.0.0}/salla_gitpuller.egg-info/PKG-INFO +1 -1
- {salla_gitpuller-0.1.2 → salla_gitpuller-1.0.0}/salla_gitpuller.egg-info/SOURCES.txt +3 -0
- salla_gitpuller-0.1.2/gitpuller/__init__.py +0 -4
- salla_gitpuller-0.1.2/gitpuller/gitpull.py +0 -115
- {salla_gitpuller-0.1.2 → salla_gitpuller-1.0.0}/LICENSE +0 -0
- {salla_gitpuller-0.1.2 → salla_gitpuller-1.0.0}/README.md +0 -0
- {salla_gitpuller-0.1.2 → salla_gitpuller-1.0.0}/gitpuller/utils.py +0 -0
- {salla_gitpuller-0.1.2 → salla_gitpuller-1.0.0}/salla_gitpuller.egg-info/dependency_links.txt +0 -0
- {salla_gitpuller-0.1.2 → salla_gitpuller-1.0.0}/salla_gitpuller.egg-info/top_level.txt +0 -0
- {salla_gitpuller-0.1.2 → salla_gitpuller-1.0.0}/setup.cfg +0 -0
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
from .gitpull import GitPullExecutor
|
|
2
|
+
from .alert_manager import AlertManager
|
|
3
|
+
from .state_manager import StateManager, InMemoryStateManager, MageAIStateManager
|
|
4
|
+
from .slack_notifier import SlackNotifier
|
|
5
|
+
from .utils import transform_custom, get_repo_path, get_env_base_path
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
__version__ = "1.0.0"
|
|
9
|
+
__all__ = [
|
|
10
|
+
"GitPullExecutor",
|
|
11
|
+
"AlertManager",
|
|
12
|
+
"StateManager",
|
|
13
|
+
"InMemoryStateManager",
|
|
14
|
+
"MageAIStateManager",
|
|
15
|
+
"SlackNotifier",
|
|
16
|
+
"transform_custom",
|
|
17
|
+
"get_repo_path",
|
|
18
|
+
"get_env_base_path"
|
|
19
|
+
]
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
from datetime import datetime, timedelta
|
|
2
|
+
from typing import Any, Dict, Optional, Tuple
|
|
3
|
+
from .state_manager import StateManager, InMemoryStateManager, MageAIStateManager
|
|
4
|
+
|
|
5
|
+
class AlertManager:
|
|
6
|
+
def __init__(self, state_manager: Optional[StateManager] = None, use_mage_ai: bool = False):
|
|
7
|
+
if state_manager is not None:
|
|
8
|
+
self.state_manager = state_manager
|
|
9
|
+
elif use_mage_ai:
|
|
10
|
+
try:
|
|
11
|
+
self.state_manager = MageAIStateManager()
|
|
12
|
+
except ImportError:
|
|
13
|
+
print("⚠️ Mage AI not available, falling back to in-memory state manager")
|
|
14
|
+
self.state_manager = InMemoryStateManager()
|
|
15
|
+
else:
|
|
16
|
+
self.state_manager = InMemoryStateManager()
|
|
17
|
+
|
|
18
|
+
def should_send_alert(
|
|
19
|
+
self,
|
|
20
|
+
pipeline_uuid: str,
|
|
21
|
+
current_error: str,
|
|
22
|
+
suppression_hours: int = 1
|
|
23
|
+
) -> Tuple[bool, Dict[str, Any]]:
|
|
24
|
+
|
|
25
|
+
state = self.state_manager.load_alert_state(pipeline_uuid)
|
|
26
|
+
last_error = state.get("last_error_message")
|
|
27
|
+
last_alert_time_str = state.get("last_alert_time")
|
|
28
|
+
|
|
29
|
+
# If no previous alert, always send
|
|
30
|
+
if not last_error or not last_alert_time_str:
|
|
31
|
+
return True, {"last_error_message": None, "last_alert_time": None}
|
|
32
|
+
|
|
33
|
+
# If error is different, always send
|
|
34
|
+
if last_error != current_error:
|
|
35
|
+
return True, {"last_error_message": last_error, "last_alert_time": last_alert_time_str}
|
|
36
|
+
|
|
37
|
+
# Same error - check if enough time has passed
|
|
38
|
+
try:
|
|
39
|
+
last_alert_time = datetime.fromisoformat(last_alert_time_str)
|
|
40
|
+
time_since_last_alert = datetime.now() - last_alert_time
|
|
41
|
+
|
|
42
|
+
if time_since_last_alert >= timedelta(hours=suppression_hours):
|
|
43
|
+
return True, {"last_error_message": last_error, "last_alert_time": last_alert_time_str}
|
|
44
|
+
else:
|
|
45
|
+
return False, {"last_error_message": last_error, "last_alert_time": last_alert_time_str}
|
|
46
|
+
except (ValueError, TypeError):
|
|
47
|
+
# If we can't parse the time, send the alert to be safe
|
|
48
|
+
return True, {"last_error_message": last_error, "last_alert_time": last_alert_time_str}
|
|
49
|
+
|
|
50
|
+
def save_alert_state(
|
|
51
|
+
self,
|
|
52
|
+
pipeline_uuid: str,
|
|
53
|
+
error_message: str,
|
|
54
|
+
alert_time: datetime,
|
|
55
|
+
pipeline_status: str
|
|
56
|
+
) -> None:
|
|
57
|
+
"""Save alert state."""
|
|
58
|
+
self.state_manager.save_alert_state(pipeline_uuid, error_message, alert_time, pipeline_status)
|
|
59
|
+
|
|
60
|
+
def clear_alert_state(self, pipeline_uuid: str) -> None:
|
|
61
|
+
"""Clear alert state on success."""
|
|
62
|
+
self.state_manager.clear_alert_state(pipeline_uuid)
|
|
63
|
+
|
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import subprocess
|
|
3
|
+
from typing import Dict, Any, Optional
|
|
4
|
+
from datetime import datetime
|
|
5
|
+
from .alert_manager import AlertManager
|
|
6
|
+
from .slack_notifier import SlackNotifier
|
|
7
|
+
|
|
8
|
+
class GitPullExecutor:
|
|
9
|
+
def __init__(
|
|
10
|
+
self,
|
|
11
|
+
slack_webhook_url: Optional[str] = None,
|
|
12
|
+
use_mage_ai: bool = False,
|
|
13
|
+
state_manager: Optional[Any] = None
|
|
14
|
+
):
|
|
15
|
+
self.slack_notifier = SlackNotifier(webhook_url=slack_webhook_url)
|
|
16
|
+
self.alert_manager = AlertManager(state_manager=state_manager, use_mage_ai=use_mage_ai)
|
|
17
|
+
|
|
18
|
+
def normalize_ssh_key(self, key_material: str) -> str:
|
|
19
|
+
if not key_material:
|
|
20
|
+
return key_material
|
|
21
|
+
|
|
22
|
+
# Remove any quotes that might be wrapping the key
|
|
23
|
+
key_material = key_material.strip().strip('"').strip("'")
|
|
24
|
+
|
|
25
|
+
# Handle literal \n sequences
|
|
26
|
+
if "\\n" in key_material:
|
|
27
|
+
key_material = key_material.replace("\\n", "\n")
|
|
28
|
+
|
|
29
|
+
# Ensure proper line endings
|
|
30
|
+
if not key_material.endswith("\n"):
|
|
31
|
+
key_material += "\n"
|
|
32
|
+
|
|
33
|
+
return key_material
|
|
34
|
+
|
|
35
|
+
def prepare_ssh_key(
|
|
36
|
+
self,
|
|
37
|
+
key_material: str,
|
|
38
|
+
key_filename: str = "deploy_key",
|
|
39
|
+
ssh_dir: str = "/home/src/.ssh"
|
|
40
|
+
) -> str:
|
|
41
|
+
os.makedirs(ssh_dir, exist_ok=True)
|
|
42
|
+
os.chmod(ssh_dir, 0o700)
|
|
43
|
+
|
|
44
|
+
key_path = os.path.join(ssh_dir, key_filename)
|
|
45
|
+
|
|
46
|
+
# Write key with restricted permissions
|
|
47
|
+
with open(key_path, "w") as f:
|
|
48
|
+
f.write(key_material)
|
|
49
|
+
os.chmod(key_path, 0o600)
|
|
50
|
+
|
|
51
|
+
return key_path
|
|
52
|
+
|
|
53
|
+
def execute_git_pull(
|
|
54
|
+
self,
|
|
55
|
+
repo_path: str,
|
|
56
|
+
git_url: str,
|
|
57
|
+
branch: str = "master",
|
|
58
|
+
ssh_key: Optional[str] = None,
|
|
59
|
+
workspace_name: Optional[str] = None,
|
|
60
|
+
key_filename: Optional[str] = None,
|
|
61
|
+
ssh_dir: str = "/home/src/.ssh"
|
|
62
|
+
) -> Dict[str, Any]:
|
|
63
|
+
if not os.path.exists(repo_path):
|
|
64
|
+
raise ValueError(f"Repo path does not exist: {repo_path}")
|
|
65
|
+
|
|
66
|
+
# Get SSH key
|
|
67
|
+
if not ssh_key and workspace_name:
|
|
68
|
+
key_env_name = f"{workspace_name}_SSHKEY"
|
|
69
|
+
ssh_key = os.environ.get(key_env_name)
|
|
70
|
+
if not ssh_key:
|
|
71
|
+
raise ValueError(f"Missing env var {key_env_name} with the PRIVATE deploy key")
|
|
72
|
+
elif not ssh_key:
|
|
73
|
+
raise ValueError("SSH key is required. Provide it directly or set workspace_name.")
|
|
74
|
+
|
|
75
|
+
# Normalize key
|
|
76
|
+
ssh_key = self.normalize_ssh_key(ssh_key)
|
|
77
|
+
|
|
78
|
+
# Prepare key file
|
|
79
|
+
if not key_filename:
|
|
80
|
+
key_filename = f"{workspace_name}_deploy" if workspace_name else "deploy_key"
|
|
81
|
+
|
|
82
|
+
key_path = self.prepare_ssh_key(ssh_key, key_filename, ssh_dir)
|
|
83
|
+
|
|
84
|
+
# Execute git pull
|
|
85
|
+
cmd = [
|
|
86
|
+
"git",
|
|
87
|
+
"-c",
|
|
88
|
+
f"core.sshCommand=ssh -i {key_path} -o IdentitiesOnly=yes -o StrictHostKeyChecking=accept-new",
|
|
89
|
+
"pull",
|
|
90
|
+
git_url,
|
|
91
|
+
branch,
|
|
92
|
+
]
|
|
93
|
+
|
|
94
|
+
git_output = ""
|
|
95
|
+
git_status = "success"
|
|
96
|
+
key_env_var_used = f"{workspace_name}_SSHKEY" if workspace_name else "N/A"
|
|
97
|
+
|
|
98
|
+
original_dir = None
|
|
99
|
+
try:
|
|
100
|
+
original_dir = os.getcwd()
|
|
101
|
+
os.chdir(repo_path)
|
|
102
|
+
|
|
103
|
+
result = subprocess.run(
|
|
104
|
+
cmd,
|
|
105
|
+
capture_output=True,
|
|
106
|
+
text=True,
|
|
107
|
+
check=True,
|
|
108
|
+
)
|
|
109
|
+
git_output = result.stdout
|
|
110
|
+
git_status = "success"
|
|
111
|
+
except subprocess.CalledProcessError as e:
|
|
112
|
+
git_output = (e.stdout or "") + (e.stderr or "")
|
|
113
|
+
git_status = "error"
|
|
114
|
+
raise
|
|
115
|
+
finally:
|
|
116
|
+
if original_dir:
|
|
117
|
+
try:
|
|
118
|
+
os.chdir(original_dir)
|
|
119
|
+
except Exception:
|
|
120
|
+
pass
|
|
121
|
+
# Clean up the key file
|
|
122
|
+
try:
|
|
123
|
+
os.remove(key_path)
|
|
124
|
+
except Exception:
|
|
125
|
+
pass
|
|
126
|
+
|
|
127
|
+
return {
|
|
128
|
+
"workspace": workspace_name or "N/A",
|
|
129
|
+
"repo_path": repo_path,
|
|
130
|
+
"git_pull_status": git_status,
|
|
131
|
+
"git_pull_output": git_output.strip(),
|
|
132
|
+
"key_env_var_used": key_env_var_used,
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
def execute_with_alerting(
|
|
136
|
+
self,
|
|
137
|
+
repo_path: str,
|
|
138
|
+
git_url: str,
|
|
139
|
+
branch: str = "master",
|
|
140
|
+
ssh_key: Optional[str] = None,
|
|
141
|
+
workspace_name: Optional[str] = None,
|
|
142
|
+
pipeline_uuid: str = "auto_git_pull",
|
|
143
|
+
suppression_hours: int = 1,
|
|
144
|
+
key_filename: Optional[str] = None,
|
|
145
|
+
ssh_dir: str = "/home/src/.ssh"
|
|
146
|
+
) -> Dict[str, Any]:
|
|
147
|
+
repo_name = git_url.split('/')[-1].replace('.git', '')
|
|
148
|
+
|
|
149
|
+
try:
|
|
150
|
+
result = self.execute_git_pull(
|
|
151
|
+
repo_path=repo_path,
|
|
152
|
+
git_url=git_url,
|
|
153
|
+
branch=branch,
|
|
154
|
+
ssh_key=ssh_key,
|
|
155
|
+
workspace_name=workspace_name,
|
|
156
|
+
key_filename=key_filename,
|
|
157
|
+
ssh_dir=ssh_dir
|
|
158
|
+
)
|
|
159
|
+
|
|
160
|
+
# Clear alert state on success
|
|
161
|
+
self.alert_manager.clear_alert_state(pipeline_uuid)
|
|
162
|
+
|
|
163
|
+
return result
|
|
164
|
+
|
|
165
|
+
except Exception as e:
|
|
166
|
+
git_output = str(e)
|
|
167
|
+
error_signature = git_output.strip()[:500]
|
|
168
|
+
|
|
169
|
+
# Check if we should send alert
|
|
170
|
+
should_alert, _ = self.alert_manager.should_send_alert(
|
|
171
|
+
pipeline_uuid,
|
|
172
|
+
error_signature,
|
|
173
|
+
suppression_hours=suppression_hours
|
|
174
|
+
)
|
|
175
|
+
|
|
176
|
+
if should_alert:
|
|
177
|
+
# Send alert
|
|
178
|
+
self.slack_notifier.send_alert(repo_name, git_output[:1500])
|
|
179
|
+
|
|
180
|
+
# Save alert state
|
|
181
|
+
self.alert_manager.save_alert_state(
|
|
182
|
+
pipeline_uuid,
|
|
183
|
+
error_signature,
|
|
184
|
+
datetime.now(),
|
|
185
|
+
pipeline_status="failed"
|
|
186
|
+
)
|
|
187
|
+
else:
|
|
188
|
+
# Alert suppressed
|
|
189
|
+
state = self.alert_manager.state_manager.load_alert_state(pipeline_uuid)
|
|
190
|
+
last_alert_time = state.get("last_alert_time")
|
|
191
|
+
if last_alert_time:
|
|
192
|
+
try:
|
|
193
|
+
from datetime import datetime, timedelta
|
|
194
|
+
last_alert_dt = datetime.fromisoformat(last_alert_time)
|
|
195
|
+
time_since = datetime.now() - last_alert_dt
|
|
196
|
+
print(f"🔇 Alert suppressed - same error occurred {time_since} ago (suppression window: {suppression_hours} hour(s))")
|
|
197
|
+
except:
|
|
198
|
+
print(f"🔇 Alert suppressed - same error within suppression window ({suppression_hours} hour(s))")
|
|
199
|
+
|
|
200
|
+
# Re-raise the exception
|
|
201
|
+
raise
|
|
202
|
+
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import requests
|
|
3
|
+
from typing import Optional, Dict, Any
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class SlackNotifier:
|
|
7
|
+
|
|
8
|
+
def __init__(self, webhook_url: Optional[str] = None):
|
|
9
|
+
self.webhook_url = webhook_url or os.environ.get("CDM_SLACK_WEBHOOK_URL")
|
|
10
|
+
if not self.webhook_url:
|
|
11
|
+
raise ValueError(
|
|
12
|
+
"Slack webhook URL is required. "
|
|
13
|
+
"Provide it as parameter or set CDM_SLACK_WEBHOOK_URL environment variable."
|
|
14
|
+
)
|
|
15
|
+
|
|
16
|
+
def create_failure_payload(self, repo_name: str, error_output: str) -> Dict[str, Any]:
|
|
17
|
+
payload = {
|
|
18
|
+
"text": f"Automate Git Pull Pipeline Failed",
|
|
19
|
+
"blocks": [
|
|
20
|
+
{
|
|
21
|
+
"type": "header",
|
|
22
|
+
"text": {
|
|
23
|
+
"type": "plain_text",
|
|
24
|
+
"text": f":alert: Automate Git Pull Pipeline Failed :alert:"
|
|
25
|
+
}
|
|
26
|
+
},
|
|
27
|
+
{
|
|
28
|
+
"type": "divider"
|
|
29
|
+
},
|
|
30
|
+
{
|
|
31
|
+
"type": "section",
|
|
32
|
+
"text": {
|
|
33
|
+
"type": "mrkdwn",
|
|
34
|
+
"text": f"*Repository:* `{repo_name}`"
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
]
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
if error_output:
|
|
41
|
+
payload["blocks"].append({
|
|
42
|
+
"type": "section",
|
|
43
|
+
"text": {
|
|
44
|
+
"type": "mrkdwn",
|
|
45
|
+
"text": f"*Error Output:*\n```{error_output[:1500]}```"
|
|
46
|
+
}
|
|
47
|
+
})
|
|
48
|
+
|
|
49
|
+
return payload
|
|
50
|
+
|
|
51
|
+
def send_alert(self, repo_name: str, error_output: str = "") -> bool:
|
|
52
|
+
try:
|
|
53
|
+
payload = self.create_failure_payload(repo_name, error_output)
|
|
54
|
+
response = requests.post(self.webhook_url, json=payload, timeout=10)
|
|
55
|
+
response.raise_for_status()
|
|
56
|
+
print("✅ Alert sent to Slack")
|
|
57
|
+
return True
|
|
58
|
+
except Exception as e:
|
|
59
|
+
print(f"⚠️ Failed to send Slack alert: {e}")
|
|
60
|
+
return False
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
from typing import Any, Dict, Optional
|
|
2
|
+
from datetime import datetime
|
|
3
|
+
|
|
4
|
+
class StateManager:
|
|
5
|
+
def load_alert_state(self, pipeline_uuid: str) -> Dict[str, Any]:
|
|
6
|
+
"""Load alert state for a pipeline."""
|
|
7
|
+
raise NotImplementedError
|
|
8
|
+
|
|
9
|
+
def save_alert_state(
|
|
10
|
+
self,
|
|
11
|
+
pipeline_uuid: str,
|
|
12
|
+
error_message: str,
|
|
13
|
+
alert_time: datetime,
|
|
14
|
+
pipeline_status: str
|
|
15
|
+
) -> None:
|
|
16
|
+
"""Save alert state for a pipeline."""
|
|
17
|
+
raise NotImplementedError
|
|
18
|
+
|
|
19
|
+
def clear_alert_state(self, pipeline_uuid: str) -> None:
|
|
20
|
+
"""Clear alert state for a pipeline."""
|
|
21
|
+
raise NotImplementedError
|
|
22
|
+
|
|
23
|
+
class InMemoryStateManager(StateManager):
|
|
24
|
+
def __init__(self):
|
|
25
|
+
self._state: Dict[str, Dict[str, Any]] = {}
|
|
26
|
+
|
|
27
|
+
def load_alert_state(self, pipeline_uuid: str) -> Dict[str, Any]:
|
|
28
|
+
"""Load alert state from memory."""
|
|
29
|
+
return self._state.get(pipeline_uuid, {
|
|
30
|
+
"last_alert_time": None,
|
|
31
|
+
"last_error_message": None
|
|
32
|
+
})
|
|
33
|
+
|
|
34
|
+
def save_alert_state(
|
|
35
|
+
self,
|
|
36
|
+
pipeline_uuid: str,
|
|
37
|
+
error_message: str,
|
|
38
|
+
alert_time: datetime,
|
|
39
|
+
pipeline_status: str
|
|
40
|
+
) -> None:
|
|
41
|
+
"""Save alert state to memory."""
|
|
42
|
+
self._state[pipeline_uuid] = {
|
|
43
|
+
"last_alert_time": alert_time.isoformat(),
|
|
44
|
+
"last_error_message": error_message,
|
|
45
|
+
"pipeline_status": pipeline_status,
|
|
46
|
+
"last_alert_timestamp": alert_time.isoformat()
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
def clear_alert_state(self, pipeline_uuid: str) -> None:
|
|
50
|
+
"""Clear alert state from memory."""
|
|
51
|
+
if pipeline_uuid in self._state:
|
|
52
|
+
self._state[pipeline_uuid] = {
|
|
53
|
+
"last_alert_time": None,
|
|
54
|
+
"last_error_message": None,
|
|
55
|
+
"pipeline_status": "success"
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
class MageAIStateManager(StateManager):
|
|
59
|
+
def __init__(self):
|
|
60
|
+
try:
|
|
61
|
+
from mage_ai.data_preparation.variable_manager import (
|
|
62
|
+
set_global_variable,
|
|
63
|
+
get_global_variable
|
|
64
|
+
)
|
|
65
|
+
self._set_global_variable = set_global_variable
|
|
66
|
+
self._get_global_variable = get_global_variable
|
|
67
|
+
except ImportError:
|
|
68
|
+
raise ImportError(
|
|
69
|
+
"Mage AI is not installed. Install it with: pip install mage-ai"
|
|
70
|
+
)
|
|
71
|
+
|
|
72
|
+
def load_alert_state(self, pipeline_uuid: str) -> Dict[str, Any]:
|
|
73
|
+
try:
|
|
74
|
+
last_error_message = self._get_global_variable(pipeline_uuid, 'last_error_message')
|
|
75
|
+
last_alert_time = self._get_global_variable(pipeline_uuid, 'last_alert_time')
|
|
76
|
+
|
|
77
|
+
if last_error_message and last_alert_time:
|
|
78
|
+
return {
|
|
79
|
+
"last_alert_time": last_alert_time,
|
|
80
|
+
"last_error_message": last_error_message
|
|
81
|
+
}
|
|
82
|
+
except Exception as e:
|
|
83
|
+
print(f"⚠️ Warning: Could not load alert state from global variables: {e}")
|
|
84
|
+
|
|
85
|
+
return {
|
|
86
|
+
"last_alert_time": None,
|
|
87
|
+
"last_error_message": None
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
def save_alert_state(
|
|
91
|
+
self,
|
|
92
|
+
pipeline_uuid: str,
|
|
93
|
+
error_message: str,
|
|
94
|
+
alert_time: datetime,
|
|
95
|
+
pipeline_status: str
|
|
96
|
+
) -> None:
|
|
97
|
+
try:
|
|
98
|
+
self._set_global_variable(pipeline_uuid, 'last_error_message', error_message)
|
|
99
|
+
self._set_global_variable(pipeline_uuid, 'last_alert_time', alert_time.isoformat())
|
|
100
|
+
self._set_global_variable(pipeline_uuid, 'pipeline_status', pipeline_status)
|
|
101
|
+
self._set_global_variable(pipeline_uuid, 'last_alert_timestamp', alert_time.isoformat())
|
|
102
|
+
print(f"✅ Saved alert state to global variables: error_message, pipeline_status={pipeline_status}")
|
|
103
|
+
except Exception as e:
|
|
104
|
+
print(f"⚠️ Warning: Could not save alert state to global variables: {e}")
|
|
105
|
+
|
|
106
|
+
def clear_alert_state(self, pipeline_uuid: str) -> None:
|
|
107
|
+
try:
|
|
108
|
+
self._set_global_variable(pipeline_uuid, 'last_error_message', None)
|
|
109
|
+
self._set_global_variable(pipeline_uuid, 'last_alert_time', None)
|
|
110
|
+
self._set_global_variable(pipeline_uuid, 'pipeline_status', 'success')
|
|
111
|
+
print("✅ Cleared alert state from global variables")
|
|
112
|
+
except Exception as e:
|
|
113
|
+
print(f"⚠️ Warning: Could not clear alert state: {e}")
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "salla_gitpuller"
|
|
7
|
-
version = "0.
|
|
7
|
+
version = "1.0.0"
|
|
8
8
|
description = "A lightweight utility to git pull a repository using SSH deploy keys stored in environment variables"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
authors = [
|
|
@@ -2,7 +2,10 @@ LICENSE
|
|
|
2
2
|
README.md
|
|
3
3
|
pyproject.toml
|
|
4
4
|
gitpuller/__init__.py
|
|
5
|
+
gitpuller/alert_manager.py
|
|
5
6
|
gitpuller/gitpull.py
|
|
7
|
+
gitpuller/slack_notifier.py
|
|
8
|
+
gitpuller/state_manager.py
|
|
6
9
|
gitpuller/utils.py
|
|
7
10
|
salla_gitpuller.egg-info/PKG-INFO
|
|
8
11
|
salla_gitpuller.egg-info/SOURCES.txt
|
|
@@ -1,115 +0,0 @@
|
|
|
1
|
-
import os
|
|
2
|
-
import requests
|
|
3
|
-
import subprocess
|
|
4
|
-
from typing import Any, Dict
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
def git_pull(repo_path: str, workspace_name: str, git_url: str, key_material: str, webhook_url: str) -> Dict[str, Any]:
|
|
8
|
-
"""
|
|
9
|
-
Executes 'git pull' in the specified repository path using a deploy key
|
|
10
|
-
provided via the env var WP_AUTOPULL_SSHKEY.
|
|
11
|
-
|
|
12
|
-
Args:
|
|
13
|
-
repo_path: Path to the local git repository
|
|
14
|
-
workspace_name: Logical workspace identifier
|
|
15
|
-
git_url: Git remote URL (SSH-based)
|
|
16
|
-
Returns:
|
|
17
|
-
Dict with status and command output.
|
|
18
|
-
"""
|
|
19
|
-
print(f"Running git pull for workspace: {workspace_name}")
|
|
20
|
-
print(f"Repo path: {repo_path}")
|
|
21
|
-
|
|
22
|
-
repo_name = git_url.split('/')[-1].replace('.git', '')
|
|
23
|
-
|
|
24
|
-
if not all([webhook_url]):
|
|
25
|
-
raise EnvironmentError(
|
|
26
|
-
"One or more Slack environment variables are missing:\n"
|
|
27
|
-
"CDM_SLACK_WEBHOOK_URL"
|
|
28
|
-
)
|
|
29
|
-
|
|
30
|
-
payload = {
|
|
31
|
-
"text": f"Automate Git Pull Pipeline Failed",
|
|
32
|
-
"blocks": [
|
|
33
|
-
{
|
|
34
|
-
"type": "header",
|
|
35
|
-
"text": {
|
|
36
|
-
"type": "plain_text",
|
|
37
|
-
"text": f":alert: Automate Git Pull Pipeline Failed :alert:"
|
|
38
|
-
}
|
|
39
|
-
},
|
|
40
|
-
{
|
|
41
|
-
"type": "divider"
|
|
42
|
-
},
|
|
43
|
-
{
|
|
44
|
-
"type": "section",
|
|
45
|
-
"text": {
|
|
46
|
-
"type": "mrkdwn",
|
|
47
|
-
"text": f"*Repository:* `{repo_name}`"
|
|
48
|
-
}
|
|
49
|
-
}
|
|
50
|
-
]
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
if not os.path.exists(repo_path):
|
|
55
|
-
raise FileNotFoundError(f"Repo path does not exist: {repo_path}")
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
if not key_material:
|
|
59
|
-
raise EnvironmentError(f"Missing SSH key material for workspace: {workspace_name}")
|
|
60
|
-
|
|
61
|
-
if key_material:
|
|
62
|
-
# Remove any quotes that might be wrapping the key
|
|
63
|
-
key_material = key_material.strip().strip('"').strip("'")
|
|
64
|
-
|
|
65
|
-
# Handle literal \n sequences
|
|
66
|
-
if "\\n" in key_material:
|
|
67
|
-
key_material = key_material.replace("\\n", "\n")
|
|
68
|
-
|
|
69
|
-
# Ensure proper line endings
|
|
70
|
-
if not key_material.endswith("\n"):
|
|
71
|
-
key_material += "\n"
|
|
72
|
-
|
|
73
|
-
ssh_dir = os.path.expanduser("~/.ssh")
|
|
74
|
-
key_path = os.path.join(ssh_dir, "SallaApp_mageai_CI_CD_deploy")
|
|
75
|
-
os.makedirs(ssh_dir, exist_ok=True)
|
|
76
|
-
os.chmod(ssh_dir, 0o700)
|
|
77
|
-
|
|
78
|
-
with open(key_path, "w") as f:
|
|
79
|
-
f.write(key_material)
|
|
80
|
-
os.chmod(key_path, 0o600)
|
|
81
|
-
|
|
82
|
-
cmd = [
|
|
83
|
-
"git",
|
|
84
|
-
"-c",
|
|
85
|
-
f"core.sshCommand=ssh -i {key_path} -o IdentitiesOnly=yes -o StrictHostKeyChecking=accept-new",
|
|
86
|
-
"pull",
|
|
87
|
-
git_url,
|
|
88
|
-
"master",
|
|
89
|
-
]
|
|
90
|
-
|
|
91
|
-
try:
|
|
92
|
-
os.chdir(repo_path)
|
|
93
|
-
result = subprocess.run(cmd, capture_output=True, text=True, check=True)
|
|
94
|
-
git_output = result.stdout
|
|
95
|
-
git_status = "success"
|
|
96
|
-
except subprocess.CalledProcessError as e:
|
|
97
|
-
git_output = (e.stdout or "") + (e.stderr or "")
|
|
98
|
-
git_status = "error"
|
|
99
|
-
payload["blocks"].append({
|
|
100
|
-
"type": "section",
|
|
101
|
-
"text": {"type": "mrkdwn", "text": f"*Error Output:*\n```{git_output[:1500]}```"} # truncate for Slack limit
|
|
102
|
-
})
|
|
103
|
-
requests.post(webhook_url, json=payload)
|
|
104
|
-
finally:
|
|
105
|
-
try:
|
|
106
|
-
os.remove(key_path)
|
|
107
|
-
except Exception:
|
|
108
|
-
pass
|
|
109
|
-
|
|
110
|
-
return {
|
|
111
|
-
"workspace": workspace_name,
|
|
112
|
-
"repo_path": repo_path,
|
|
113
|
-
"git_pull_status": git_status,
|
|
114
|
-
"git_pull_output": git_output.strip(),
|
|
115
|
-
}
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{salla_gitpuller-0.1.2 → salla_gitpuller-1.0.0}/salla_gitpuller.egg-info/dependency_links.txt
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|