diff --git a/.bumpversion.cfg b/.bumpversion.cfg index 741cf0e..3134a53 100644 --- a/.bumpversion.cfg +++ b/.bumpversion.cfg @@ -1,5 +1,5 @@ [bumpversion] -current_version = 1.0.1 +current_version = 1.0.2 commit = True tag = False diff --git a/.github/workflows/auto-approve.yml b/.github/workflows/auto-approve.yml index d537315..0de2e3d 100644 --- a/.github/workflows/auto-approve.yml +++ b/.github/workflows/auto-approve.yml @@ -7,7 +7,7 @@ jobs: auto-approve: runs-on: ubuntu-latest steps: - - uses: hmarr/auto-approve-action@v2 + - uses: hmarr/auto-approve-action@v3 if: | ( github.event.pull_request.user.login == 'dependabot[bot]' || diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml new file mode 100644 index 0000000..4e26255 --- /dev/null +++ b/.github/workflows/codeql.yml @@ -0,0 +1,41 @@ +name: "CodeQL" + +on: + push: + branches: [ "main" ] + pull_request: + branches: [ "main" ] + schedule: + - cron: "32 0 * * 4" + +jobs: + analyze: + name: Analyze + runs-on: ubuntu-latest + permissions: + actions: read + contents: read + security-events: write + + strategy: + fail-fast: false + matrix: + language: [ python ] + + steps: + - name: Checkout + uses: actions/checkout@v3 + + - name: Initialize CodeQL + uses: github/codeql-action/init@v2 + with: + languages: ${{ matrix.language }} + queries: +security-and-quality + + - name: Autobuild + uses: github/codeql-action/autobuild@v2 + + - name: Perform CodeQL Analysis + uses: github/codeql-action/analyze@v2 + with: + category: "/language:${{ matrix.language }}" diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index b02f205..da3a297 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -43,7 +43,7 @@ jobs: TWINE_PASSWORD: ${{ secrets.PYPI_PASSWORD }} - name: Generate CHANGELOG - uses: tj-actions/github-changelog-generator@v1.15 + uses: tj-actions/github-changelog-generator@v1.17 - name: Create Pull Request uses: peter-evans/create-pull-request@v4 diff --git a/CHANGELOG.md b/CHANGELOG.md index 27a9493..5c2b4fa 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,25 @@ # Changelog +## [v1.0.2](https://github.com/tj-python/github-deploy/tree/v1.0.2) (2022-10-28) + +[Full Changelog](https://github.com/tj-python/github-deploy/compare/v1.0.1...v1.0.2) + +**Merged pull requests:** + +- chore: upgrade required python version to 3.7 [\#27](https://github.com/tj-python/github-deploy/pull/27) ([jackton1](https://github.com/jackton1)) +- chore: reformatted and restructured modules [\#26](https://github.com/tj-python/github-deploy/pull/26) ([jackton1](https://github.com/jackton1)) +- fix: bug with listing repositories [\#25](https://github.com/tj-python/github-deploy/pull/25) ([jackton1](https://github.com/jackton1)) +- Bump pascalgn/automerge-action from 0.15.3 to 0.15.5 [\#24](https://github.com/tj-python/github-deploy/pull/24) ([dependabot[bot]](https://github.com/apps/dependabot)) +- Bump codacy/codacy-analysis-cli-action from 4.1.0 to 4.2.0 [\#23](https://github.com/tj-python/github-deploy/pull/23) ([dependabot[bot]](https://github.com/apps/dependabot)) +- Bump tj-actions/semver-diff from 2.0.0 to 2.1.0 [\#22](https://github.com/tj-python/github-deploy/pull/22) ([dependabot[bot]](https://github.com/apps/dependabot)) +- Bump tj-actions/github-changelog-generator from 1.14 to 1.15 [\#21](https://github.com/tj-python/github-deploy/pull/21) ([dependabot[bot]](https://github.com/apps/dependabot)) +- Bump codacy/codacy-analysis-cli-action from 4.0.2 to 4.1 [\#20](https://github.com/tj-python/github-deploy/pull/20) ([dependabot[bot]](https://github.com/apps/dependabot)) +- Bump tj-actions/github-changelog-generator from 1.13 to 1.14 [\#19](https://github.com/tj-python/github-deploy/pull/19) ([dependabot[bot]](https://github.com/apps/dependabot)) +- Bump codacy/codacy-analysis-cli-action from 1.1.0 to 4.0.2 [\#18](https://github.com/tj-python/github-deploy/pull/18) ([dependabot[bot]](https://github.com/apps/dependabot)) +- chore: Fixed lint errors. [\#17](https://github.com/tj-python/github-deploy/pull/17) ([jackton1](https://github.com/jackton1)) +- Update README.md [\#16](https://github.com/tj-python/github-deploy/pull/16) ([jackton1](https://github.com/jackton1)) +- Upgraded v1.0.0 → v1.0.1 [\#15](https://github.com/tj-python/github-deploy/pull/15) ([jackton1](https://github.com/jackton1)) + ## [v1.0.1](https://github.com/tj-python/github-deploy/tree/v1.0.1) (2022-06-12) [Full Changelog](https://github.com/tj-python/github-deploy/compare/v1.0.0...v1.0.1) diff --git a/README.md b/README.md index 2f9fd94..7e9c63a 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,6 @@ +[![Codacy Badge](https://app.codacy.com/project/badge/Grade/867aeabe457f4367b9e0013b713add6b)](https://www.codacy.com/gh/tj-python/github-deploy/dashboard?utm_source=github.com&utm_medium=referral&utm_content=tj-python/github-deploy&utm_campaign=Badge_Grade) [![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/deploy.yml/badge.svg)](https://github.com/tj-python/github-deploy/actions/workflows/deploy.yml) [![Downloads](https://pepy.tech/badge/github-deploy)](https://pepy.tech/project/github-deploy) -[![Total alerts](https://img.shields.io/lgtm/alerts/g/tj-python/github-deploy.svg?logo=lgtm&logoWidth=18)](https://lgtm.com/projects/g/tj-python/github-deploy/alerts/) -[![Language grade: Python](https://img.shields.io/lgtm/grade/python/g/tj-python/github-deploy.svg?logo=lgtm&logoWidth=18)](https://lgtm.com/projects/g/tj-python/github-deploy/context:python) # github-deploy diff --git a/github_deploy/commands/_constants.py b/github_deploy/commands/_constants.py index 1249c71..ab4c9ba 100644 --- a/github_deploy/commands/_constants.py +++ b/github_deploy/commands/_constants.py @@ -1,2 +1,2 @@ -REPOS_URL = "https://api.github.com/search/repositories?q=org:{org}" -BASE_URL = "https://api.github.com/repos/{repo}/contents/{path}" +REPOS_URL = "https://api.github.com/users/{org}/repos" +FILE_CONTENTS_URL = "https://api.github.com/repos/{repo}/contents/{path}" diff --git a/github_deploy/commands/_repo_utils.py b/github_deploy/commands/_repo_utils.py index 438d970..a7aa389 100644 --- a/github_deploy/commands/_repo_utils.py +++ b/github_deploy/commands/_repo_utils.py @@ -1,9 +1,10 @@ import base64 +import os -import aiofiles import asyncclick as click +from aiofiles import os as aiofiles_os, open as aiofiles_open -from github_deploy.commands._constants import REPOS_URL, BASE_URL +from github_deploy.commands._constants import REPOS_URL, FILE_CONTENTS_URL from github_deploy.commands._http_utils import get, delete, put from github_deploy.commands._utils import get_headers @@ -29,7 +30,7 @@ async def delete_content( if exists: data["sha"] = current_sha - url = BASE_URL.format(repo=repo, path=dest) + url = FILE_CONTENTS_URL.format(repo=repo, path=dest) async with semaphore: response = await delete( @@ -40,7 +41,7 @@ async def delete_content( async def check_exists(*, session, repo, dest, token, semaphore, skip_missing): - url = BASE_URL.format(repo=repo, path=dest) + url = FILE_CONTENTS_URL.format(repo=repo, path=dest) async with semaphore: response = await get( @@ -62,17 +63,50 @@ async def upload_content( token, semaphore, exists, + only_update, current_sha, current_content ): async with semaphore: - async with aiofiles.open(source, mode="rb") as f: + async with aiofiles_open(source, mode="rb") as f: output = await f.read() base64_content = base64.b64encode(output).decode("ascii") if current_content == base64_content: - click.echo("Skipping: Contents are the same.") + click.echo( + click.style( + f"Skipped uploading {source} to {repo}/{dest}: No changes detected.", + fg="yellow", + bold=True, + ) + ) return + else: + if exists: + click.echo( + click.style( + "Storing backup of existing file at {repo}/{path}...".format( + repo=repo, path=dest + ), + fg="cyan", + ), + ) + + dirname, filename = os.path.split(f"{repo}/{dest}") + + await aiofiles_os.makedirs(dirname, exist_ok=True) + + async with aiofiles_open(f"{dirname}/{filename}", mode="wb") as f: + await f.write(base64.b64decode(current_content)) + elif only_update: + click.echo( + click.style( + f"Updates only: Skipped uploading {source} to {repo}/{dest}. File does not exist.", + fg="yellow", + bold=True, + ) + ) + return data = { "message": f"Updated {dest}" @@ -83,7 +117,15 @@ async def upload_content( if exists: data["sha"] = current_sha - url = BASE_URL.format(repo=repo, path=dest) + url = FILE_CONTENTS_URL.format(repo=repo, path=dest) + + click.echo( + click.style( + f"Uploading {source} to {repo}/{dest}...", + fg="green", + bold=True, + ) + ) async with semaphore: response = await put( diff --git a/github_deploy/commands/delete.py b/github_deploy/commands/delete.py index 9b7209e..f34a8ef 100644 --- a/github_deploy/commands/delete.py +++ b/github_deploy/commands/delete.py @@ -87,7 +87,7 @@ async def main(org, token, dest): response = await list_repos(org=org, token=token, session=session) repos = [ get_repo(org=org, project=v["name"]) - for v in response["items"] + for v in response if not v["archived"] ] click.echo( diff --git a/github_deploy/commands/upload.py b/github_deploy/commands/upload.py index 3baf4a3..52813b0 100644 --- a/github_deploy/commands/upload.py +++ b/github_deploy/commands/upload.py @@ -8,7 +8,7 @@ async def handle_file_upload( - *, repo, source, dest, overwrite, token, semaphore, session + *, repo, source, dest, overwrite, only_update, token, semaphore, session ): check_exists_response = await check_exists( session=session, @@ -23,19 +23,18 @@ async def handle_file_upload( current_content = check_exists_response.get("content") exists = current_sha is not None - if exists and not overwrite: - 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: - if exists: + if exists: + if not overwrite: + click.style( + "Skipped uploading {source} to {repo}/{path}: Found an existing copy.".format( + source=source, + repo=repo, + path=dest, + ), + fg="blue", + bold=True, + ) + else: click.echo( click.style( "Found an existing copy at {repo}/{path} overwriting it's contents...".format( @@ -45,36 +44,32 @@ async def handle_file_upload( ), ) - upload_response = await upload_content( - session=session, - repo=repo, - source=source, - dest=dest, - token=token, - semaphore=semaphore, - exists=exists, - current_sha=current_sha, - current_content=current_content, - ) + upload_response = await upload_content( + session=session, + repo=repo, + source=source, + dest=dest, + token=token, + semaphore=semaphore, + exists=exists, + only_update=only_update, + current_sha=current_sha, + current_content=current_content, + ) - if upload_response: - 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, - ) + if upload_response: + 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, + ) @click.command() -@click.option( - "--org", - prompt=click.style("Enter your github user/organization", bold=True), - help="The github organization.", -) @click.option( "--token", prompt=click.style("Enter your personal access token", bold=True), @@ -82,6 +77,11 @@ async def handle_file_upload( hide_input=True, envvar="TOKEN", ) +@click.option( + "--org", + prompt=click.style("Enter your github user/organization", bold=True), + help="The github organization.", +) @click.option( "--source", prompt=click.style("Enter path to source file", fg="blue"), @@ -98,16 +98,30 @@ async def handle_file_upload( prompt=click.style( "Should we overwrite existing contents at this path", fg="blue" ), + is_flag=True, + show_default=True, help="Overwrite existing files.", default=False, ) +@click.option( + "--only-update/--no-only-update", + prompt=click.style( + "Should we only update existing files at this path", fg="blue" + ), + is_flag=True, + show_default=True, + help="Only update existing files.", + default=False, +) @click.option( "--private/--no-private", prompt=click.style("Should we Include private repositories", bold=True), + is_flag=True, + show_default=True, help="Upload files to private repositories.", default=True, ) -async def main(org, token, source, dest, overwrite, private): +async def main(org, token, source, dest, overwrite, only_update, private): """Upload a file to all repositories owned by an organization/user.""" # create instance of Semaphore: max concurrent requests. semaphore = asyncio.Semaphore(1000) @@ -118,7 +132,7 @@ async def main(org, token, source, dest, overwrite, private): response = await list_repos(org=org, token=token, session=session) repos = [ get_repo(org=org, project=r["name"]) - for r in response["items"] + for r in response if not r["archived"] and can_upload(repo=r, include_private=private) ] @@ -142,15 +156,21 @@ async def main(org, token, source, dest, overwrite, private): fg="bright_red", ) ) - deploy_msg = ( - 'Deploying "{source}" to "{path}" for all repositories'.format( - source=source, path=dest - ) - if overwrite - else 'Deploying "{source}" to repositories that don\'t already have contents at "{path}"'.format( + + if overwrite: + if only_update: + deploy_msg = "Updating '{source}' for existing files located at '{dest}'".format( + source=source, dest=dest + ) + else: + deploy_msg = "Overwriting '{dest}' with '{source}'".format( + source=source, dest=dest + ) + else: + deploy_msg = "Deploying '{source}' to repositories that don\'t already have contents at '{path}'".format( source=source, path=dest ) - ) + click.echo(click.style(deploy_msg, fg="blue")) c = click.prompt(click.style("Continue? [YN] ", fg="blue")) @@ -171,6 +191,7 @@ async def main(org, token, source, dest, overwrite, private): dest=dest, token=token, overwrite=overwrite, + only_update=only_update, session=session, semaphore=semaphore, ) diff --git a/setup.py b/setup.py index b8719f4..ade0984 100644 --- a/setup.py +++ b/setup.py @@ -23,7 +23,7 @@ setup( name="github-deploy", - version="1.0.1", + version="1.0.2", description="Deploy yaml files to a large number of repositories in seconds.", long_description=LONG_DESCRIPTION, long_description_content_type=LONG_DESCRIPTION_TYPE,