augint-github 1.0.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,40 @@
1
+ Metadata-Version: 2.3
2
+ Name: augint-github
3
+ Version: 1.0.0
4
+ Summary: GitHub tools for Augmenting Integrations
5
+ License: AGPL-3.0-only
6
+ Author: Samuel Vange
7
+ Author-email: 7166607+svange@users.noreply.github.com
8
+ Requires-Python: >=3.12,<4.0
9
+ Classifier: License :: OSI Approved :: GNU Affero General Public License v3
10
+ Classifier: Programming Language :: Python :: 3
11
+ Classifier: Programming Language :: Python :: 3.12
12
+ Classifier: Programming Language :: Python :: 3.13
13
+ Requires-Dist: click (>=8.1.8,<9.0.0)
14
+ Requires-Dist: loguru (>=0.7.3,<0.8.0)
15
+ Requires-Dist: pygithub (>=2.5.0,<3.0.0)
16
+ Requires-Dist: python-dotenv (>=1.0.1,<2.0.0)
17
+ Requires-Dist: rich (>=13.9.4,<14.0.0)
18
+ Project-URL: Homepage, https://github.com/svange/augint-github
19
+ Description-Content-Type: text/markdown
20
+
21
+ # Augmenting Integrations GitHub Tools
22
+ ![ci status](https://github.com/svange/augint-github/actions/workflows/pipeline.yaml/badge.svg?branch=main)
23
+
24
+ ![PyPI - Version](https://img.shields.io/pypi/v/openbrain)
25
+ [![License: AGPL v3](https://img.shields.io/badge/License-AGPL_v3-blue.svg)](https://www.gnu.org/licenses/agpl-3.0)[![Ruff](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/astral-sh/ruff/main/assets/badge/v2.json)](https://github.com/astral-sh/ruff)
26
+ <a href="https://github.com/psf/black"><img alt="Code style: black" src="https://img.shields.io/badge/code%20style-black-000000.svg"></a>
27
+ [![Conventional Commits](https://img.shields.io/badge/Conventional%20Commits-1.0.0-yellow.svg?style=flat-square)](https://conventionalcommits.org)
28
+ [![pre-commit](https://img.shields.io/badge/pre--commit-enabled-brightgreen?style=flat-square&logo=pre-commit&logoColor=white)](https://github.com/pre-commit/pre-commit)
29
+ [![Made with GH Actions](https://img.shields.io/badge/CI-GitHub_Actions-blue?logo=github-actions&logoColor=white)](https://github.com/features/actions "Go to GitHub Actions homepage")
30
+ [![semantic-release](https://img.shields.io/badge/%20%20%F0%9F%93%A6%F0%9F%9A%80-semantic--release-e10079.svg)](https://github.com/semantic-release/semantic-release)
31
+
32
+ # Variables and Secrets
33
+ Push variables and secrets from .env to GH Actions.
34
+
35
+
36
+ Here is a draft for a `README.md` section that provides a general overview of the `LeadmoApiV1` class, its capabilities, and usage examples:
37
+
38
+ ---
39
+
40
+
@@ -0,0 +1,19 @@
1
+ # Augmenting Integrations GitHub Tools
2
+ ![ci status](https://github.com/svange/augint-github/actions/workflows/pipeline.yaml/badge.svg?branch=main)
3
+
4
+ ![PyPI - Version](https://img.shields.io/pypi/v/openbrain)
5
+ [![License: AGPL v3](https://img.shields.io/badge/License-AGPL_v3-blue.svg)](https://www.gnu.org/licenses/agpl-3.0)[![Ruff](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/astral-sh/ruff/main/assets/badge/v2.json)](https://github.com/astral-sh/ruff)
6
+ <a href="https://github.com/psf/black"><img alt="Code style: black" src="https://img.shields.io/badge/code%20style-black-000000.svg"></a>
7
+ [![Conventional Commits](https://img.shields.io/badge/Conventional%20Commits-1.0.0-yellow.svg?style=flat-square)](https://conventionalcommits.org)
8
+ [![pre-commit](https://img.shields.io/badge/pre--commit-enabled-brightgreen?style=flat-square&logo=pre-commit&logoColor=white)](https://github.com/pre-commit/pre-commit)
9
+ [![Made with GH Actions](https://img.shields.io/badge/CI-GitHub_Actions-blue?logo=github-actions&logoColor=white)](https://github.com/features/actions "Go to GitHub Actions homepage")
10
+ [![semantic-release](https://img.shields.io/badge/%20%20%F0%9F%93%A6%F0%9F%9A%80-semantic--release-e10079.svg)](https://github.com/semantic-release/semantic-release)
11
+
12
+ # Variables and Secrets
13
+ Push variables and secrets from .env to GH Actions.
14
+
15
+
16
+ Here is a draft for a `README.md` section that provides a general overview of the `LeadmoApiV1` class, its capabilities, and usage examples:
17
+
18
+ ---
19
+
@@ -0,0 +1,237 @@
1
+ [tool.poetry]
2
+ name = "augint-github"
3
+ version = "1.0.0"
4
+ description = "GitHub tools for Augmenting Integrations"
5
+ authors = ["Samuel Vange <7166607+svange@users.noreply.github.com>"]
6
+ readme = "README.md"
7
+ license = "AGPL-3.0-only"
8
+ homepage = "https://github.com/svange/augint-github"
9
+ packages = [
10
+ { include = "*", from = "src" },
11
+ ]
12
+
13
+ [tool.poetry.scripts]
14
+ ai-gh-push = "gh_secrets_and_vars_async:cli"
15
+
16
+ [tool.poetry.plugins."commitizen.plugin"] # completely untested
17
+ cz_conventional_commits = "commitizen.cz.conventional_commits:ConventionalCommitsCz"
18
+ #cz_jira = "commitizen.cz.jira:JiraSmartCz"
19
+ cz_customize = "commitizen.cz.customize:CustomizeCommitsCz"
20
+ cargo = "commitizen.providers:CargoProvider"
21
+ commitizen = "commitizen.providers:CommitizenProvider"
22
+ composer = "commitizen.providers:ComposerProvider"
23
+ npm = "commitizen.providers:NpmProvider"
24
+ pep621 = "commitizen.providers:Pep621Provider"
25
+ poetry = "commitizen.providers:PoetryProvider"
26
+ scm = "commitizen.providers:ScmProvider"
27
+
28
+
29
+ [tool.poetry.group.dev.dependencies]
30
+ pytest = "^8.3.4"
31
+ pytest-html = "^4.1.1"
32
+ pytest-cov = "^6.0.0"
33
+
34
+ [tool.semantic_release]
35
+ major_on_zero = false
36
+ commit_message = "chore(release): release {version}"
37
+
38
+ [tool.semantic_release.branches.main]
39
+ match = "main"
40
+ #commit_message = "chore(release): release {version}"
41
+ prerelease = false
42
+
43
+ [tool.semantic_release.branches.dev]
44
+ match = "dev"
45
+ #commit_message = "chore(pre-release): release {version}"
46
+ prerelease_token = "dev"
47
+ prerelease = true
48
+
49
+ version_toml = [
50
+ "pyproject.toml:tool.poetry.version",
51
+ ]
52
+ version_variable = [
53
+ "__init__.py:__version__",
54
+ ]
55
+ build_command = "pip install poetry && poetry build"
56
+ #logging_use_named_masks = true
57
+ #major_on_zero = true
58
+ tag_format = "v{version}"
59
+ #exclude_commit_patterns = [
60
+ # 'skip: ',
61
+ #]
62
+
63
+ [tool.semantic_release.publish]
64
+ dist_glob_patterns = ["dist/*"]
65
+ upload_to_vcs_release = true
66
+ type = "github"
67
+
68
+ [tool.semantic_release.remote.token]
69
+ env = "GITHUB_TOKEN"
70
+ fallback_env = "GH_TOKEN"
71
+
72
+
73
+
74
+ [tool.commitizen]
75
+ name = "cz_conventional_commits"
76
+ tag_format = "v$version"
77
+ version_scheme = "SemVer"
78
+ version_provider = "poetry" #"scm"
79
+ update_changelog_on_bump = true
80
+ major_version_zero = true
81
+ #version = "3.9.1"
82
+ #version_files = [
83
+ # "pyproject.toml:version",
84
+ # "openbrain/__version__.py",
85
+ # ".pre-commit-config.yaml:rev:.+Openbrain",
86
+ #]
87
+
88
+ [tool.poetry.plugins."commitizen.scheme"]
89
+ pep440 = "commitizen.version_schemes:Pep440"
90
+ semver = "commitizen.version_schemes:SemVer"
91
+
92
+ [tool.poetry.dependencies]
93
+ python = "^3.12"
94
+ click = "^8.1.8"
95
+ python-dotenv = "^1.0.1"
96
+ rich = "^13.9.4"
97
+ loguru = "^0.7.3"
98
+ pygithub = "^2.5.0"
99
+
100
+
101
+ [tool.coverage] # not tested
102
+ [tool.coverage.report]
103
+ show_missing = true
104
+ exclude_lines = [
105
+ # Have to re-enable the standard pragma
106
+ 'pragma: no cover',
107
+ # Don't complain about missing debug-only code:
108
+ 'def __repr__',
109
+ 'if self\.debug',
110
+ # Don't complain if tests don't hit defensive assertion code:
111
+ 'raise AssertionError',
112
+ 'raise NotImplementedError',
113
+ # Don't complain if non-runnable code isn't run:
114
+ 'if 0:',
115
+ 'if __name__ == .__main__.:',
116
+ 'if TYPE_CHECKING:',
117
+ ]
118
+ omit = [
119
+ 'env/*',
120
+ 'venv/*',
121
+ '.venv/*',
122
+ '*/virtualenv/*',
123
+ '*/virtualenvs/*',
124
+ '*/tests/*',
125
+ ]
126
+
127
+ [tool.pytest.ini_options]
128
+ addopts = '-m "not redundant and not skip_ci" --html=report.html --self-contained-html'
129
+ log_cli = true
130
+ log_cli_level = "INFO"
131
+ log_format = "%(asctime)s %(levelname)s %(message)s"
132
+ log_date_format = "%Y-%m-%d %H:%M:%S"
133
+ log_file = "pytest.log"
134
+ log_file_level = "INFO"
135
+ #environment_table_redact_list = ".*key.*"
136
+ #render_collapsed = "failed,error"
137
+ markers = [
138
+ "redudant: mark test as redundant",
139
+ "end_to_end: mark test as an end-to-end test, meant for running in CI/CD against a live environment",
140
+ "no_infra: mark test as not requiring infrastructure, intended for running in CI/CD against local web server (to the runner)",
141
+ "skip_ci: mark as not suitable for ci",
142
+ ]
143
+
144
+ [tool.ruff]
145
+ line-length = 130
146
+ lint.ignore = [
147
+ "E501",
148
+ "D1",
149
+ "D415"
150
+ ]
151
+ # Enable pycodestyle (`E`) and Pyflakes (`F`) codes by default.
152
+ lint.select = ["E", "F"]
153
+
154
+ [tool.lint.isort]
155
+ known-first-party = ["commitizen", "tests"]
156
+
157
+ [tool.lint.pydocstyle]
158
+ convention = "restructuredtext"
159
+
160
+ [tool.mypy]
161
+ files = "commitizen"
162
+ disallow_untyped_decorators = true
163
+ disallow_subclassing_any = true
164
+ warn_return_any = true
165
+ warn_redundant_casts = true
166
+ warn_unused_ignores = true
167
+ warn_unused_configs = true
168
+
169
+ [[tool.mypy.overrides]]
170
+ module = "py.*" # Legacy pytest dependencies
171
+ ignore_missing_imports = true
172
+
173
+ [tool.black]
174
+ line-length = 130
175
+ target-version = ['py312']
176
+ include = '\.pyi?$'
177
+ exclude = '''
178
+ /(
179
+ \.git
180
+ | \.hg
181
+ | \.mypy_cache
182
+ | \.tox
183
+ | \.venv
184
+ | _build
185
+ | buck-out
186
+ | build
187
+ )/
188
+ '''
189
+
190
+ [tool.flake8]
191
+ max-line-length = 130
192
+ extend-ignore = ["D203", "E203", "E251", "E266", "E302", "E305", "E401", "E402", "E501", "F401", "F403", "W503"]
193
+ exclude = [".git", "__pycache__", "dist"]
194
+ max-complexity = 10
195
+
196
+ [tool.isort]
197
+ atomic = true
198
+ profile = "black"
199
+ line_length = 130
200
+ skip_gitignore = true
201
+
202
+ # Allow autofix for all enabled rules (when `--fix`) is provided.
203
+ fixable = ["A", "B", "C", "D", "E", "F", "G", "I", "N", "Q", "S", "T", "W", "ANN", "ARG", "BLE", "COM", "DJ", "DTZ", "EM", "ERA", "EXE", "FBT", "ICN", "INP", "ISC", "NPY", "PD", "PGH", "PIE", "PL", "PT", "PTH", "PYI", "RET", "RSE", "RUF", "SIM", "SLF", "TCH", "TID", "TRY", "UP", "YTT"]
204
+ unfixable = []
205
+
206
+ # Exclude a variety of commonly ignored directories.
207
+ exclude = [
208
+ ".bzr",
209
+ ".direnv",
210
+ ".eggs",
211
+ ".git",
212
+ ".git-rewrite",
213
+ ".hg",
214
+ ".mypy_cache",
215
+ ".nox",
216
+ ".pants.d",
217
+ ".pytype",
218
+ ".ruff_cache",
219
+ ".svn",
220
+ ".tox",
221
+ ".venv",
222
+ "__pypackages__",
223
+ "_build",
224
+ "buck-out",
225
+ "build",
226
+ "dist",
227
+ "node_modules",
228
+ "venv",
229
+ ]
230
+
231
+ # Same as Black.
232
+ line-length = 130
233
+
234
+ # Allow unused variables when underscore-prefixed.
235
+ dummy-variable-rgx = "^(_+|(_+[a-zA-Z0-9_]*[a-zA-Z0-9]+?))$"
236
+
237
+ target-version = "py312"
File without changes
@@ -0,0 +1,178 @@
1
+ import asyncio
2
+ import os
3
+ from pathlib import Path
4
+
5
+ import click
6
+ import github.GithubException
7
+ from dotenv import load_dotenv
8
+ from github import Github, Auth
9
+ from github.Repository import Repository
10
+ from github.GithubException import UnknownObjectException
11
+ from rich import print
12
+ from loguru import logger
13
+
14
+
15
+ @click.command()
16
+ @click.option("--verbose", "-v", is_flag=True, help="Print all the output.")
17
+ @click.option("--dry-run", "-d", is_flag=True, help="Run through the process, but make no changes to GitHub.")
18
+ @click.argument("filename", type=click.Path(exists=True, readable=True), default=".env")
19
+ def cli(verbose: bool, dry_run: bool, filename: click.Path):
20
+ """
21
+ Update GitHub repository secrets and environment variables from a .env file. Requires the values for `GH_REPO`, `GH_ACCOUNT`, and `GH_TOKEN` in your .env file. This script finds sensitive key/value pairs in FILENAME (by looking for substrins like KEY, TOKEN, SECRET, etc. in the key name) and updates the GitHub repository secrets with them. It then updates the GitHub repository environment variables with the remaining key/value pairs.
22
+ """
23
+
24
+ if dry_run:
25
+ logger.info("Dry run mode enabled. No changes will be made to GitHub.")
26
+ results = asyncio.run(perform_update(filename, verbose, dry_run))
27
+ if verbose:
28
+ print(results)
29
+ total_secrets = len(results["SECRETS"])
30
+ total_vars = len(results["VARIABLES"])
31
+ print(f"Updated {total_secrets} secrets and {total_vars} variables.")
32
+
33
+
34
+ async def perform_update(filename: click.Path, verbose: bool = False, dry_run: bool = False):
35
+ """
36
+ Performs the update of the GitHub repository secrets and environment variables.
37
+ :param filename: file to use for secrets, variables, and github repo
38
+ :return:
39
+ """
40
+ if not filename:
41
+ raise ValueError("No filename specified. Exiting to avoid an accident.")
42
+
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)
46
+
47
+ file_path = Path(filename.__str__())
48
+ # Read the .env file and convert it to a JSON object
49
+ secrets = {}
50
+ not_secrets = {}
51
+ SECRETS_INDICATORS = ["secret", "key", "token", "bearer", "password", "pass", "pwd", "pword", "hash"]
52
+ logger.debug(f"Reading file {file_path}")
53
+
54
+ with file_path.open("r") as file:
55
+ for line in file:
56
+ line = line.strip()
57
+ if not line or line.startswith("#") or line.startswith(";") or "=" not in line:
58
+ continue
59
+ key, value = line.strip().split("=", 1)
60
+
61
+ match key:
62
+ case key if key.startswith("AWS_PROFILE"):
63
+ continue
64
+ case key if any(indicator in key.casefold() for indicator in SECRETS_INDICATORS):
65
+ secrets[key] = value
66
+ case _:
67
+ not_secrets[key] = value
68
+
69
+ try:
70
+ repo = get_github_repo(github_account, github_repo)
71
+ except github.GithubException as e:
72
+ logger.critical(f"Repo {github_repo} not found. Ensure GH_REPO and GH_ACCOUNT are in your env file.")
73
+ exit(1)
74
+
75
+ secret_update_result = await create_or_update_github_secrets(repo=repo, env_data=secrets, dry_run=dry_run)
76
+ not_secret_update_result = await create_or_update_github_variables(repo=repo, env_data=not_secrets, dry_run=dry_run)
77
+
78
+ results = {"SECRETS": secret_update_result, "VARIABLES": not_secret_update_result}
79
+
80
+ return results
81
+
82
+
83
+ def get_github_repo(github_account, github_repo_name) -> Repository:
84
+ """
85
+ Get the GitHub repository object.
86
+ :param github_account: Name of your GitHub account (i.e. your username), or the name of the organization.
87
+ :param github_repo_name: Name of the repository.
88
+ :return: a GitHub repository object
89
+ """
90
+ auth = Auth.Token(os.environ.get("GH_TOKEN", None))
91
+ g = Github(auth=auth)
92
+ try:
93
+ repo = g.get_user(github_account).get_repo(github_repo_name)
94
+ except UnknownObjectException as e:
95
+ logger.critical(e)
96
+ repo = g.get_organization(github_account).get_repo(github_repo_name)
97
+ logger.critical("You must add GH_USER to your env file.")
98
+
99
+ return repo
100
+
101
+
102
+ async def create_or_update_github_secrets(repo, env_data, dry_run: bool = False):
103
+ """
104
+ Create or update GitHub repository secrets.
105
+ :param dry_run: run through the process, but make no changes to GitHub
106
+ :param repo: the repository on which to operate
107
+ :param env_data: the secrets and their values in a dictionary
108
+ :return: list of results
109
+ """
110
+ secrets = await asyncio.to_thread(repo.get_secrets)
111
+ secret_names = [secret.name for secret in secrets]
112
+ dry_run_prefix = "[DRY RUN] " if dry_run else ""
113
+
114
+ tasks = []
115
+ for env_var_name, env_var_value in env_data.items():
116
+ # Create or update secrets
117
+ if env_var_name in secret_names:
118
+ logger.info(f"{dry_run_prefix}Updating secret {env_var_name}...")
119
+ tasks.append(asyncio.to_thread(repo.create_secret, env_var_name, env_var_value))
120
+ else:
121
+ logger.info(f"{dry_run_prefix}Creating secret {env_var_name}...")
122
+ tasks.append(asyncio.to_thread(repo.create_secret, env_var_name, env_var_value))
123
+
124
+ for secret_name in secret_names:
125
+ if secret_name not in env_data.keys():
126
+ logger.info(f"{dry_run_prefix}Deleting secret {secret_name}...")
127
+ tasks.append(asyncio.to_thread(repo.delete_secret, secret_name))
128
+
129
+ if dry_run:
130
+ results = [secret for secret in secrets]
131
+ else:
132
+ results = await asyncio.gather(*tasks)
133
+ return results
134
+
135
+
136
+ async def create_or_update_github_variables(repo, env_data, dry_run: bool = False):
137
+ """
138
+ Create or update GitHub repository environment variables.
139
+ :param repo: the repository on which to operate
140
+ :param dry_run: run through the process, but make no changes to GitHub
141
+ :param env_data: the secrets and their values in a dictionary
142
+ :return: list of results
143
+ """
144
+ vars = await asyncio.to_thread(repo.get_variables)
145
+ var_names = [var.name for var in vars]
146
+ dry_run_prefix = "[DRY RUN] " if dry_run else ""
147
+
148
+ tasks = []
149
+ for env_var_name, env_var_value in env_data.items():
150
+ # Create or update secrets
151
+ if env_var_name in var_names:
152
+ logger.info(f"{dry_run_prefix}Updating variable {env_var_name}...")
153
+
154
+ def delete_then_create_variable(repo, env_var_name, env_var_value):
155
+ repo.delete_variable(env_var_name)
156
+ return repo.create_variable(env_var_name, env_var_value)
157
+
158
+ # tasks.append(asyncio.to_thread(repo.delete_variable, env_var_name))
159
+ tasks.append(asyncio.to_thread(delete_then_create_variable, repo, env_var_name, env_var_value))
160
+ else:
161
+ logger.info(f"{dry_run_prefix}Creating variable {env_var_name}...")
162
+ tasks.append(asyncio.to_thread(repo.create_variable, env_var_name, env_var_value))
163
+
164
+ for var_name in var_names:
165
+ if var_name not in env_data.keys():
166
+ logger.info(f"{dry_run_prefix}Deleting variable {var_name}...")
167
+ tasks.append(asyncio.to_thread(repo.delete_variable, var_name))
168
+
169
+ if dry_run:
170
+ results = [var for var in vars]
171
+ else:
172
+ results = await asyncio.gather(*tasks)
173
+
174
+ return results
175
+
176
+
177
+ if __name__ == "__main__":
178
+ cli()