diff --git a/.bumpversion.cfg b/.bumpversion.cfg new file mode 100644 index 0000000..1223f49 --- /dev/null +++ b/.bumpversion.cfg @@ -0,0 +1,11 @@ +[bumpversion] +current_version = 0.0.4 +commit = True +tag = True + +[bumpversion:file:setup.py] +search = version="{current_version}" +replace = version="{new_version}" + +[bdist_wheel] +universal = 1 diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml new file mode 100644 index 0000000..d2d45d1 --- /dev/null +++ b/.github/workflows/deploy.yml @@ -0,0 +1,56 @@ +name: Upload Python Package + +on: + release: + types: [created] + +jobs: + deploy: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + with: + fetch-depth: 0 + + - name: Run semver-diff + id: semver-diff + uses: tj-actions/semver-diff@v1.2.0 + + - name: Set up Python + uses: actions/setup-python@v2 + with: + python-version: '3.6.x' + + - name: Upgrade pip + run: pip install -U pip + + - name: Install dependencies + run: make install-deploy + + - name: Setup git + run: | + git config --local user.email "github-actions[bot]@users.noreply.github.com" + git config --local user.name "github-actions[bot]" + + - name: bumpversion + run: | + make increase-version PART="${{ steps.semver-diff.outputs.release_type }}" + + - name: Build and publish + run: make release + env: + TWINE_USERNAME: ${{ secrets.PYPI_USERNAME }} + TWINE_PASSWORD: ${{ secrets.PYPI_PASSWORD }} + + - name: Generate CHANGELOG + uses: tj-actions/github-changelog-generator@v1.8 + + - name: Create Pull Request + uses: peter-evans/create-pull-request@v3 + with: + base: "main" + title: "Upgraded ${{ steps.semver-diff.outputs.old_version }} → ${{ steps.semver-diff.outputs.new_version }}" + branch: "chore/upgrade-${{ steps.semver-diff.outputs.old_version }}-to-${{ steps.semver-diff.outputs.new_version }}" + commit-message: "Upgraded from ${{ steps.semver-diff.outputs.old_version }} → ${{ steps.semver-diff.outputs.new_version }}" + body: "View [CHANGES](https://github.com/${{ github.repository }}/compare/${{ steps.semver-diff.outputs.old_version }}...${{ steps.semver-diff.outputs.new_version }})" + token: ${{ secrets.PAT_TOKEN }} diff --git a/.github/workflows/python-publish.yml b/.github/workflows/python-publish.yml deleted file mode 100644 index 1a03a7b..0000000 --- a/.github/workflows/python-publish.yml +++ /dev/null @@ -1,31 +0,0 @@ -# This workflow will upload a Python Package using Twine when a release is created -# For more information see: https://help.github.com/en/actions/language-and-framework-guides/using-python-with-github-actions#publishing-to-package-registries - -name: Upload Python Package - -on: - release: - types: [created] - -jobs: - deploy: - - runs-on: ubuntu-latest - - steps: - - uses: actions/checkout@v2 - - name: Set up Python - uses: actions/setup-python@v2 - with: - python-version: '3.x' - - name: Install dependencies - run: | - python -m pip install --upgrade pip - pip install setuptools wheel twine - - name: Build and publish - env: - TWINE_USERNAME: ${{ secrets.PYPI_USERNAME }} - TWINE_PASSWORD: ${{ secrets.PYPI_PASSWORD }} - run: | - python setup.py sdist bdist_wheel - twine upload dist/* diff --git a/.gitignore b/.gitignore index 38dee03..7e58086 100644 --- a/.gitignore +++ b/.gitignore @@ -129,4 +129,6 @@ dmypy.json .pyre/ # Pycharm -.idea/ \ No newline at end of file +.idea/ + +.envrc diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..2904151 --- /dev/null +++ b/Makefile @@ -0,0 +1,59 @@ +# Self-Documented Makefile see https://marmelab.com/blog/2016/02/29/auto-documented-makefile.html + +.DEFAULT_GOAL := help +PART := minor + +# Put it first so that "make" without argument is like "make help". +help: + @awk 'BEGIN {FS = ":.*?## "} /^[a-zA-Z_-]+:.*?## / {printf "\033[36m%-32s-\033[0m %s\n", $$1, $$2}' $(MAKEFILE_LIST) + +guard-%: ## Checks that env var is set else exits with non 0 mainly used in CI; + @if [ -z '${${*}}' ]; then echo 'Environment variable $* not set' && exit 1; fi + +# -------------------------------------------------------- +# ------- Python package (pip) management commands ------- +# -------------------------------------------------------- + +clean: clean-build clean-pyc ## remove all build and Python artifacts + +clean-build: ## remove build artifacts + @rm -fr build/ + @rm -fr dist/ + @rm -fr .eggs/ + @find . -name '*.egg-info' -exec rm -fr {} + + @find . -name '*.egg' -exec rm -f {} + + +clean-pyc: ## remove Python file artifacts + @find . -name '*.pyc' -exec rm -f {} + + @find . -name '*.pyo' -exec rm -f {} + + @find . -name '*~' -exec rm -f {} + + @find . -name '__pycache__' -exec rm -fr {} + || true + +lint: ## check style with flake8 + @flake8 github_deploy + +release: dist ## package and upload a release + @twine upload dist/* + +dist: clean install-deploy ## builds source and wheel package + @pip install twine==3.4.1 + @python setup.py sdist bdist_wheel + +increase-version: guard-PART ## Increase project version + @bump2version $(PART) + @git switch -c main + +install-wheel: clean ## Install wheel + @echo "Installing wheel..." + @pip install wheel + +install: install-wheel ## install the package to the active Python's site-packages + @pip install . + +install-deploy: install-wheel + @pip install -e .'[deploy]' + +migrations: + @python manage.py makemigrations + +.PHONY: clean clean-build clean-pyc dist increase-version install-wheel install install-deploy increase-version lint release migrations diff --git a/README.md b/README.md index eec1346..7079482 100644 --- a/README.md +++ b/README.md @@ -1,30 +1,37 @@ [![PyPI version](https://badge.fury.io/py/github-deploy.svg)](https://badge.fury.io/py/github-deploy) -[![Upload Python Package](https://github.com/tj-python/github-deploy/actions/workflows/python-publish.yml/badge.svg)](https://github.com/tj-python/github-deploy/actions/workflows/python-publish.yml) +[![Upload Python Package](https://github.com/tj-python/github-deploy/actions/workflows/python-publish.yml/badge.svg)](https://github.com/tj-python/github-deploy/actions/workflows/python-publish.yml) [![Downloads](https://pepy.tech/badge/github-deploy)](https://pepy.tech/project/github-deploy) # github-deploy ## Problem Using [poly repositories](https://github.com/joelparkerhenderson/monorepo_vs_polyrepo#what-is-polyrepo) to manage projects ? -This can introduce a number challenges one of which is maintaining consistency across multiple repositories for files like shared configurations in your organization without introducing git submodules. +This can introduce a number challenges one of which is maintaining consistency across multiple repositories, for files like shared configurations without introducing git submodules or mono repositories which requires a more complex deployment configuration. > For example adding a github action or maintaing a consistent pull request template accross your organization. +## Solution + +`github-deploy` makes maintaining such configurations as easy as a single command. + +**Alais** : `gh-deploy` + + ## Usage ### Creating or Updating files on github ```shell script -github-deploy update --org [org] --token [PAT_TOKEN] --dest [LOCATION TO UPLOAD FILE] --source [SOURCE FILE LOCATION] +gh-deploy update --org [org] --token [PAT_TOKEN] --dest [LOCATION TO UPLOAD FILE] --source [SOURCE FILE LOCATION] ``` Example: ```shell script -github-deploy update --org tj-actions --token [PAT_TOKEN] --dest '.github/workflows/auto-approve.yml' --source auto-approve.yml +gh-deploy update --org tj-actions --token [PAT_TOKEN] --dest '.github/workflows/auto-approve.yml' --source auto-approve.yml ``` > NOTE: `auto-approve.yml` is located on your local system. @@ -34,15 +41,66 @@ github-deploy update --org tj-actions --token [PAT_TOKEN] --dest '.github/workfl ```shell script -github-deploy delete --org [org] --token [PAT_TOKEN] --dest [LOCATION TO DELETE] +gh-deploy delete --org [org] --token [PAT_TOKEN] --dest [LOCATION TO DELETE] ``` Example: ```shell script -github-deploy delete --org tj-actions --token [PAT_TOKEN] --dest '.github/auto-approve.yml' +gh-deploy delete --org tj-actions --token [PAT_TOKEN] --dest '.github/auto-approve.yml' +``` + + + +## COMMAND +`gh-deploy --help` + +``` +Usage: gh-deploy [OPTIONS] COMMAND [ARGS]... + + Deploy changes to multiple github repositories using a single command. + +Options: + --help Show this message and exit. + +Commands: + delete Delete a file in all repositories owned by an organization/user. + upload Upload a file to all repositories owned by an organization/user. + +``` + +`gh-deploy upload --help` + ``` +Usage: gh-deploy upload [OPTIONS] + Upload a file to all repositories owned by an organization/user. + +Options: + --org TEXT The github organization. + --token TEXT Personal Access token with read and write + access to org. + + --source PATH Source file. + --dest TEXT Destination path. + --overwrite / --no-overwrite Overwrite existing files. + --private / --no-private Upload files to private repositories. + --help Show this message and exit. +``` + +`gh-deploy delete --help` + +``` +Usage: gh-deploy delete [OPTIONS] + + Delete a file in all repositories owned by an organization/user. + +Options: + --org TEXT The github organization. + --token TEXT Personal Access token with read and write access to org. + --dest TEXT Destination path to delete. + --help Show this message and exit. +``` ### Resources - http://www.gigamonkeys.com/mono-vs-multi/ diff --git a/commands/__init__.py b/github_deploy/__init__.py similarity index 100% rename from commands/__init__.py rename to github_deploy/__init__.py diff --git a/github_deploy/commands/__init__.py b/github_deploy/commands/__init__.py new file mode 100644 index 0000000..0260537 --- /dev/null +++ b/github_deploy/commands/__init__.py @@ -0,0 +1 @@ +__path__ = __import__('pkgutil').extend_path(__path__, __name__) \ No newline at end of file diff --git a/constants.py b/github_deploy/commands/_constants.py similarity index 100% rename from constants.py rename to github_deploy/commands/_constants.py diff --git a/github_deploy/commands/_utils.py b/github_deploy/commands/_utils.py new file mode 100644 index 0000000..67cdb6b --- /dev/null +++ b/github_deploy/commands/_utils.py @@ -0,0 +1,6 @@ +def get_repo(*, org, project): + return "{org}/{project}".format(project=project, org=org) + + +def can_upload(*, repo, include_private): + return True if include_private and repo['private'] == True else not repo['private'] diff --git a/commands/delete.py b/github_deploy/commands/delete.py similarity index 91% rename from commands/delete.py rename to github_deploy/commands/delete.py index 301e285..053548b 100644 --- a/commands/delete.py +++ b/github_deploy/commands/delete.py @@ -5,8 +5,8 @@ import asyncclick as click import certifi -from constants import BASE_URL, REPOS_URL -from utils import get_repo +from github_deploy.commands._constants import BASE_URL, REPOS_URL +from github_deploy.commands._utils import get_repo async def get(*, session, url, headers=None, skip_missing=False): @@ -116,12 +116,20 @@ async def handle_file_delete( ) if delete_response: - return "Successfully deleted contents at {repo}/{dest}".format( - repo=repo, - dest=dest, + return click.style( + "Successfully deleted contents at {repo}/{dest}".format( + repo=repo, + dest=dest, + ), + fg="green", + bold=True, ) - return "No content found at {repo}/{dest}".format(repo=repo, dest=dest) + return click.style( + "No content found at {repo}/{dest}".format(repo=repo, dest=dest), + fg="blue", + bold=True, + ) async def list_repos(*, session, org, token): @@ -146,6 +154,7 @@ async def list_repos(*, session, org, token): prompt=click.style("Enter your personal access token", bold=True), help="Personal Access token with read and write access to org.", hide_input=True, + envvar='TOKEN', ) @click.option( "--dest", @@ -188,10 +197,6 @@ async def main(org, token, dest): return for repo in repos: - click.echo( - "Deleting {path} for repository: {repo}...".format(path=dest, repo=repo) - ) - task = asyncio.ensure_future( handle_file_delete( repo=repo, @@ -217,7 +222,7 @@ async def main(org, token, dest): err=True, ) else: - click.echo(click.style(result, fg="green", bold=True)) + click.echo(result) if __name__ == "__main__": diff --git a/commands/update.py b/github_deploy/commands/upload.py similarity index 81% rename from commands/update.py rename to github_deploy/commands/upload.py index 7881fcf..00ff12f 100644 --- a/commands/update.py +++ b/github_deploy/commands/upload.py @@ -7,8 +7,8 @@ import asyncclick as click import certifi -from constants import BASE_URL, REPOS_URL -from utils import get_repo +from github_deploy.commands._constants import BASE_URL, REPOS_URL +from github_deploy.commands._utils import get_repo, can_upload async def get(*, session, url, headers=None, skip_missing=False): @@ -113,8 +113,14 @@ async def handle_file_upload( exists = current_sha is not None if exists and not overwrite: - return "Skipped uploading {source} to {repo}: Found an existing copy.".format( - source=source, repo=repo + return click.style( + "Skipped uploading {source} to {repo}/{path}: Found an existing copy.".format( + source=source, + repo=repo, + path=dest, + ), + fg="blue", + bold=True ) else: @@ -141,10 +147,14 @@ async def handle_file_upload( ) if upload_response: - return "Successfully uploaded '{source}' to {repo}/{dest}".format( - source=upload_response["content"]["name"], - repo=repo, - dest=upload_response["content"]["path"], + return click.style( + "Successfully uploaded '{source}' to {repo}/{dest}".format( + source=upload_response["content"]["name"], + repo=repo, + dest=upload_response["content"]["path"], + ), + fg="green", + bold=True ) @@ -170,6 +180,7 @@ async def list_repos(*, session, org, token): prompt=click.style("Enter your personal access token", bold=True), help="Personal Access token with read and write access to org.", hide_input=True, + envvar='TOKEN', ) @click.option( "--source", @@ -183,10 +194,19 @@ async def list_repos(*, session, org, token): help="Destination path.", ) @click.option( - "--overwrite/--no-overwrite", help="Overwrite existing files.", default=True + "--overwrite/--no-overwrite", + prompt=click.style("Should we overwrite existing contents at this path", fg="blue"), + help="Overwrite existing files.", + default=False, ) -async def main(org, token, source, dest, overwrite): - """Deploy a file to all repositories owned by an organization/user.""" +@click.option( + "--private/--no-private", + prompt=click.style("Should we Include private repositories", bold=True), + help="Upload files to private repositories.", + default=True, +) +async def main(org, token, source, dest, overwrite, private): + """Upload a file to all repositories owned by an organization/user.""" # create instance of Semaphore: max concurrent requests. semaphore = asyncio.Semaphore(1000) @@ -195,13 +215,14 @@ async def main(org, token, source, dest, overwrite): async with aiohttp.ClientSession() as session: response = await list_repos(org=org, token=token, session=session) repos = [ - get_repo(org=org, project=v["name"]) - for v in response["items"] - if not v["archived"] + get_repo(org=org, project=r["name"]) + for r in response["items"] + if not r["archived"] and can_upload(repo=r, include_private=private) ] + repo_type = 'public and private' if private else 'public' click.echo( click.style( - "Found '{}' repositories non archived repositories:".format(len(repos)), + "Found '{}' repositories non archived {} repositories:".format(len(repos), repo_type), fg="green", ) ) @@ -237,10 +258,6 @@ async def main(org, token, source, dest, overwrite): return for repo in repos: - click.echo( - "Uploading {path} to repository {repo}...".format(path=dest, repo=repo) - ) - task = asyncio.ensure_future( handle_file_upload( repo=repo, @@ -268,7 +285,7 @@ async def main(org, token, source, dest, overwrite): err=True, ) else: - click.echo(click.style(result, fg="green", bold=True)) + click.echo(result) if __name__ == "__main__": diff --git a/main.py b/github_deploy/main.py similarity index 93% rename from main.py rename to github_deploy/main.py index 640087a..bec9463 100644 --- a/main.py +++ b/github_deploy/main.py @@ -5,15 +5,15 @@ class GithubDeploy(click.MultiCommand): - + def list_commands(self, ctx): rv = [] for filename in os.listdir(plugin_folder): - if filename.endswith('.py') and not filename.startswith('__init__'): + if filename.endswith('.py') and not filename.startswith('__init__') and not filename.startswith('_'): rv.append(filename[:-3]) rv.sort() return rv - + def get_command(self, ctx, name): ns = {} fn = os.path.join(plugin_folder, name + '.py') diff --git a/setup.py b/setup.py index f294507..95b6c50 100644 --- a/setup.py +++ b/setup.py @@ -1,5 +1,3 @@ -from __future__ import division - import os from setuptools import setup, find_packages @@ -13,17 +11,27 @@ else: LONG_DESCRIPTION = "" +deploy_requires = [ + "bump2version", + "readme_renderer[md]", +] + +extras_require = { + "deploy": deploy_requires, +} + setup( name="github-deploy", - version="0.0.3", + version="0.0.4", description="Deploy yaml files to a large number of repositories in seconds.", long_description=LONG_DESCRIPTION, long_description_content_type=LONG_DESCRIPTION_TYPE, url="https://github.com/tj-python/github-deploy", entry_points={ "console_scripts": [ - "github-deploy=main:main", + "github-deploy=github_deploy.main:main", + "gh-deploy=github_deploy.main:main", ], }, keywords=["yaml", "deploy", "poly repository", "github", "single configuration"], @@ -31,6 +39,8 @@ author_email="jtonye@ymail.com", license="MIT", packages=find_packages(), + python_requires='>=3.6', + extras_require=extras_require, install_requires=[ "asyncclick", "asyncio", diff --git a/utils.py b/utils.py deleted file mode 100644 index 9d90cb5..0000000 --- a/utils.py +++ /dev/null @@ -1,2 +0,0 @@ -def get_repo(*, org, project): - return "{org}/{project}".format(project=project, org=org) pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy