-
Notifications
You must be signed in to change notification settings - Fork 44
Expand file tree
/
Copy pathverify.py
More file actions
518 lines (431 loc) · 18.8 KB
/
Copy pathverify.py
File metadata and controls
518 lines (431 loc) · 18.8 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
import sys
import os
import requests
import time
from typing import Dict, List, Optional, Tuple
from dotenv import load_dotenv
def _get_github_api(
endpoint: str, headers: Dict[str, str], owner: str, repo: str
) -> Tuple[bool, Optional[Dict]]:
"""Make a GET request to GitHub API and return (success, response)."""
url = f"https://api.github.com/repos/{owner}/{repo}/{endpoint}"
try:
response = requests.get(url, headers=headers)
if response.status_code == 200:
return True, response.json()
elif response.status_code == 404:
return False, None
else:
print(f"API error for {endpoint}: {response.status_code}", file=sys.stderr)
return False, None
except Exception as e:
print(f"Exception for {endpoint}: {e}", file=sys.stderr)
return False, None
def _search_github_issues(
query: str, headers: Dict[str, str]
) -> Tuple[bool, Optional[List]]:
"""Search GitHub issues using the search API."""
url = f"https://api.github.com/search/issues?q={query}&per_page=100"
try:
response = requests.get(url, headers=headers)
if response.status_code == 200:
data = response.json()
return True, data.get("items", [])
else:
print(f"Search API error: {response.status_code}", file=sys.stderr)
return False, None
except Exception as e:
print(f"Search exception: {e}", file=sys.stderr)
return False, None
def _wait_for_workflow_completion(
headers: Dict[str, str], owner: str, repo: str, max_wait: int = 90
) -> bool:
"""Wait for GitHub Actions workflows to complete processing."""
print("⏳ Waiting for deployment status workflows to complete...")
start_time = time.time()
no_workflow_check_count = 0
while time.time() - start_time < max_wait:
try:
# Check workflow runs for deployment-status.yml
success, response = _get_github_api(
"actions/workflows/deployment-status.yml/runs?per_page=10",
headers,
owner,
repo,
)
if success and response:
runs = response.get("workflow_runs", [])
if len(runs) > 0:
# Check status of recent runs
running_count = 0
completed_count = 0
failed_count = 0
for run in runs[:3]: # Check recent runs
status = run["status"]
conclusion = run.get("conclusion")
if status == "completed":
completed_count += 1
if conclusion == "failure":
failed_count += 1
elif status in ["in_progress", "queued"]:
running_count += 1
print(
f" Status: {completed_count} completed, {running_count} running/queued"
)
# Wait until NO workflows are running
if running_count == 0:
if failed_count > 0:
print(
f"⚠️ Warning: {failed_count} workflow runs failed, but continuing verification..."
)
print(
f"✅ All workflows completed. Found {completed_count} completed runs."
)
# Additional wait to ensure all processing is done
print(
"⏳ Additional wait for deployment processing to complete..."
)
time.sleep(5)
return True
else:
# No workflow runs found
no_workflow_check_count += 1
if no_workflow_check_count == 1:
print(
" No workflow runs found yet, waiting 5 seconds and checking once more..."
)
time.sleep(5)
continue
elif no_workflow_check_count >= 2:
print(
"⚠️ No workflow runs detected after 2 checks. Workflow may not have been triggered."
)
print(" Continuing with verification...")
return False
print(f"⏳ Still waiting... ({int(time.time() - start_time)}s elapsed)")
time.sleep(5)
except Exception as e:
print(f"⚠️ Error checking workflow status: {e}")
time.sleep(5)
print(f"⚠️ Workflow completion wait timed out after {max_wait}s")
return False
def _verify_workflow_runs(
headers: Dict[str, str], owner: str, repo: str
) -> Tuple[bool, List[str], Optional[Dict]]:
"""Verify that the deployment status workflow runs have the correct jobs."""
print("\n⚙️ Verifying deployment status workflow runs...")
errors = []
# Get the most recent workflow run
success, response = _get_github_api(
"actions/workflows/deployment-status.yml/runs?per_page=5",
headers,
owner,
repo,
)
if not success or not response:
return False, ["Failed to fetch workflow runs"], None
runs = response.get("workflow_runs", [])
if not runs:
return False, ["No workflow runs found for deployment-status.yml"], None
# Find the most recent successful run
latest_successful_run = None
for run in runs:
if run.get("conclusion") == "success":
latest_successful_run = run
break
if not latest_successful_run:
return False, ["No successful workflow runs found"], None
run_id = latest_successful_run["id"]
print(f" Found successful workflow run #{run_id}")
# Get jobs for this run
success, jobs_response = _get_github_api(
f"actions/runs/{run_id}/jobs", headers, owner, repo
)
if not success:
return False, ["Failed to fetch workflow jobs"], None
jobs = jobs_response.get("jobs", [])
expected_jobs = ["pre-deployment", "rollback-preparation", "post-deployment"]
found_jobs = [job["name"] for job in jobs]
missing_jobs = [job for job in expected_jobs if job not in found_jobs]
if missing_jobs:
errors.append(f"Missing jobs: {missing_jobs}. Found: {found_jobs}")
else:
print(f" ✅ All 3 required jobs found: {found_jobs}")
# Verify all jobs succeeded
failed_jobs = [job["name"] for job in jobs if job["conclusion"] != "success"]
if failed_jobs:
errors.append(f"Failed jobs: {failed_jobs}")
else:
print(" ✅ All jobs completed successfully")
# Verify sequential execution (each job should start after the previous one)
if len(jobs) >= 3:
job_times = {}
for job in jobs:
if job["name"] in expected_jobs and job["started_at"]:
job_times[job["name"]] = job["started_at"]
if len(job_times) >= 3:
# Check that jobs ran in correct sequence
import datetime
times = {
name: datetime.datetime.fromisoformat(time.replace("Z", "+00:00"))
for name, time in job_times.items()
}
# pre-deployment should start first
# rollback-preparation should start after pre-deployment
# post-deployment should start after rollback-preparation
if all(job in times for job in expected_jobs):
if (
times["rollback-preparation"] <= times["pre-deployment"]
or times["post-deployment"] <= times["rollback-preparation"]
):
errors.append("Jobs did not run in correct sequential order")
else:
print(" ✅ Jobs ran in correct sequential order")
else:
errors.append(
"Not enough job timing data to verify sequential execution"
)
return len(errors) == 0, errors, latest_successful_run
def _verify_deployment_issue(
run_data: Dict, headers: Dict[str, str], owner: str, repo: str
) -> Tuple[bool, List[str]]:
"""Verify that a deployment tracking issue was created and closed properly."""
print("\n📋 Verifying deployment tracking issue...")
errors = []
# Extract commit SHA from the workflow run
head_sha = run_data.get("head_sha")
if not head_sha:
return False, ["Could not determine head SHA from workflow run"]
short_sha = head_sha[:7]
expected_title = f"Deployment Tracking - {short_sha}"
# Search for the deployment tracking issue
success, issues = _search_github_issues(
f'repo:{owner}/{repo} "{expected_title}" is:issue', headers
)
if not success:
return False, ["Failed to search for deployment tracking issue"]
# Find the exact issue
deployment_issue = None
for issue in issues:
if issue.get("title") == expected_title:
deployment_issue = issue
break
if not deployment_issue:
return False, [f"Deployment tracking issue '{expected_title}' not found"]
issue_number = deployment_issue["number"]
print(f" Found deployment tracking issue #{issue_number}: {expected_title}")
# Check that issue is closed
if deployment_issue.get("state") != "closed":
errors.append(
f"Deployment issue #{issue_number} is not closed (state: {deployment_issue.get('state')})"
)
else:
print(f" ✅ Deployment issue #{issue_number} is closed")
# Check required labels
expected_labels = ["deployment", "completed"]
actual_labels = [label["name"] for label in deployment_issue.get("labels", [])]
missing_labels = [label for label in expected_labels if label not in actual_labels]
if missing_labels:
errors.append(
f"Missing labels on deployment issue: {missing_labels}. Found: {actual_labels}"
)
else:
print(f" ✅ Required labels found: {expected_labels}")
# Get issue comments to verify GitHub Actions bot comments
success, comments = _get_github_api(
f"issues/{issue_number}/comments", headers, owner, repo
)
if not success:
errors.append("Failed to get deployment issue comments")
return len(errors) == 0, errors
# Filter for GitHub Actions bot comments only
bot_comments = [
comment
for comment in comments
if comment.get("user", {}).get("login") == "github-actions[bot]"
]
if not bot_comments:
errors.append("No comments found from GitHub Actions bot")
return len(errors) == 0, errors
print(f" Found {len(bot_comments)} comment(s) from GitHub Actions bot")
# Get all bot comment bodies
bot_comment_bodies = [comment.get("body", "") for comment in bot_comments]
all_bot_comments = " ".join(bot_comment_bodies)
# Check for required GitHub Actions bot comment indicators
required_comment_indicators = [
"Pre-deployment checks completed",
"🔄 Rollback Plan Ready",
"Deployment Completed Successfully",
]
for indicator in required_comment_indicators:
if indicator not in all_bot_comments:
errors.append(
f"Missing required GitHub Actions bot comment indicator: '{indicator}'"
)
else:
print(f" ✅ Found GitHub Actions bot comment indicator: '{indicator}'")
# Find and verify the rollback plan comment from GitHub Actions bot
rollback_comment = None
for comment in bot_comments:
if "🔄 Rollback Plan Ready" in comment.get("body", ""):
rollback_comment = comment.get("body", "")
break
if rollback_comment:
print(" ✅ Found rollback plan comment from GitHub Actions bot")
# Check for required rollback plan elements
# Use flexible matching: accept both markdown bold ("**Key**:") and plain text ("Key:")
import re as _re
def _flex_match(comment: str, keyword: str) -> bool:
"""Match 'keyword:' with optional markdown bold wrapping."""
pattern = r"(\*\*\s*)?" + _re.escape(keyword) + r"(\s*\*\*)?\s*:"
return bool(_re.search(pattern, comment))
field_keywords = [
"Previous Commit",
"Current Commit",
"Package Version",
"SHA256",
"Artifact",
]
for kw in field_keywords:
if _flex_match(rollback_comment, kw):
print(f" ✅ Found rollback plan field: '{kw}'")
else:
errors.append(f"Missing field in rollback plan: '{kw}'")
# Check for at least 5 checkmarks (✅) with rollback-related keywords
# Accept any reasonable wording as long as the semantic component is mentioned
rollback_component_keywords = [
r"rollback\s+script",
r"configuration\s+backup|config.*backup",
r"dependency\s+verification|dependency.*check",
r"rollback\s+documentation|rollback.*doc",
r"rollback\s+package|compressed.*package",
]
checkmark_lines = [
line.strip()
for line in rollback_comment.split("\n")
if "✅" in line
]
matched_components = 0
for kw_pattern in rollback_component_keywords:
for line in checkmark_lines:
if _re.search(kw_pattern, line, _re.IGNORECASE):
matched_components += 1
print(f" ✅ Found rollback component checkmark matching: '{kw_pattern}'")
break
if matched_components < 5:
errors.append(
f"Expected at least 5 rollback component checkmarks (✅), found {matched_components}"
)
else:
print(f" ✅ All 5 rollback component checkmarks found")
# Check for Quick Rollback Commands section
if "Quick Rollback Command" in rollback_comment or "rollback command" in rollback_comment.lower():
print(" ✅ Found rollback commands section")
else:
errors.append("Missing 'Quick Rollback Commands' section in rollback plan")
# Verify commit SHAs in rollback comment
# Accept both "**Current Commit**: sha" and "Current Commit: sha"
current_sha_pattern = r"(?:\*\*\s*)?Current\s+Commit(?:\s*\*\*)?\s*:\s*" + _re.escape(head_sha)
if _re.search(current_sha_pattern, rollback_comment):
print(f" ✅ Current commit SHA verified: {head_sha}")
else:
errors.append(
f"Current commit SHA {head_sha} not found in rollback comment"
)
# Extract and verify previous commit SHA
prev_sha_pattern = r"(?:\*\*\s*)?Previous\s+Commit(?:\s*\*\*)?\s*:\s*([a-f0-9]{40})"
prev_sha_match = _re.search(prev_sha_pattern, rollback_comment)
if prev_sha_match:
prev_sha = prev_sha_match.group(1)
print(f" ✅ Previous commit SHA found: {prev_sha}")
else:
errors.append(
"Previous commit SHA (40-char hex) not found in rollback comment"
)
# Verify SHA256 checksum is present
sha256_pattern = r"(?:\*\*\s*)?SHA256(?:\s*\*\*)?\s*:\s*([a-f0-9]{64})"
sha256_match = _re.search(sha256_pattern, rollback_comment)
if sha256_match:
sha256_value = sha256_match.group(1)
print(f" ✅ SHA256 checksum found: {sha256_value[:16]}...")
else:
errors.append(
"SHA256 checksum not found or invalid format in rollback comment"
)
else:
errors.append("Rollback plan comment not found from GitHub Actions bot")
return len(errors) == 0, errors
def verify() -> bool:
"""
Verify that the deployment status workflow automation is working correctly.
"""
# Load environment variables
load_dotenv(".mcp_env")
github_token = os.environ.get("MCP_GITHUB_TOKEN")
if not github_token:
print("Error: MCP_GITHUB_TOKEN environment variable not set", file=sys.stderr)
return False
# Get GitHub organization
github_org = os.environ.get("GITHUB_EVAL_ORG")
if not github_org:
print("Error: GITHUB_EVAL_ORG environment variable not set", file=sys.stderr)
return False
# Repository configuration
owner = github_org
repo = "mcpmark-cicd"
headers = {
"Authorization": f"token {github_token}",
"Accept": "application/vnd.github.v3+json",
}
print("🔍 Starting Deployment Status Workflow Verification")
print("=" * 60)
# Wait for workflows to complete
workflows_completed = _wait_for_workflow_completion(headers, owner, repo)
if not workflows_completed:
print(
"⚠️ Warning: Workflows may still be running. Continuing with verification..."
)
# Verify workflow runs and jobs
all_passed = True
# 1. Verify workflow runs have correct jobs
runs_ok, runs_errors, run_data = _verify_workflow_runs(headers, owner, repo)
if not runs_ok:
all_passed = False
print("❌ Workflow Runs Verification Failed:")
for error in runs_errors:
print(f" - {error}")
else:
print("✅ Workflow Runs Verification Passed")
# 2. Verify deployment issue if workflow runs passed
if run_data:
issue_ok, issue_errors = _verify_deployment_issue(
run_data, headers, owner, repo
)
if not issue_ok:
all_passed = False
print("❌ Deployment Issue Verification Failed:")
for error in issue_errors:
print(f" - {error}")
else:
print("✅ Deployment Issue Verification Passed")
print("\n" + "=" * 60)
if all_passed:
print("🎉 All Deployment Status Workflow verifications PASSED!")
print("\n📋 Summary:")
print(
" ✅ Workflow runs with correct 3 sequential jobs: pre-deployment, rollback-preparation, post-deployment"
)
print(" ✅ Deployment tracking issue created and closed with proper labels")
print(" ✅ Issue contains rollback plan with all required elements")
print(" ✅ Previous and current commit SHAs are correctly tracked")
print(" ✅ All workflow automation comments are present")
print(
"\n🤖 The GitHub Actions deployment status workflow is working correctly!"
)
else:
print("❌ Deployment Status Workflow verification FAILED!")
print(" Some components did not meet the expected automation requirements.")
return all_passed
if __name__ == "__main__":
success = verify()
sys.exit(0 if success else 1)