Coverage for src / gh_secrets_and_vars_async / push.py: 45%
97 statements
« prev ^ index » next coverage.py v7.13.5, created at 2026-04-16 17:56 +0000
« prev ^ index » next coverage.py v7.13.5, created at 2026-04-16 17:56 +0000
1import asyncio
2import os
3from pathlib import Path
5import click
6import github.GithubException
7from dotenv import load_dotenv
8from loguru import logger
9from rich import print
11from .common import configure_logging, get_github_repo
14@click.command("sync")
15@click.option("--verbose", "-v", is_flag=True, help="Print all the output.")
16@click.option(
17 "--dry-run", "-d", is_flag=True, help="Run through the process, but make no changes to GitHub."
18)
19@click.argument("filename", type=click.Path(exists=True, readable=True), default=".env")
20def push_command(verbose: bool, dry_run: bool, filename: click.Path):
21 """Push .env secrets and variables to a GitHub repository.
23 Requires GH_REPO, GH_ACCOUNT, and GH_TOKEN in your .env file.
24 Sensitive keys (containing TOKEN, SECRET, KEY, etc.) become GitHub secrets;
25 all others become GitHub variables.
26 """
27 configure_logging(verbose)
28 if dry_run:
29 logger.info("Dry run mode enabled. No changes will be made to GitHub.")
30 results = asyncio.run(perform_update(filename, verbose, dry_run))
31 if verbose:
32 print(results)
33 total_secrets = len(results["SECRETS"])
34 total_vars = len(results["VARIABLES"])
35 print(f"Updated {total_secrets} secrets and {total_vars} variables.")
38async def perform_update(filename, verbose: bool = False, dry_run: bool = False):
39 """Perform the update of GitHub repository secrets and environment variables."""
40 if not filename:
41 raise ValueError("No filename specified. Exiting to avoid an accident.")
43 load_dotenv(str(filename), override=True)
44 github_repo = os.environ.get("GH_REPO", None)
45 github_account = os.environ.get("GH_ACCOUNT", None)
47 file_path = Path(filename.__str__())
48 secrets = {}
49 not_secrets = {}
50 SECRETS_INDICATORS = [
51 "secret",
52 "key",
53 "token",
54 "bearer",
55 "password",
56 "pass",
57 "pwd",
58 "pword",
59 "hash",
60 ]
61 logger.debug(f"Reading file {file_path}")
63 with file_path.open("r") as file:
64 for line in file:
65 line = line.strip()
66 if not line or line.startswith("#") or line.startswith(";") or "=" not in line:
67 continue
68 key, value = line.strip().split("=", 1)
70 match key:
71 case key if key.startswith("AWS_PROFILE"):
72 continue
73 case key if any(indicator in key.casefold() for indicator in SECRETS_INDICATORS):
74 secrets[key] = value
75 case _:
76 not_secrets[key] = value
78 try:
79 repo = get_github_repo(github_account or "", github_repo or "")
80 except github.GithubException:
81 logger.critical(
82 f"Repo {github_repo} not found. Ensure GH_REPO and GH_ACCOUNT are in your env file."
83 )
84 exit(1)
86 secret_update_result = await create_or_update_github_secrets(
87 repo=repo, env_data=secrets, dry_run=dry_run
88 )
89 not_secret_update_result = await create_or_update_github_variables(
90 repo=repo, env_data=not_secrets, dry_run=dry_run
91 )
93 results = {"SECRETS": secret_update_result, "VARIABLES": not_secret_update_result}
95 return results
98async def create_or_update_github_secrets(repo, env_data, dry_run: bool = False):
99 """Create or update GitHub repository secrets."""
100 secrets = await asyncio.to_thread(repo.get_secrets)
101 secret_names = [secret.name for secret in secrets]
102 dry_run_prefix = "[DRY RUN] " if dry_run else ""
104 tasks = []
105 for env_var_name, env_var_value in env_data.items():
106 if env_var_name in secret_names:
107 logger.info(f"{dry_run_prefix}Updating secret {env_var_name}...")
108 tasks.append(asyncio.to_thread(repo.create_secret, env_var_name, env_var_value))
109 else:
110 logger.info(f"{dry_run_prefix}Creating secret {env_var_name}...")
111 tasks.append(asyncio.to_thread(repo.create_secret, env_var_name, env_var_value))
113 for secret_name in secret_names:
114 if secret_name not in env_data.keys():
115 logger.info(f"{dry_run_prefix}Deleting secret {secret_name}...")
116 tasks.append(asyncio.to_thread(repo.delete_secret, secret_name))
118 if dry_run:
119 results = list(secrets)
120 else:
121 results = await asyncio.gather(*tasks)
122 return results
125async def create_or_update_github_variables(repo, env_data, dry_run: bool = False):
126 """Create or update GitHub repository environment variables."""
127 vars = await asyncio.to_thread(repo.get_variables)
128 var_names = [var.name for var in vars]
129 dry_run_prefix = "[DRY RUN] " if dry_run else ""
131 tasks = []
132 for env_var_name, env_var_value in env_data.items():
133 if env_var_name in var_names:
134 logger.info(f"{dry_run_prefix}Updating variable {env_var_name}...")
136 def delete_then_create_variable(repo, env_var_name, env_var_value):
137 repo.delete_variable(env_var_name)
138 return repo.create_variable(env_var_name, env_var_value)
140 tasks.append(
141 asyncio.to_thread(delete_then_create_variable, repo, env_var_name, env_var_value)
142 )
143 else:
144 logger.info(f"{dry_run_prefix}Creating variable {env_var_name}...")
145 tasks.append(asyncio.to_thread(repo.create_variable, env_var_name, env_var_value))
147 for var_name in var_names:
148 if var_name not in env_data.keys():
149 logger.info(f"{dry_run_prefix}Deleting variable {var_name}...")
150 tasks.append(asyncio.to_thread(repo.delete_variable, var_name))
152 if dry_run:
153 results = list(vars)
154 else:
155 results = await asyncio.gather(*tasks)
157 return results