11import json
2- import boto3
3- import typer
42import os
53import subprocess
4+ import boto3
5+ import typer
6+ from github import Github
67
78from devolv .drift .aws_fetcher import get_aws_policy_document , merge_policy_documents , build_superset_policy
89from devolv .drift .issues import create_approval_issue , wait_for_sync_choice
910from devolv .drift .github_approvals import create_github_pr
10- from devolv .drift .report import detect_and_print_drift
11- from github import Github # Needed for auto-close
1211
1312app = typer .Typer ()
1413
1514def push_branch (branch_name : str ):
16- import subprocess
17- import typer
18-
1915 try :
20- # Create or switch to branch safely
2116 subprocess .run (["git" , "checkout" , "-B" , branch_name ], check = True )
22-
23- # Ensure Git identity is set
2417 subprocess .run (["git" , "config" , "user.email" , "github-actions@users.noreply.github.com" ], check = True )
2518 subprocess .run (["git" , "config" , "user.name" , "github-actions" ], check = True )
26-
27- # Add, commit
2819 subprocess .run (["git" , "add" , "." ], check = True )
2920 subprocess .run (["git" , "commit" , "-m" , f"Update policy: { branch_name } " ], check = True )
3021
31- # Try pushing
3222 try :
3323 subprocess .run (["git" , "push" , "--set-upstream" , "origin" , branch_name ], check = True )
3424 except subprocess .CalledProcessError :
@@ -42,6 +32,22 @@ def push_branch(branch_name: str):
4232 typer .echo (f"❌ Git command failed: { e } " )
4333 raise typer .Exit (1 )
4434
35+ def detect_drift (local_doc , aws_doc ) -> bool :
36+ """Detect if AWS permissions exist that are missing in local (removal drift)."""
37+ local_statements = {json .dumps (stmt , sort_keys = True ) for stmt in local_doc .get ("Statement" , [])}
38+ aws_statements = {json .dumps (stmt , sort_keys = True ) for stmt in aws_doc .get ("Statement" , [])}
39+
40+ missing_in_local = aws_statements - local_statements
41+
42+ if missing_in_local :
43+ typer .echo ("❌ Drift detected: Your local policy is missing some permissions present in AWS." )
44+ for stmt in missing_in_local :
45+ typer .echo (stmt )
46+ return True
47+
48+ typer .echo ("✅ No drift detected (local may have extra permissions; that's fine)." )
49+ return False
50+
4551@app .command ()
4652def drift (
4753 policy_name : str = typer .Option (..., "--policy-name" , help = "Name of the IAM policy" ),
@@ -63,25 +69,25 @@ def drift(
6369 raise typer .Exit (1 )
6470
6571 aws_doc = get_aws_policy_document (policy_arn )
66- drift = detect_and_print_drift (local_doc , aws_doc )
6772
68- if not drift and not approval_anyway :
69- typer .echo ("✅ No drift detected. Use --approval-anyway to force approval." )
73+ drift_detected = detect_drift (local_doc , aws_doc )
74+
75+ if not drift_detected and not approval_anyway :
76+ typer .echo ("✅ No drift and no forced approval requested." )
7077 raise typer .Exit ()
7178
7279 repo_full_name = repo_full_name or os .getenv ("GITHUB_REPOSITORY" )
80+ token = os .getenv ("GITHUB_TOKEN" )
81+
7382 if not repo_full_name :
7483 typer .echo ("❌ GitHub repo not specified. Use --repo or set GITHUB_REPOSITORY." )
7584 raise typer .Exit (1 )
76-
77- token = os .getenv ("GITHUB_TOKEN" )
7885 if not token :
7986 typer .echo ("❌ GITHUB_TOKEN not set in environment." )
8087 raise typer .Exit (1 )
8188
8289 assignees = [a .strip () for a in approvers .split ("," ) if a .strip ()]
8390 issue_num , _ = create_approval_issue (repo_full_name , token , policy_name , assignees = assignees )
84- typer .echo (f"✅ Created issue #{ issue_num } in { repo_full_name } : https://github.com/{ repo_full_name } /issues/{ issue_num } " )
8591
8692 choice = wait_for_sync_choice (repo_full_name , issue_num , token )
8793 iam = boto3 .client ("iam" )
@@ -115,15 +121,10 @@ def _update_aws_policy(iam, policy_arn, policy_doc):
115121 )
116122
117123def _update_local_and_create_pr (doc , policy_file , repo_full_name , policy_name , issue_num , token , description = "" ):
118- import json
119- from github import Github
120- from devolv .drift .github_approvals import create_github_pr
121-
122124 new_content = json .dumps (doc , indent = 2 )
123125 with open (policy_file , "w" ) as f :
124126 f .write (new_content )
125127
126- # Clean branch name
127128 branch = (
128129 f"{ description .replace (' ' , '-' ).replace ('+' , 'plus' ).replace ('/' , '-' )} -policy-{ policy_name } "
129130 .strip ("-" )
@@ -135,16 +136,10 @@ def _update_local_and_create_pr(doc, policy_file, repo_full_name, policy_name, i
135136 pr_title = f"Update { policy_file } { description } " .strip ()
136137 pr_body = f"This PR updates `{ policy_file } ` { description } .\n \n Linked to issue #{ issue_num } ." .strip ()
137138
138- # ✅ Pass correct branch name
139139 pr_num , pr_url = create_github_pr (repo_full_name , branch , pr_title , pr_body , issue_num = issue_num )
140140
141- #typer.echo(f"✅ Created PR #{pr_num}: {pr_url}")
142-
143- # ✅ Auto-close issue immediately
144141 gh = Github (token )
145142 repo = gh .get_repo (repo_full_name )
146143 issue = repo .get_issue (number = issue_num )
147144 issue .create_comment (f"✅ PR created and linked: { pr_url } . Closing issue." )
148145 issue .edit (state = "closed" )
149-
150-
0 commit comments