Created
August 20, 2025 13:39
-
-
Save cmdr2/7261109b0e214942f1c7864024a6daa1 to your computer and use it in GitHub Desktop.
Utility script to easily setup a GitHub deploy action in a repository for pushing to S3 or Lambda. Creates the required IAM roles automatically.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/usr/bin/env python3 | |
import os | |
import subprocess | |
import json | |
import time | |
def run_capture(cmd): | |
print(f"Running: {' '.join(cmd)}") | |
result = subprocess.run(cmd, check=True, capture_output=True, text=True) | |
return result.stdout | |
def run(cmd): | |
print(f"Running: {' '.join(cmd)}") | |
subprocess.run(cmd, check=True) | |
def get_github_info(): | |
github_repo = input("Enter GitHub repo (e.g., cmdr2/carbon, or full URL): ").strip() | |
github_branch = input("Enter GitHub branch [default=main]: ").strip() or "main" | |
if github_repo.startswith("http://") or github_repo.startswith("https://"): | |
parts = github_repo.rstrip("/").split("/") | |
owner, repo = parts[-2], parts[-1].replace(".git", "") | |
else: | |
owner, repo = github_repo.split("/") | |
return owner, repo, github_branch | |
def get_account_id(): | |
output = run_capture(["aws", "sts", "get-caller-identity", "--query", "Account", "--output", "text"]) | |
return output.strip() | |
def create_policy_and_role(base_name, policy_doc, trust_doc): | |
policy_file = f"/tmp/{base_name}-policy.json" | |
with open(policy_file, "w") as f: | |
json.dump(policy_doc, f, indent=2) | |
output = run_capture( | |
["aws", "iam", "create-policy", "--policy-name", base_name, "--policy-document", f"file://{policy_file}"] | |
) | |
policy_arn = json.loads(output)["Policy"]["Arn"] | |
trust_file = f"/tmp/{base_name}-trust.json" | |
with open(trust_file, "w") as f: | |
json.dump(trust_doc, f, indent=2) | |
output = run_capture( | |
["aws", "iam", "create-role", "--role-name", base_name, "--assume-role-policy-document", f"file://{trust_file}"] | |
) | |
role_arn = json.loads(output)["Role"]["Arn"] | |
print("Waiting 5 seconds for IAM policy to propagate...") | |
time.sleep(5) | |
run(["aws", "iam", "attach-role-policy", "--role-name", base_name, "--policy-arn", policy_arn]) | |
return role_arn | |
def create_trust_policy(owner, repo, branch, account_id): | |
return { | |
"Version": "2012-10-17", | |
"Statement": [ | |
{ | |
"Effect": "Allow", | |
"Principal": { | |
"Federated": f"arn:aws:iam::{account_id}:oidc-provider/token.actions.githubusercontent.com" | |
}, | |
"Action": "sts:AssumeRoleWithWebIdentity", | |
"Condition": { | |
"StringEquals": { | |
"token.actions.githubusercontent.com:aud": "sts.amazonaws.com", | |
"token.actions.githubusercontent.com:sub": f"repo:{owner}/{repo}:ref:refs/heads/{branch}", | |
} | |
}, | |
} | |
], | |
} | |
def write_workflow(branch, role_var, deploy_step): | |
workflow_dir = ".github/workflows" | |
os.makedirs(workflow_dir, exist_ok=True) | |
workflow_file = os.path.join(workflow_dir, "deploy.yml") | |
workflow_yaml = f"""name: deploy | |
run-name: Deployment | |
on: | |
push: | |
branches: | |
- {branch} | |
jobs: | |
deploy: | |
runs-on: ubuntu-latest | |
permissions: | |
id-token: write | |
contents: read | |
steps: | |
- name: Checkout | |
uses: actions/checkout@v3 | |
- name: Configure AWS Credentials | |
uses: aws-actions/configure-aws-credentials@v4 | |
with: | |
role-to-assume: ${{{{ vars.{role_var} }}}} | |
aws-region: us-east-1 | |
{deploy_step} | |
""" | |
with open(workflow_file, "w") as f: | |
f.write(workflow_yaml) | |
return workflow_file | |
def setup_s3(owner, repo, branch, account_id): | |
s3_path = input("Enter S3 path to deploy to (e.g., my-bucket-name/some/path): ").strip() | |
bucket_name = s3_path.split("/")[0] | |
base_name = f"{owner}-{repo}-github-deploy-to-s3" | |
policy_doc = { | |
"Version": "2012-10-17", | |
"Statement": [ | |
{ | |
"Action": ["s3:PutObject", "s3:PutObjectAcl"], | |
"Effect": "Allow", | |
"Resource": [f"arn:aws:s3:::{s3_path}/*"], | |
}, | |
{"Action": ["s3:ListBucket"], "Effect": "Allow", "Resource": [f"arn:aws:s3:::{bucket_name}"]}, | |
], | |
} | |
trust_doc = create_trust_policy(owner, repo, branch, account_id) | |
role_arn = create_policy_and_role(base_name, policy_doc, trust_doc) | |
deploy_step = f""" - name: Copy files to S3 | |
run: | | |
aws s3 sync . "s3://{s3_path}" --exclude ".git/*" --exclude ".github/*" --exclude "LICENSE" --exclude "README.md" --exclude "*.sh" --acl public-read | |
shell: bash""" | |
workflow_file = write_workflow(branch, "S3_DEPLOY_ROLE", deploy_step) | |
print("\n✅ S3 setup complete.") | |
print(f"Workflow written: {workflow_file}. Please customize it as necessary.") | |
print(f"**IMPORTANT:** Set GitHub repo variable S3_DEPLOY_ROLE to {role_arn}") | |
def setup_lambda(owner, repo, branch, account_id): | |
function_name = input("Enter Lambda function name: ").strip() | |
base_name = f"{owner}-{repo}-github-deploy-to-lambda" | |
policy_doc = { | |
"Version": "2012-10-17", | |
"Statement": [ | |
{ | |
"Action": ["lambda:UpdateFunctionCode"], | |
"Effect": "Allow", | |
"Resource": [f"arn:aws:lambda:us-east-1:{account_id}:function:{function_name}"], | |
} | |
], | |
} | |
trust_doc = create_trust_policy(owner, repo, branch, account_id) | |
role_arn = create_policy_and_role(base_name, policy_doc, trust_doc) | |
deploy_step = f""" - name: Zip and Upload Lambda | |
run: | | |
zip -r function.zip . -x ".git/*" ".github/*" "LICENSE" "README.md" "*.sh" | |
aws lambda update-function-code --function-name {function_name} --zip-file fileb://function.zip | |
shell: bash""" | |
workflow_file = write_workflow(branch, "LAMBDA_DEPLOY_ROLE", deploy_step) | |
print("\n✅ Lambda setup complete.") | |
print(f"Workflow written: {workflow_file}. Please customize it as necessary.") | |
print(f"**IMPORTANT:** Set GitHub repo variable LAMBDA_DEPLOY_ROLE to {role_arn}") | |
def main(): | |
if not os.path.exists(".git"): | |
print("This script must be run from a Git repository.") | |
return | |
print("Choose publish destination:") | |
print("1. S3") | |
print("2. Lambda") | |
choice = input("Enter number: ").strip() | |
owner, repo, branch = get_github_info() | |
account_id = get_account_id() | |
if choice == "1": | |
setup_s3(owner, repo, branch, account_id) | |
elif choice == "2": | |
setup_lambda(owner, repo, branch, account_id) | |
else: | |
print("Invalid choice. Exiting.") | |
if __name__ == "__main__": | |
main() |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment