Skip to content

Commit 2171b76

Browse files
committed
testing new version
1 parent 8479805 commit 2171b76

4 files changed

Lines changed: 78 additions & 42 deletions

File tree

devolv/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,2 @@
1-
__version__ = "0.2.27"
1+
__version__ = "0.2.28"
22

devolv/drift/cli.py

Lines changed: 21 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -39,24 +39,18 @@ def push_branch(branch_name: str):
3939
raise typer.Exit(1)
4040

4141
def detect_drift(local_doc, aws_doc) -> bool:
42-
"""Detect removal drift: AWS has permissions missing from local (danger)."""
4342
local_statements = {json.dumps(s, sort_keys=True) for s in local_doc.get("Statement", [])}
4443
aws_statements = {json.dumps(s, sort_keys=True) for s in aws_doc.get("Statement", [])}
4544

4645
missing_in_local = aws_statements - local_statements
4746

4847
if missing_in_local:
4948
typer.echo("❌ Drift detected: Local is missing permissions present in AWS.")
50-
# No need to print each JSON line — rich diff will handle details
5149
return True
5250

5351
typer.echo("✅ No removal drift detected (local may have extra permissions; that's fine).")
5452
return False
5553

56-
57-
typer.echo("✅ No removal drift detected (local may have extra permissions; that's fine).")
58-
return False
59-
6054
@app.command()
6155
def drift(
6256
policy_name: str = typer.Option(..., "--policy-name", help="Name of the IAM policy"),
@@ -102,7 +96,11 @@ def drift(
10296
raise typer.Exit(1)
10397

10498
assignees = [a.strip() for a in approvers.split(",") if a.strip()]
105-
issue_num, _ = create_approval_issue(repo_full_name, token, policy_name, assignees=assignees)
99+
100+
# 💡 Pass drift_detected to dynamically control issue body
101+
issue_num, _ = create_approval_issue(
102+
repo_full_name, token, policy_name, assignees=assignees, drift_detected=drift_detected
103+
)
106104
issue_url = f"https://github.com/{repo_full_name}/issues/{issue_num}"
107105
typer.echo(f"✅ Approval issue created: {issue_url}")
108106

@@ -122,6 +120,22 @@ def drift(
122120
typer.echo(f"✅ AWS policy {policy_arn} updated with superset of local + AWS.")
123121
_update_local_and_create_pr(superset_doc, policy_file, repo_full_name, policy_name, issue_num, token, "with superset of local + AWS")
124122

123+
elif choice == "accept":
124+
typer.echo("✅ Approval received: no changes required. Closing issue.")
125+
gh = Github(token)
126+
repo = gh.get_repo(repo_full_name)
127+
issue = repo.get_issue(number=issue_num)
128+
issue.create_comment("✅ Approved current state. No action needed.")
129+
issue.edit(state="closed")
130+
131+
elif choice == "reject":
132+
typer.echo("❌ Approval rejected: no changes made. Closing issue.")
133+
gh = Github(token)
134+
repo = gh.get_repo(repo_full_name)
135+
issue = repo.get_issue(number=issue_num)
136+
issue.create_comment("❌ Rejected current state. No action taken.")
137+
issue.edit(state="closed")
138+
125139
else:
126140
typer.echo("⏭ No synchronization performed (skip).")
127141

devolv/drift/github_approvals.py

Lines changed: 35 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -17,26 +17,48 @@ def _get_github_repo(repo_full_name: str):
1717
gh = Github(_get_github_token())
1818
return gh.get_repo(repo_full_name)
1919

20-
def create_github_issue(repo: str, title: str, body: str, assignees: list) -> tuple:
20+
def create_github_issue(repo: str, policy_name: str, assignees: list, drift_detected: bool = True) -> tuple:
2121
"""
22-
Create a GitHub issue and return (number, url)
22+
Create a GitHub issue with dynamic body based on drift_detected.
23+
Returns (issue number, issue URL)
2324
"""
2425
try:
2526
repo_obj = _get_github_repo(repo)
27+
approver_list = ", ".join([f"@{a}" for a in assignees]) if assignees else "anyone"
28+
29+
title = f"Approval needed for IAM policy: {policy_name}"
30+
if drift_detected:
31+
body = (
32+
f"Please review and approve the sync for `{policy_name}`.\n\n"
33+
f"✅ **Allowed approvers:** {approver_list}\n\n"
34+
"**Reply with one of the following commands to proceed:**\n"
35+
"- `local->aws` → Apply local policy changes to AWS\n"
36+
"- `aws->local` → Update local policy file from AWS\n"
37+
"- `aws<->local` → Sync both ways (superset, update AWS + local)\n"
38+
"- `skip` → Skip this sync"
39+
)
40+
else:
41+
body = (
42+
f"Please review and approve the current state for `{policy_name}` (✅ No drift detected).\n\n"
43+
f"✅ **Allowed approvers:** {approver_list}\n\n"
44+
"**Reply with one of the following commands to proceed:**\n"
45+
"- `accept` → Approve the current state, no further action\n"
46+
"- `reject` → Reject the current state (no changes will be made)"
47+
)
48+
2649
issue = repo_obj.create_issue(title=title, body=body, assignees=assignees)
2750
print(f"✅ Created issue #{issue.number} in {repo}: {issue.html_url}")
2851
return issue.number, issue.html_url
52+
2953
except Exception as e:
3054
print(f"❌ Failed to create issue in {repo}: {e}")
3155
raise
3256

3357
def create_github_pr(repo: str, head_branch: str, title: str, body: str, base: str = "main", issue_num: int = None) -> tuple:
3458
"""
35-
Create a GitHub PR. If issue_num is provided, comment on the issue.
36-
Return (PR number, PR URL).
59+
Create a GitHub PR. Optionally comments on linked issue.
60+
Returns (PR number, PR URL)
3761
"""
38-
from github import Github
39-
4062
try:
4163
repo_obj = _get_github_repo(repo)
4264
pr = repo_obj.create_pull(
@@ -45,35 +67,29 @@ def create_github_pr(repo: str, head_branch: str, title: str, body: str, base: s
4567
head=head_branch,
4668
base=base
4769
)
48-
#print(f"✅ Created PR #{pr.number} in {repo}: {pr.html_url}")
4970

5071
if issue_num:
5172
issue = repo_obj.get_issue(number=issue_num)
52-
issue.create_comment(f"A PR has been created for this sync: {pr.html_url}")
53-
73+
issue.create_comment(f"✅ PR created and linked: {pr.html_url}")
74+
75+
print(f"✅ Created PR #{pr.number} in {repo}: {pr.html_url}")
5476
return pr.number, pr.html_url
5577

5678
except Exception as e:
57-
print(f"❌ Failed to create PR: {e}")
79+
print(f"❌ Failed to create PR in {repo}: {e}")
5880
raise
5981

6082
def push_branch(branch_name: str):
61-
import subprocess
62-
import typer
63-
83+
"""
84+
Create, commit to, and push a git branch, rebasing if needed.
85+
"""
6486
try:
65-
# Create or switch to branch safely
6687
subprocess.run(["git", "checkout", "-B", branch_name], check=True)
67-
68-
# Ensure Git identity is set
6988
subprocess.run(["git", "config", "user.email", "github-actions@users.noreply.github.com"], check=True)
7089
subprocess.run(["git", "config", "user.name", "github-actions"], check=True)
71-
72-
# Add, commit
7390
subprocess.run(["git", "add", "."], check=True)
7491
subprocess.run(["git", "commit", "-m", f"Update policy: {branch_name}"], check=True)
7592

76-
# Try pushing
7793
try:
7894
subprocess.run(["git", "push", "--set-upstream", "origin", branch_name], check=True)
7995
except subprocess.CalledProcessError:
@@ -86,5 +102,3 @@ def push_branch(branch_name: str):
86102
except subprocess.CalledProcessError as e:
87103
typer.echo(f"❌ Git command failed: {e}")
88104
raise typer.Exit(1)
89-
90-

devolv/drift/issues.py

Lines changed: 21 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,33 +1,41 @@
11
from github import Github
22
import time
33

4-
def create_approval_issue(repo_full_name, token, policy_name, assignees=None):
4+
def create_approval_issue(repo_full_name, token, policy_name, assignees=None, drift_detected=True):
55
gh = Github(token)
66
repo = gh.get_repo(repo_full_name)
77

88
approver_list = ", ".join([f"@{a}" for a in assignees]) if assignees else "anyone"
99

1010
title = f"Approval needed for IAM policy: {policy_name}"
11-
body = (
12-
f"Please review and approve the sync for `{policy_name}`.\n\n"
13-
f"✅ **Allowed approvers:** {approver_list}\n\n"
14-
"**Reply with one of the following commands to proceed:**\n"
15-
"- `local->aws` → Apply local policy changes to AWS\n"
16-
"- `aws->local` → Update local policy file from AWS\n"
17-
"- `aws<->local` → Sync both ways (superset, update AWS + local)\n"
18-
"- `skip` → Skip this sync"
19-
)
11+
12+
if drift_detected:
13+
body = (
14+
f"Please review and approve the sync for `{policy_name}`.\n\n"
15+
f"✅ **Allowed approvers:** {approver_list}\n\n"
16+
"**Reply with one of the following commands to proceed:**\n"
17+
"- `local->aws` → Apply local policy changes to AWS\n"
18+
"- `aws->local` → Update local policy file from AWS\n"
19+
"- `aws<->local` → Sync both ways (superset, update AWS + local)\n"
20+
"- `skip` → Skip this sync"
21+
)
22+
else:
23+
body = (
24+
f"Please review and approve the current state for `{policy_name}` (✅ No drift detected).\n\n"
25+
f"✅ **Allowed approvers:** {approver_list}\n\n"
26+
"**Reply with one of the following commands to proceed:**\n"
27+
"- `accept` → Approve the current state, no further action\n"
28+
"- `reject` → Reject the current state (no changes will be made)"
29+
)
2030

2131
issue = repo.create_issue(
2232
title=title,
2333
body=body,
2434
assignees=assignees or []
2535
)
2636

27-
#print(f"✅ Created issue #{issue.number} in {repo_full_name}: {issue.html_url}")
2837
return issue.number, issue.html_url
2938

30-
3139
def wait_for_sync_choice(repo_full_name, issue_number, token, allowed_approvers=None):
3240
g = Github(token)
3341
repo = g.get_repo(repo_full_name)
@@ -45,7 +53,7 @@ def wait_for_sync_choice(repo_full_name, issue_number, token, allowed_approvers=
4553
print(f"Ignoring comment from unauthorized user: {commenter}")
4654
continue
4755

48-
if content in ["local->aws", "aws->local", "aws<->local", "skip"]:
56+
if content in ["local->aws", "aws->local", "aws<->local", "skip","accept", "reject"]:
4957
return content
5058

5159
print("Waiting for approval comment...")

0 commit comments

Comments
 (0)