├── .github ├── FUNDING.yml ├── dependabot.yml └── workflows │ ├── python.yml │ └── release.yml ├── .gitignore ├── .tool-versions ├── Dockerfile ├── LICENSE ├── Pipfile ├── Pipfile.lock ├── README.md ├── action.yml ├── run.py ├── setup.cfg └── test_run.py /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: benjefferies 4 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: "pip" # See documentation for possible values 4 | directory: "/" # Location of package manifests 5 | schedule: 6 | interval: "weekly" 7 | -------------------------------------------------------------------------------- /.github/workflows/python.yml: -------------------------------------------------------------------------------- 1 | name: Python 2 | 3 | on: [push] 4 | permissions: write-all 5 | 6 | jobs: 7 | build: 8 | if: github.event_name == 'push' && contains(toJson(github.event.commits), '[ci]') == false 9 | runs-on: ubuntu-latest 10 | strategy: 11 | max-parallel: 1 12 | matrix: 13 | python-version: [3.12] 14 | 15 | steps: 16 | - uses: actions/checkout@v1 17 | - name: Set up Python ${{ matrix.python-version }} 18 | uses: actions/setup-python@v1 19 | with: 20 | python-version: ${{ matrix.python-version }} 21 | - name: Install pipenv 22 | run: | 23 | pip install pipenv 24 | - name: Install dependencies 25 | run: | 26 | pipenv install --dev 27 | - name: Lint with flake8 28 | run: | 29 | # stop the build if there are Python syntax errors or undefined names 30 | pipenv run flake8 . --count --select=E9,F63,F7,F82 --show-source --statistics 31 | # exit-zero treats all errors as warnings. The GitHub editor is 127 chars wide 32 | pipenv run flake8 . --count --exit-zero --max-complexity=10 --statistics 33 | - name: Test with pytest 34 | run: pipenv run pytest 35 | - name: Force disable "include administrators" branch protection 36 | uses: ./ 37 | if: always() 38 | with: 39 | access_token: ${{ secrets.PAT_ACCESS_TOKEN }} 40 | enforce_admins: false 41 | - name: Test empty commit 42 | run: | 43 | git config --global user.email "bot@echosoft.uk" 44 | git config --global user.name "Branch Protection Bot" 45 | git commit --allow-empty -m "[ci] Testing commit to master works with temporary branch protection disable" 46 | git checkout -b master-to-be 47 | git push https://benjefferies:${{ secrets.PAT_ACCESS_TOKEN }}@github.com/benjefferies/branch-protection-bot.git master-to-be:master 48 | - name: Toggle "include administrators" branch protection 49 | uses: ./ 50 | if: always() 51 | with: 52 | access_token: ${{ secrets.PAT_ACCESS_TOKEN }} 53 | - name: Force enable "include administrators" branch protection 54 | uses: ./ 55 | if: always() 56 | with: 57 | access_token: ${{ secrets.PAT_ACCESS_TOKEN }} 58 | enforce_admins: true 59 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release new action version 2 | 3 | on: 4 | release: 5 | types: [released] 6 | workflow_dispatch: 7 | inputs: 8 | TAG_NAME: 9 | description: 'Tag name that the major tag will point to' 10 | required: true 11 | 12 | env: 13 | TAG_NAME: ${{ github.event.inputs.TAG_NAME || github.event.release.tag_name }} 14 | permissions: 15 | contents: write 16 | 17 | jobs: 18 | update_tag: 19 | name: Update the major tag to include the ${{ github.event.inputs.TAG_NAME || github.event.release.tag_name }} changes 20 | environment: 21 | name: releaseNewActionVersion 22 | runs-on: ubuntu-latest 23 | steps: 24 | - name: Update the ${{ env.TAG_NAME }} tag 25 | uses: actions/publish-action@v0.2.1 26 | with: 27 | source-tag: ${{ env.TAG_NAME }} 28 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | *.egg-info/ 24 | .installed.cfg 25 | *.egg 26 | MANIFEST 27 | 28 | # PyInstaller 29 | # Usually these files are written by a python script from a template 30 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 31 | *.manifest 32 | *.spec 33 | 34 | # Installer logs 35 | pip-log.txt 36 | pip-delete-this-directory.txt 37 | 38 | # Unit test / coverage reports 39 | htmlcov/ 40 | .tox/ 41 | .coverage 42 | .coverage.* 43 | .cache 44 | nosetests.xml 45 | coverage.xml 46 | *.cover 47 | .hypothesis/ 48 | .pytest_cache/ 49 | 50 | # Translations 51 | *.mo 52 | *.pot 53 | 54 | # Django stuff: 55 | *.log 56 | local_settings.py 57 | db.sqlite3 58 | 59 | # Flask stuff: 60 | instance/ 61 | .webassets-cache 62 | 63 | # Scrapy stuff: 64 | .scrapy 65 | 66 | # Sphinx documentation 67 | docs/_build/ 68 | 69 | # PyBuilder 70 | target/ 71 | 72 | # Jupyter Notebook 73 | .ipynb_checkpoints 74 | 75 | # pyenv 76 | .python-version 77 | 78 | # celery beat schedule file 79 | celerybeat-schedule 80 | 81 | # SageMath parsed files 82 | *.sage.py 83 | 84 | # Environments 85 | .env 86 | .venv 87 | env/ 88 | venv/ 89 | ENV/ 90 | env.bak/ 91 | venv.bak/ 92 | 93 | # Spyder project settings 94 | .spyderproject 95 | .spyproject 96 | 97 | # Rope project settings 98 | .ropeproject 99 | 100 | # mkdocs documentation 101 | /site 102 | 103 | # mypy 104 | .mypy_cache/ 105 | 106 | # pycharm 107 | .idea/ -------------------------------------------------------------------------------- /.tool-versions: -------------------------------------------------------------------------------- 1 | python 3.12.0 2 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM python:3.12.0-slim-bullseye AS base 2 | 3 | ENV PYROOT /usr/local/lib/python3.12 4 | 5 | 6 | FROM base AS builder 7 | 8 | RUN pip install pipenv && \ 9 | apt-get update -y && \ 10 | apt-get install -y git && \ 11 | rm -rf /var/lib/apt/lists/* 12 | 13 | COPY Pipfile* /home/src/ 14 | 15 | WORKDIR /home/src 16 | 17 | RUN PIP_IGNORE_INSTALLED=1 pipenv install --system --deploy 18 | 19 | FROM base 20 | 21 | COPY --from=builder $PYROOT/site-packages $PYROOT/site-packages 22 | COPY run.py /bin 23 | 24 | CMD ["run.py"] 25 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Ben Jefferies 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Pipfile: -------------------------------------------------------------------------------- 1 | [[source]] 2 | name = "pypi" 3 | url = "https://pypi.org/simple" 4 | verify_ssl = true 5 | 6 | [dev-packages] 7 | flake8 = "*" 8 | pytest = "*" 9 | requests = "*" 10 | 11 | [packages] 12 | configargparse = "*" 13 | github3-py = "*" 14 | typing-extensions = "*" 15 | certifi = "*" 16 | 17 | [requires] 18 | python_version = "3.12" 19 | -------------------------------------------------------------------------------- /Pipfile.lock: -------------------------------------------------------------------------------- 1 | { 2 | "_meta": { 3 | "hash": { 4 | "sha256": "28dc7950b61d0cdcd5eb857a15e8f81f607692706ef360b61ea431e733c057c7" 5 | }, 6 | "pipfile-spec": 6, 7 | "requires": { 8 | "python_version": "3.12" 9 | }, 10 | "sources": [ 11 | { 12 | "name": "pypi", 13 | "url": "https://pypi.org/simple", 14 | "verify_ssl": true 15 | } 16 | ] 17 | }, 18 | "default": { 19 | "certifi": { 20 | "hashes": [ 21 | "sha256:9b469f3a900bf28dc19b8cfbf8019bf47f7fdd1a65a1d4ffb98fc14166beb4d1", 22 | "sha256:e036ab49d5b79556f99cfc2d9320b34cfbe5be05c5871b51de9329f0603b0474" 23 | ], 24 | "index": "pypi", 25 | "markers": "python_version >= '3.6'", 26 | "version": "==2023.11.17" 27 | }, 28 | "cffi": { 29 | "hashes": [ 30 | "sha256:0c9ef6ff37e974b73c25eecc13952c55bceed9112be2d9d938ded8e856138bcc", 31 | "sha256:131fd094d1065b19540c3d72594260f118b231090295d8c34e19a7bbcf2e860a", 32 | "sha256:1b8ebc27c014c59692bb2664c7d13ce7a6e9a629be20e54e7271fa696ff2b417", 33 | "sha256:2c56b361916f390cd758a57f2e16233eb4f64bcbeee88a4881ea90fca14dc6ab", 34 | "sha256:2d92b25dbf6cae33f65005baf472d2c245c050b1ce709cc4588cdcdd5495b520", 35 | "sha256:31d13b0f99e0836b7ff893d37af07366ebc90b678b6664c955b54561fc36ef36", 36 | "sha256:32c68ef735dbe5857c810328cb2481e24722a59a2003018885514d4c09af9743", 37 | "sha256:3686dffb02459559c74dd3d81748269ffb0eb027c39a6fc99502de37d501faa8", 38 | "sha256:582215a0e9adbe0e379761260553ba11c58943e4bbe9c36430c4ca6ac74b15ed", 39 | "sha256:5b50bf3f55561dac5438f8e70bfcdfd74543fd60df5fa5f62d94e5867deca684", 40 | "sha256:5bf44d66cdf9e893637896c7faa22298baebcd18d1ddb6d2626a6e39793a1d56", 41 | "sha256:6602bc8dc6f3a9e02b6c22c4fc1e47aa50f8f8e6d3f78a5e16ac33ef5fefa324", 42 | "sha256:673739cb539f8cdaa07d92d02efa93c9ccf87e345b9a0b556e3ecc666718468d", 43 | "sha256:68678abf380b42ce21a5f2abde8efee05c114c2fdb2e9eef2efdb0257fba1235", 44 | "sha256:68e7c44931cc171c54ccb702482e9fc723192e88d25a0e133edd7aff8fcd1f6e", 45 | "sha256:6b3d6606d369fc1da4fd8c357d026317fbb9c9b75d36dc16e90e84c26854b088", 46 | "sha256:748dcd1e3d3d7cd5443ef03ce8685043294ad6bd7c02a38d1bd367cfd968e000", 47 | "sha256:7651c50c8c5ef7bdb41108b7b8c5a83013bfaa8a935590c5d74627c047a583c7", 48 | "sha256:7b78010e7b97fef4bee1e896df8a4bbb6712b7f05b7ef630f9d1da00f6444d2e", 49 | "sha256:7e61e3e4fa664a8588aa25c883eab612a188c725755afff6289454d6362b9673", 50 | "sha256:80876338e19c951fdfed6198e70bc88f1c9758b94578d5a7c4c91a87af3cf31c", 51 | "sha256:8895613bcc094d4a1b2dbe179d88d7fb4a15cee43c052e8885783fac397d91fe", 52 | "sha256:88e2b3c14bdb32e440be531ade29d3c50a1a59cd4e51b1dd8b0865c54ea5d2e2", 53 | "sha256:8f8e709127c6c77446a8c0a8c8bf3c8ee706a06cd44b1e827c3e6a2ee6b8c098", 54 | "sha256:9cb4a35b3642fc5c005a6755a5d17c6c8b6bcb6981baf81cea8bfbc8903e8ba8", 55 | "sha256:9f90389693731ff1f659e55c7d1640e2ec43ff725cc61b04b2f9c6d8d017df6a", 56 | "sha256:a09582f178759ee8128d9270cd1344154fd473bb77d94ce0aeb2a93ebf0feaf0", 57 | "sha256:a6a14b17d7e17fa0d207ac08642c8820f84f25ce17a442fd15e27ea18d67c59b", 58 | "sha256:a72e8961a86d19bdb45851d8f1f08b041ea37d2bd8d4fd19903bc3083d80c896", 59 | "sha256:abd808f9c129ba2beda4cfc53bde801e5bcf9d6e0f22f095e45327c038bfe68e", 60 | "sha256:ac0f5edd2360eea2f1daa9e26a41db02dd4b0451b48f7c318e217ee092a213e9", 61 | "sha256:b29ebffcf550f9da55bec9e02ad430c992a87e5f512cd63388abb76f1036d8d2", 62 | "sha256:b2ca4e77f9f47c55c194982e10f058db063937845bb2b7a86c84a6cfe0aefa8b", 63 | "sha256:b7be2d771cdba2942e13215c4e340bfd76398e9227ad10402a8767ab1865d2e6", 64 | "sha256:b84834d0cf97e7d27dd5b7f3aca7b6e9263c56308ab9dc8aae9784abb774d404", 65 | "sha256:b86851a328eedc692acf81fb05444bdf1891747c25af7529e39ddafaf68a4f3f", 66 | "sha256:bcb3ef43e58665bbda2fb198698fcae6776483e0c4a631aa5647806c25e02cc0", 67 | "sha256:c0f31130ebc2d37cdd8e44605fb5fa7ad59049298b3f745c74fa74c62fbfcfc4", 68 | "sha256:c6a164aa47843fb1b01e941d385aab7215563bb8816d80ff3a363a9f8448a8dc", 69 | "sha256:d8a9d3ebe49f084ad71f9269834ceccbf398253c9fac910c4fd7053ff1386936", 70 | "sha256:db8e577c19c0fda0beb7e0d4e09e0ba74b1e4c092e0e40bfa12fe05b6f6d75ba", 71 | "sha256:dc9b18bf40cc75f66f40a7379f6a9513244fe33c0e8aa72e2d56b0196a7ef872", 72 | "sha256:e09f3ff613345df5e8c3667da1d918f9149bd623cd9070c983c013792a9a62eb", 73 | "sha256:e4108df7fe9b707191e55f33efbcb2d81928e10cea45527879a4749cbe472614", 74 | "sha256:e6024675e67af929088fda399b2094574609396b1decb609c55fa58b028a32a1", 75 | "sha256:e70f54f1796669ef691ca07d046cd81a29cb4deb1e5f942003f401c0c4a2695d", 76 | "sha256:e715596e683d2ce000574bae5d07bd522c781a822866c20495e52520564f0969", 77 | "sha256:e760191dd42581e023a68b758769e2da259b5d52e3103c6060ddc02c9edb8d7b", 78 | "sha256:ed86a35631f7bfbb28e108dd96773b9d5a6ce4811cf6ea468bb6a359b256b1e4", 79 | "sha256:ee07e47c12890ef248766a6e55bd38ebfb2bb8edd4142d56db91b21ea68b7627", 80 | "sha256:fa3a0128b152627161ce47201262d3140edb5a5c3da88d73a1b790a959126956", 81 | "sha256:fcc8eb6d5902bb1cf6dc4f187ee3ea80a1eba0a89aba40a5cb20a5087d961357" 82 | ], 83 | "markers": "python_version >= '3.8'", 84 | "version": "==1.16.0" 85 | }, 86 | "charset-normalizer": { 87 | "hashes": [ 88 | "sha256:06435b539f889b1f6f4ac1758871aae42dc3a8c0e24ac9e60c2384973ad73027", 89 | "sha256:06a81e93cd441c56a9b65d8e1d043daeb97a3d0856d177d5c90ba85acb3db087", 90 | "sha256:0a55554a2fa0d408816b3b5cedf0045f4b8e1a6065aec45849de2d6f3f8e9786", 91 | "sha256:0b2b64d2bb6d3fb9112bafa732def486049e63de9618b5843bcdd081d8144cd8", 92 | "sha256:10955842570876604d404661fbccbc9c7e684caf432c09c715ec38fbae45ae09", 93 | "sha256:122c7fa62b130ed55f8f285bfd56d5f4b4a5b503609d181f9ad85e55c89f4185", 94 | "sha256:1ceae2f17a9c33cb48e3263960dc5fc8005351ee19db217e9b1bb15d28c02574", 95 | "sha256:1d3193f4a680c64b4b6a9115943538edb896edc190f0b222e73761716519268e", 96 | "sha256:1f79682fbe303db92bc2b1136016a38a42e835d932bab5b3b1bfcfbf0640e519", 97 | "sha256:2127566c664442652f024c837091890cb1942c30937add288223dc895793f898", 98 | "sha256:22afcb9f253dac0696b5a4be4a1c0f8762f8239e21b99680099abd9b2b1b2269", 99 | "sha256:25baf083bf6f6b341f4121c2f3c548875ee6f5339300e08be3f2b2ba1721cdd3", 100 | "sha256:2e81c7b9c8979ce92ed306c249d46894776a909505d8f5a4ba55b14206e3222f", 101 | "sha256:3287761bc4ee9e33561a7e058c72ac0938c4f57fe49a09eae428fd88aafe7bb6", 102 | "sha256:34d1c8da1e78d2e001f363791c98a272bb734000fcef47a491c1e3b0505657a8", 103 | "sha256:37e55c8e51c236f95b033f6fb391d7d7970ba5fe7ff453dad675e88cf303377a", 104 | "sha256:3d47fa203a7bd9c5b6cee4736ee84ca03b8ef23193c0d1ca99b5089f72645c73", 105 | "sha256:3e4d1f6587322d2788836a99c69062fbb091331ec940e02d12d179c1d53e25fc", 106 | "sha256:42cb296636fcc8b0644486d15c12376cb9fa75443e00fb25de0b8602e64c1714", 107 | "sha256:45485e01ff4d3630ec0d9617310448a8702f70e9c01906b0d0118bdf9d124cf2", 108 | "sha256:4a78b2b446bd7c934f5dcedc588903fb2f5eec172f3d29e52a9096a43722adfc", 109 | "sha256:4ab2fe47fae9e0f9dee8c04187ce5d09f48eabe611be8259444906793ab7cbce", 110 | "sha256:4d0d1650369165a14e14e1e47b372cfcb31d6ab44e6e33cb2d4e57265290044d", 111 | "sha256:549a3a73da901d5bc3ce8d24e0600d1fa85524c10287f6004fbab87672bf3e1e", 112 | "sha256:55086ee1064215781fff39a1af09518bc9255b50d6333f2e4c74ca09fac6a8f6", 113 | "sha256:572c3763a264ba47b3cf708a44ce965d98555f618ca42c926a9c1616d8f34269", 114 | "sha256:573f6eac48f4769d667c4442081b1794f52919e7edada77495aaed9236d13a96", 115 | "sha256:5b4c145409bef602a690e7cfad0a15a55c13320ff7a3ad7ca59c13bb8ba4d45d", 116 | "sha256:6463effa3186ea09411d50efc7d85360b38d5f09b870c48e4600f63af490e56a", 117 | "sha256:65f6f63034100ead094b8744b3b97965785388f308a64cf8d7c34f2f2e5be0c4", 118 | "sha256:663946639d296df6a2bb2aa51b60a2454ca1cb29835324c640dafb5ff2131a77", 119 | "sha256:6897af51655e3691ff853668779c7bad41579facacf5fd7253b0133308cf000d", 120 | "sha256:68d1f8a9e9e37c1223b656399be5d6b448dea850bed7d0f87a8311f1ff3dabb0", 121 | "sha256:6ac7ffc7ad6d040517be39eb591cac5ff87416c2537df6ba3cba3bae290c0fed", 122 | "sha256:6b3251890fff30ee142c44144871185dbe13b11bab478a88887a639655be1068", 123 | "sha256:6c4caeef8fa63d06bd437cd4bdcf3ffefe6738fb1b25951440d80dc7df8c03ac", 124 | "sha256:6ef1d82a3af9d3eecdba2321dc1b3c238245d890843e040e41e470ffa64c3e25", 125 | "sha256:753f10e867343b4511128c6ed8c82f7bec3bd026875576dfd88483c5c73b2fd8", 126 | "sha256:7cd13a2e3ddeed6913a65e66e94b51d80a041145a026c27e6bb76c31a853c6ab", 127 | "sha256:7ed9e526742851e8d5cc9e6cf41427dfc6068d4f5a3bb03659444b4cabf6bc26", 128 | "sha256:7f04c839ed0b6b98b1a7501a002144b76c18fb1c1850c8b98d458ac269e26ed2", 129 | "sha256:802fe99cca7457642125a8a88a084cef28ff0cf9407060f7b93dca5aa25480db", 130 | "sha256:80402cd6ee291dcb72644d6eac93785fe2c8b9cb30893c1af5b8fdd753b9d40f", 131 | "sha256:8465322196c8b4d7ab6d1e049e4c5cb460d0394da4a27d23cc242fbf0034b6b5", 132 | "sha256:86216b5cee4b06df986d214f664305142d9c76df9b6512be2738aa72a2048f99", 133 | "sha256:87d1351268731db79e0f8e745d92493ee2841c974128ef629dc518b937d9194c", 134 | "sha256:8bdb58ff7ba23002a4c5808d608e4e6c687175724f54a5dade5fa8c67b604e4d", 135 | "sha256:8c622a5fe39a48f78944a87d4fb8a53ee07344641b0562c540d840748571b811", 136 | "sha256:8d756e44e94489e49571086ef83b2bb8ce311e730092d2c34ca8f7d925cb20aa", 137 | "sha256:8f4a014bc36d3c57402e2977dada34f9c12300af536839dc38c0beab8878f38a", 138 | "sha256:9063e24fdb1e498ab71cb7419e24622516c4a04476b17a2dab57e8baa30d6e03", 139 | "sha256:90d558489962fd4918143277a773316e56c72da56ec7aa3dc3dbbe20fdfed15b", 140 | "sha256:923c0c831b7cfcb071580d3f46c4baf50f174be571576556269530f4bbd79d04", 141 | "sha256:95f2a5796329323b8f0512e09dbb7a1860c46a39da62ecb2324f116fa8fdc85c", 142 | "sha256:96b02a3dc4381e5494fad39be677abcb5e6634bf7b4fa83a6dd3112607547001", 143 | "sha256:9f96df6923e21816da7e0ad3fd47dd8f94b2a5ce594e00677c0013018b813458", 144 | "sha256:a10af20b82360ab00827f916a6058451b723b4e65030c5a18577c8b2de5b3389", 145 | "sha256:a50aebfa173e157099939b17f18600f72f84eed3049e743b68ad15bd69b6bf99", 146 | "sha256:a981a536974bbc7a512cf44ed14938cf01030a99e9b3a06dd59578882f06f985", 147 | "sha256:a9a8e9031d613fd2009c182b69c7b2c1ef8239a0efb1df3f7c8da66d5dd3d537", 148 | "sha256:ae5f4161f18c61806f411a13b0310bea87f987c7d2ecdbdaad0e94eb2e404238", 149 | "sha256:aed38f6e4fb3f5d6bf81bfa990a07806be9d83cf7bacef998ab1a9bd660a581f", 150 | "sha256:b01b88d45a6fcb69667cd6d2f7a9aeb4bf53760d7fc536bf679ec94fe9f3ff3d", 151 | "sha256:b261ccdec7821281dade748d088bb6e9b69e6d15b30652b74cbbac25e280b796", 152 | "sha256:b2b0a0c0517616b6869869f8c581d4eb2dd83a4d79e0ebcb7d373ef9956aeb0a", 153 | "sha256:b4a23f61ce87adf89be746c8a8974fe1c823c891d8f86eb218bb957c924bb143", 154 | "sha256:bd8f7df7d12c2db9fab40bdd87a7c09b1530128315d047a086fa3ae3435cb3a8", 155 | "sha256:beb58fe5cdb101e3a055192ac291b7a21e3b7ef4f67fa1d74e331a7f2124341c", 156 | "sha256:c002b4ffc0be611f0d9da932eb0f704fe2602a9a949d1f738e4c34c75b0863d5", 157 | "sha256:c083af607d2515612056a31f0a8d9e0fcb5876b7bfc0abad3ecd275bc4ebc2d5", 158 | "sha256:c180f51afb394e165eafe4ac2936a14bee3eb10debc9d9e4db8958fe36afe711", 159 | "sha256:c235ebd9baae02f1b77bcea61bce332cb4331dc3617d254df3323aa01ab47bd4", 160 | "sha256:cd70574b12bb8a4d2aaa0094515df2463cb429d8536cfb6c7ce983246983e5a6", 161 | "sha256:d0eccceffcb53201b5bfebb52600a5fb483a20b61da9dbc885f8b103cbe7598c", 162 | "sha256:d965bba47ddeec8cd560687584e88cf699fd28f192ceb452d1d7ee807c5597b7", 163 | "sha256:db364eca23f876da6f9e16c9da0df51aa4f104a972735574842618b8c6d999d4", 164 | "sha256:ddbb2551d7e0102e7252db79ba445cdab71b26640817ab1e3e3648dad515003b", 165 | "sha256:deb6be0ac38ece9ba87dea880e438f25ca3eddfac8b002a2ec3d9183a454e8ae", 166 | "sha256:e06ed3eb3218bc64786f7db41917d4e686cc4856944f53d5bdf83a6884432e12", 167 | "sha256:e27ad930a842b4c5eb8ac0016b0a54f5aebbe679340c26101df33424142c143c", 168 | "sha256:e537484df0d8f426ce2afb2d0f8e1c3d0b114b83f8850e5f2fbea0e797bd82ae", 169 | "sha256:eb00ed941194665c332bf8e078baf037d6c35d7c4f3102ea2d4f16ca94a26dc8", 170 | "sha256:eb6904c354526e758fda7167b33005998fb68c46fbc10e013ca97f21ca5c8887", 171 | "sha256:eb8821e09e916165e160797a6c17edda0679379a4be5c716c260e836e122f54b", 172 | "sha256:efcb3f6676480691518c177e3b465bcddf57cea040302f9f4e6e191af91174d4", 173 | "sha256:f27273b60488abe721a075bcca6d7f3964f9f6f067c8c4c605743023d7d3944f", 174 | "sha256:f30c3cb33b24454a82faecaf01b19c18562b1e89558fb6c56de4d9118a032fd5", 175 | "sha256:fb69256e180cb6c8a894fee62b3afebae785babc1ee98b81cdf68bbca1987f33", 176 | "sha256:fd1abc0d89e30cc4e02e4064dc67fcc51bd941eb395c502aac3ec19fab46b519", 177 | "sha256:ff8fa367d09b717b2a17a052544193ad76cd49979c805768879cb63d9ca50561" 178 | ], 179 | "markers": "python_full_version >= '3.7.0'", 180 | "version": "==3.3.2" 181 | }, 182 | "configargparse": { 183 | "hashes": [ 184 | "sha256:d249da6591465c6c26df64a9f73d2536e743be2f244eb3ebe61114af2f94f86b", 185 | "sha256:e7067471884de5478c58a511e529f0f9bd1c66bfef1dea90935438d6c23306d1" 186 | ], 187 | "index": "pypi", 188 | "markers": "python_version >= '3.5'", 189 | "version": "==1.7" 190 | }, 191 | "cryptography": { 192 | "hashes": [ 193 | "sha256:079b85658ea2f59c4f43b70f8119a52414cdb7be34da5d019a77bf96d473b960", 194 | "sha256:09616eeaef406f99046553b8a40fbf8b1e70795a91885ba4c96a70793de5504a", 195 | "sha256:13f93ce9bea8016c253b34afc6bd6a75993e5c40672ed5405a9c832f0d4a00bc", 196 | "sha256:37a138589b12069efb424220bf78eac59ca68b95696fc622b6ccc1c0a197204a", 197 | "sha256:3c78451b78313fa81607fa1b3f1ae0a5ddd8014c38a02d9db0616133987b9cdf", 198 | "sha256:43f2552a2378b44869fe8827aa19e69512e3245a219104438692385b0ee119d1", 199 | "sha256:48a0476626da912a44cc078f9893f292f0b3e4c739caf289268168d8f4702a39", 200 | "sha256:49f0805fc0b2ac8d4882dd52f4a3b935b210935d500b6b805f321addc8177406", 201 | "sha256:5429ec739a29df2e29e15d082f1d9ad683701f0ec7709ca479b3ff2708dae65a", 202 | "sha256:5a1b41bc97f1ad230a41657d9155113c7521953869ae57ac39ac7f1bb471469a", 203 | "sha256:68a2dec79deebc5d26d617bfdf6e8aab065a4f34934b22d3b5010df3ba36612c", 204 | "sha256:7a698cb1dac82c35fcf8fe3417a3aaba97de16a01ac914b89a0889d364d2f6be", 205 | "sha256:841df4caa01008bad253bce2a6f7b47f86dc9f08df4b433c404def869f590a15", 206 | "sha256:90452ba79b8788fa380dfb587cca692976ef4e757b194b093d845e8d99f612f2", 207 | "sha256:928258ba5d6f8ae644e764d0f996d61a8777559f72dfeb2eea7e2fe0ad6e782d", 208 | "sha256:af03b32695b24d85a75d40e1ba39ffe7db7ffcb099fe507b39fd41a565f1b157", 209 | "sha256:b640981bf64a3e978a56167594a0e97db71c89a479da8e175d8bb5be5178c003", 210 | "sha256:c5ca78485a255e03c32b513f8c2bc39fedb7f5c5f8535545bdc223a03b24f248", 211 | "sha256:c7f3201ec47d5207841402594f1d7950879ef890c0c495052fa62f58283fde1a", 212 | "sha256:d5ec85080cce7b0513cfd233914eb8b7bbd0633f1d1703aa28d1dd5a72f678ec", 213 | "sha256:d6c391c021ab1f7a82da5d8d0b3cee2f4b2c455ec86c8aebbc84837a631ff309", 214 | "sha256:e3114da6d7f95d2dee7d3f4eec16dacff819740bbab931aff8648cb13c5ff5e7", 215 | "sha256:f983596065a18a2183e7f79ab3fd4c475205b839e02cbc0efbbf9666c4b3083d" 216 | ], 217 | "version": "==41.0.7" 218 | }, 219 | "github3-py": { 220 | "hashes": [ 221 | "sha256:30d571076753efc389edc7f9aaef338a4fcb24b54d8968d5f39b1342f45ddd36", 222 | "sha256:a89af7de25650612d1da2f0609622bcdeb07ee8a45a1c06b2d16a05e4234e753" 223 | ], 224 | "index": "pypi", 225 | "markers": "python_version >= '3.7'", 226 | "version": "==4.0.1" 227 | }, 228 | "idna": { 229 | "hashes": [ 230 | "sha256:9ecdbbd083b06798ae1e86adcbfe8ab1479cf864e4ee30fe4e46a003d12491ca", 231 | "sha256:c05567e9c24a6b9faaa835c4821bad0590fbb9d5779e7caa6e1cc4978e7eb24f" 232 | ], 233 | "markers": "python_version >= '3.5'", 234 | "version": "==3.6" 235 | }, 236 | "pycparser": { 237 | "hashes": [ 238 | "sha256:8ee45429555515e1f6b185e78100aea234072576aa43ab53aefcae078162fca9", 239 | "sha256:e644fdec12f7872f86c58ff790da456218b10f863970249516d60a5eaca77206" 240 | ], 241 | "version": "==2.21" 242 | }, 243 | "pyjwt": { 244 | "extras": [ 245 | "crypto" 246 | ], 247 | "hashes": [ 248 | "sha256:57e28d156e3d5c10088e0c68abb90bfac3df82b40a71bd0daa20c65ccd5c23de", 249 | "sha256:59127c392cc44c2da5bb3192169a91f429924e17aff6534d70fdc02ab3e04320" 250 | ], 251 | "markers": "python_version >= '3.7'", 252 | "version": "==2.8.0" 253 | }, 254 | "python-dateutil": { 255 | "hashes": [ 256 | "sha256:0123cacc1627ae19ddf3c27a5de5bd67ee4586fbdd6440d9748f8abb483d3e86", 257 | "sha256:961d03dc3453ebbc59dbdea9e4e11c5651520a876d0f4db161e8674aae935da9" 258 | ], 259 | "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", 260 | "version": "==2.8.2" 261 | }, 262 | "requests": { 263 | "hashes": [ 264 | "sha256:58cd2187c01e70e6e26505bca751777aa9f2ee0b7f4300988b709f44e013003f", 265 | "sha256:942c5a758f98d790eaed1a29cb6eefc7ffb0d1cf7af05c3d2791656dbd6ad1e1" 266 | ], 267 | "markers": "python_version >= '3.7'", 268 | "version": "==2.31.0" 269 | }, 270 | "six": { 271 | "hashes": [ 272 | "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926", 273 | "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254" 274 | ], 275 | "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", 276 | "version": "==1.16.0" 277 | }, 278 | "typing-extensions": { 279 | "hashes": [ 280 | "sha256:23478f88c37f27d76ac8aee6c905017a143b0b1b886c3c9f66bc2fd94f9f5783", 281 | "sha256:af72aea155e91adfc61c3ae9e0e342dbc0cba726d6cba4b6c72c1f34e47291cd" 282 | ], 283 | "index": "pypi", 284 | "markers": "python_version >= '3.8'", 285 | "version": "==4.9.0" 286 | }, 287 | "uritemplate": { 288 | "hashes": [ 289 | "sha256:4346edfc5c3b79f694bccd6d6099a322bbeb628dbf2cd86eea55a456ce5124f0", 290 | "sha256:830c08b8d99bdd312ea4ead05994a38e8936266f84b9a7878232db50b044e02e" 291 | ], 292 | "markers": "python_version >= '3.6'", 293 | "version": "==4.1.1" 294 | }, 295 | "urllib3": { 296 | "hashes": [ 297 | "sha256:55901e917a5896a349ff771be919f8bd99aff50b79fe58fec595eb37bbc56bb3", 298 | "sha256:df7aa8afb0148fa78488e7899b2c59b5f4ffcfa82e6c54ccb9dd37c1d7b52d54" 299 | ], 300 | "markers": "python_version >= '3.8'", 301 | "version": "==2.1.0" 302 | } 303 | }, 304 | "develop": { 305 | "certifi": { 306 | "hashes": [ 307 | "sha256:9b469f3a900bf28dc19b8cfbf8019bf47f7fdd1a65a1d4ffb98fc14166beb4d1", 308 | "sha256:e036ab49d5b79556f99cfc2d9320b34cfbe5be05c5871b51de9329f0603b0474" 309 | ], 310 | "index": "pypi", 311 | "markers": "python_version >= '3.6'", 312 | "version": "==2023.11.17" 313 | }, 314 | "charset-normalizer": { 315 | "hashes": [ 316 | "sha256:06435b539f889b1f6f4ac1758871aae42dc3a8c0e24ac9e60c2384973ad73027", 317 | "sha256:06a81e93cd441c56a9b65d8e1d043daeb97a3d0856d177d5c90ba85acb3db087", 318 | "sha256:0a55554a2fa0d408816b3b5cedf0045f4b8e1a6065aec45849de2d6f3f8e9786", 319 | "sha256:0b2b64d2bb6d3fb9112bafa732def486049e63de9618b5843bcdd081d8144cd8", 320 | "sha256:10955842570876604d404661fbccbc9c7e684caf432c09c715ec38fbae45ae09", 321 | "sha256:122c7fa62b130ed55f8f285bfd56d5f4b4a5b503609d181f9ad85e55c89f4185", 322 | "sha256:1ceae2f17a9c33cb48e3263960dc5fc8005351ee19db217e9b1bb15d28c02574", 323 | "sha256:1d3193f4a680c64b4b6a9115943538edb896edc190f0b222e73761716519268e", 324 | "sha256:1f79682fbe303db92bc2b1136016a38a42e835d932bab5b3b1bfcfbf0640e519", 325 | "sha256:2127566c664442652f024c837091890cb1942c30937add288223dc895793f898", 326 | "sha256:22afcb9f253dac0696b5a4be4a1c0f8762f8239e21b99680099abd9b2b1b2269", 327 | "sha256:25baf083bf6f6b341f4121c2f3c548875ee6f5339300e08be3f2b2ba1721cdd3", 328 | "sha256:2e81c7b9c8979ce92ed306c249d46894776a909505d8f5a4ba55b14206e3222f", 329 | "sha256:3287761bc4ee9e33561a7e058c72ac0938c4f57fe49a09eae428fd88aafe7bb6", 330 | "sha256:34d1c8da1e78d2e001f363791c98a272bb734000fcef47a491c1e3b0505657a8", 331 | "sha256:37e55c8e51c236f95b033f6fb391d7d7970ba5fe7ff453dad675e88cf303377a", 332 | "sha256:3d47fa203a7bd9c5b6cee4736ee84ca03b8ef23193c0d1ca99b5089f72645c73", 333 | "sha256:3e4d1f6587322d2788836a99c69062fbb091331ec940e02d12d179c1d53e25fc", 334 | "sha256:42cb296636fcc8b0644486d15c12376cb9fa75443e00fb25de0b8602e64c1714", 335 | "sha256:45485e01ff4d3630ec0d9617310448a8702f70e9c01906b0d0118bdf9d124cf2", 336 | "sha256:4a78b2b446bd7c934f5dcedc588903fb2f5eec172f3d29e52a9096a43722adfc", 337 | "sha256:4ab2fe47fae9e0f9dee8c04187ce5d09f48eabe611be8259444906793ab7cbce", 338 | "sha256:4d0d1650369165a14e14e1e47b372cfcb31d6ab44e6e33cb2d4e57265290044d", 339 | "sha256:549a3a73da901d5bc3ce8d24e0600d1fa85524c10287f6004fbab87672bf3e1e", 340 | "sha256:55086ee1064215781fff39a1af09518bc9255b50d6333f2e4c74ca09fac6a8f6", 341 | "sha256:572c3763a264ba47b3cf708a44ce965d98555f618ca42c926a9c1616d8f34269", 342 | "sha256:573f6eac48f4769d667c4442081b1794f52919e7edada77495aaed9236d13a96", 343 | "sha256:5b4c145409bef602a690e7cfad0a15a55c13320ff7a3ad7ca59c13bb8ba4d45d", 344 | "sha256:6463effa3186ea09411d50efc7d85360b38d5f09b870c48e4600f63af490e56a", 345 | "sha256:65f6f63034100ead094b8744b3b97965785388f308a64cf8d7c34f2f2e5be0c4", 346 | "sha256:663946639d296df6a2bb2aa51b60a2454ca1cb29835324c640dafb5ff2131a77", 347 | "sha256:6897af51655e3691ff853668779c7bad41579facacf5fd7253b0133308cf000d", 348 | "sha256:68d1f8a9e9e37c1223b656399be5d6b448dea850bed7d0f87a8311f1ff3dabb0", 349 | "sha256:6ac7ffc7ad6d040517be39eb591cac5ff87416c2537df6ba3cba3bae290c0fed", 350 | "sha256:6b3251890fff30ee142c44144871185dbe13b11bab478a88887a639655be1068", 351 | "sha256:6c4caeef8fa63d06bd437cd4bdcf3ffefe6738fb1b25951440d80dc7df8c03ac", 352 | "sha256:6ef1d82a3af9d3eecdba2321dc1b3c238245d890843e040e41e470ffa64c3e25", 353 | "sha256:753f10e867343b4511128c6ed8c82f7bec3bd026875576dfd88483c5c73b2fd8", 354 | "sha256:7cd13a2e3ddeed6913a65e66e94b51d80a041145a026c27e6bb76c31a853c6ab", 355 | "sha256:7ed9e526742851e8d5cc9e6cf41427dfc6068d4f5a3bb03659444b4cabf6bc26", 356 | "sha256:7f04c839ed0b6b98b1a7501a002144b76c18fb1c1850c8b98d458ac269e26ed2", 357 | "sha256:802fe99cca7457642125a8a88a084cef28ff0cf9407060f7b93dca5aa25480db", 358 | "sha256:80402cd6ee291dcb72644d6eac93785fe2c8b9cb30893c1af5b8fdd753b9d40f", 359 | "sha256:8465322196c8b4d7ab6d1e049e4c5cb460d0394da4a27d23cc242fbf0034b6b5", 360 | "sha256:86216b5cee4b06df986d214f664305142d9c76df9b6512be2738aa72a2048f99", 361 | "sha256:87d1351268731db79e0f8e745d92493ee2841c974128ef629dc518b937d9194c", 362 | "sha256:8bdb58ff7ba23002a4c5808d608e4e6c687175724f54a5dade5fa8c67b604e4d", 363 | "sha256:8c622a5fe39a48f78944a87d4fb8a53ee07344641b0562c540d840748571b811", 364 | "sha256:8d756e44e94489e49571086ef83b2bb8ce311e730092d2c34ca8f7d925cb20aa", 365 | "sha256:8f4a014bc36d3c57402e2977dada34f9c12300af536839dc38c0beab8878f38a", 366 | "sha256:9063e24fdb1e498ab71cb7419e24622516c4a04476b17a2dab57e8baa30d6e03", 367 | "sha256:90d558489962fd4918143277a773316e56c72da56ec7aa3dc3dbbe20fdfed15b", 368 | "sha256:923c0c831b7cfcb071580d3f46c4baf50f174be571576556269530f4bbd79d04", 369 | "sha256:95f2a5796329323b8f0512e09dbb7a1860c46a39da62ecb2324f116fa8fdc85c", 370 | "sha256:96b02a3dc4381e5494fad39be677abcb5e6634bf7b4fa83a6dd3112607547001", 371 | "sha256:9f96df6923e21816da7e0ad3fd47dd8f94b2a5ce594e00677c0013018b813458", 372 | "sha256:a10af20b82360ab00827f916a6058451b723b4e65030c5a18577c8b2de5b3389", 373 | "sha256:a50aebfa173e157099939b17f18600f72f84eed3049e743b68ad15bd69b6bf99", 374 | "sha256:a981a536974bbc7a512cf44ed14938cf01030a99e9b3a06dd59578882f06f985", 375 | "sha256:a9a8e9031d613fd2009c182b69c7b2c1ef8239a0efb1df3f7c8da66d5dd3d537", 376 | "sha256:ae5f4161f18c61806f411a13b0310bea87f987c7d2ecdbdaad0e94eb2e404238", 377 | "sha256:aed38f6e4fb3f5d6bf81bfa990a07806be9d83cf7bacef998ab1a9bd660a581f", 378 | "sha256:b01b88d45a6fcb69667cd6d2f7a9aeb4bf53760d7fc536bf679ec94fe9f3ff3d", 379 | "sha256:b261ccdec7821281dade748d088bb6e9b69e6d15b30652b74cbbac25e280b796", 380 | "sha256:b2b0a0c0517616b6869869f8c581d4eb2dd83a4d79e0ebcb7d373ef9956aeb0a", 381 | "sha256:b4a23f61ce87adf89be746c8a8974fe1c823c891d8f86eb218bb957c924bb143", 382 | "sha256:bd8f7df7d12c2db9fab40bdd87a7c09b1530128315d047a086fa3ae3435cb3a8", 383 | "sha256:beb58fe5cdb101e3a055192ac291b7a21e3b7ef4f67fa1d74e331a7f2124341c", 384 | "sha256:c002b4ffc0be611f0d9da932eb0f704fe2602a9a949d1f738e4c34c75b0863d5", 385 | "sha256:c083af607d2515612056a31f0a8d9e0fcb5876b7bfc0abad3ecd275bc4ebc2d5", 386 | "sha256:c180f51afb394e165eafe4ac2936a14bee3eb10debc9d9e4db8958fe36afe711", 387 | "sha256:c235ebd9baae02f1b77bcea61bce332cb4331dc3617d254df3323aa01ab47bd4", 388 | "sha256:cd70574b12bb8a4d2aaa0094515df2463cb429d8536cfb6c7ce983246983e5a6", 389 | "sha256:d0eccceffcb53201b5bfebb52600a5fb483a20b61da9dbc885f8b103cbe7598c", 390 | "sha256:d965bba47ddeec8cd560687584e88cf699fd28f192ceb452d1d7ee807c5597b7", 391 | "sha256:db364eca23f876da6f9e16c9da0df51aa4f104a972735574842618b8c6d999d4", 392 | "sha256:ddbb2551d7e0102e7252db79ba445cdab71b26640817ab1e3e3648dad515003b", 393 | "sha256:deb6be0ac38ece9ba87dea880e438f25ca3eddfac8b002a2ec3d9183a454e8ae", 394 | "sha256:e06ed3eb3218bc64786f7db41917d4e686cc4856944f53d5bdf83a6884432e12", 395 | "sha256:e27ad930a842b4c5eb8ac0016b0a54f5aebbe679340c26101df33424142c143c", 396 | "sha256:e537484df0d8f426ce2afb2d0f8e1c3d0b114b83f8850e5f2fbea0e797bd82ae", 397 | "sha256:eb00ed941194665c332bf8e078baf037d6c35d7c4f3102ea2d4f16ca94a26dc8", 398 | "sha256:eb6904c354526e758fda7167b33005998fb68c46fbc10e013ca97f21ca5c8887", 399 | "sha256:eb8821e09e916165e160797a6c17edda0679379a4be5c716c260e836e122f54b", 400 | "sha256:efcb3f6676480691518c177e3b465bcddf57cea040302f9f4e6e191af91174d4", 401 | "sha256:f27273b60488abe721a075bcca6d7f3964f9f6f067c8c4c605743023d7d3944f", 402 | "sha256:f30c3cb33b24454a82faecaf01b19c18562b1e89558fb6c56de4d9118a032fd5", 403 | "sha256:fb69256e180cb6c8a894fee62b3afebae785babc1ee98b81cdf68bbca1987f33", 404 | "sha256:fd1abc0d89e30cc4e02e4064dc67fcc51bd941eb395c502aac3ec19fab46b519", 405 | "sha256:ff8fa367d09b717b2a17a052544193ad76cd49979c805768879cb63d9ca50561" 406 | ], 407 | "markers": "python_full_version >= '3.7.0'", 408 | "version": "==3.3.2" 409 | }, 410 | "flake8": { 411 | "hashes": [ 412 | "sha256:33f96621059e65eec474169085dc92bf26e7b2d47366b70be2f67ab80dc25132", 413 | "sha256:a6dfbb75e03252917f2473ea9653f7cd799c3064e54d4c8140044c5c065f53c3" 414 | ], 415 | "index": "pypi", 416 | "markers": "python_full_version >= '3.8.1'", 417 | "version": "==7.0.0" 418 | }, 419 | "idna": { 420 | "hashes": [ 421 | "sha256:9ecdbbd083b06798ae1e86adcbfe8ab1479cf864e4ee30fe4e46a003d12491ca", 422 | "sha256:c05567e9c24a6b9faaa835c4821bad0590fbb9d5779e7caa6e1cc4978e7eb24f" 423 | ], 424 | "markers": "python_version >= '3.5'", 425 | "version": "==3.6" 426 | }, 427 | "iniconfig": { 428 | "hashes": [ 429 | "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3", 430 | "sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374" 431 | ], 432 | "markers": "python_version >= '3.7'", 433 | "version": "==2.0.0" 434 | }, 435 | "mccabe": { 436 | "hashes": [ 437 | "sha256:348e0240c33b60bbdf4e523192ef919f28cb2c3d7d5c7794f74009290f236325", 438 | "sha256:6c2d30ab6be0e4a46919781807b4f0d834ebdd6c6e3dca0bda5a15f863427b6e" 439 | ], 440 | "markers": "python_version >= '3.6'", 441 | "version": "==0.7.0" 442 | }, 443 | "packaging": { 444 | "hashes": [ 445 | "sha256:048fb0e9405036518eaaf48a55953c750c11e1a1b68e0dd1a9d62ed0c092cfc5", 446 | "sha256:8c491190033a9af7e1d931d0b5dacc2ef47509b34dd0de67ed209b5203fc88c7" 447 | ], 448 | "markers": "python_version >= '3.7'", 449 | "version": "==23.2" 450 | }, 451 | "pluggy": { 452 | "hashes": [ 453 | "sha256:7db9f7b503d67d1c5b95f59773ebb58a8c1c288129a88665838012cfb07b8981", 454 | "sha256:8c85c2876142a764e5b7548e7d9a0e0ddb46f5185161049a79b7e974454223be" 455 | ], 456 | "markers": "python_version >= '3.8'", 457 | "version": "==1.4.0" 458 | }, 459 | "pycodestyle": { 460 | "hashes": [ 461 | "sha256:41ba0e7afc9752dfb53ced5489e89f8186be00e599e712660695b7a75ff2663f", 462 | "sha256:44fe31000b2d866f2e41841b18528a505fbd7fef9017b04eff4e2648a0fadc67" 463 | ], 464 | "markers": "python_version >= '3.8'", 465 | "version": "==2.11.1" 466 | }, 467 | "pyflakes": { 468 | "hashes": [ 469 | "sha256:1c61603ff154621fb2a9172037d84dca3500def8c8b630657d1701f026f8af3f", 470 | "sha256:84b5be138a2dfbb40689ca07e2152deb896a65c3a3e24c251c5c62489568074a" 471 | ], 472 | "markers": "python_version >= '3.8'", 473 | "version": "==3.2.0" 474 | }, 475 | "pytest": { 476 | "hashes": [ 477 | "sha256:249b1b0864530ba251b7438274c4d251c58d868edaaec8762893ad4a0d71c36c", 478 | "sha256:50fb9cbe836c3f20f0dfa99c565201fb75dc54c8d76373cd1bde06b06657bdb6" 479 | ], 480 | "index": "pypi", 481 | "markers": "python_version >= '3.8'", 482 | "version": "==8.0.0" 483 | }, 484 | "requests": { 485 | "hashes": [ 486 | "sha256:58cd2187c01e70e6e26505bca751777aa9f2ee0b7f4300988b709f44e013003f", 487 | "sha256:942c5a758f98d790eaed1a29cb6eefc7ffb0d1cf7af05c3d2791656dbd6ad1e1" 488 | ], 489 | "markers": "python_version >= '3.7'", 490 | "version": "==2.31.0" 491 | }, 492 | "urllib3": { 493 | "hashes": [ 494 | "sha256:55901e917a5896a349ff771be919f8bd99aff50b79fe58fec595eb37bbc56bb3", 495 | "sha256:df7aa8afb0148fa78488e7899b2c59b5f4ffcfa82e6c54ccb9dd37c1d7b52d54" 496 | ], 497 | "markers": "python_version >= '3.8'", 498 | "version": "==2.1.0" 499 | } 500 | } 501 | } 502 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Branch Protection Bot 2 | A bot tool to temporarily disable and re-enable `Do not allow bypassing the above settings` option in branch protection 3 | 4 | Github doesn't have a way to give a Bot access to override the branch protection, specifically if you [Do not allow bypassing the above settings](https://github.com/isaacs/github/issues/1390). 5 | The only possible solution is to disable the `Do not allow bypassing the above settings` option. This increases risk of accidental pushes to master from administrators (I've done it a few times). 6 | This tool doesn't completely solve the problem of accidents happening but reduces the chances by closing the window. 7 | 8 | The intended use of this tool is to is in a CI/CD pipeline where you require temporary access to allow a administrator Bot push to a branch. 9 | 10 | [Tutorial](https://www.turfemon.com/bump-version-protected-branch-github-actions) 11 | 12 | ## How it works 13 | 1. Your automated pipeline is kicked off 14 | 1. Before you push to github you run this tool to disable `Do not allow bypassing the above settings` 15 | 1. Push to the repository 16 | 1. After you push to github you run this tool to enable `Do not allow bypassing the above settings` 17 | 18 | ## Example usage 19 | ### Docker 20 | ``` 21 | docker run -e ACCESS_TOKEN=abc123 -e BRANCH=master -e REPO=branch-protection-bot -e OWNER=benjefferies benjjefferies/branch-protection-bot 22 | ``` 23 | 24 | ### Github Actions 25 | 26 | ``` 27 | - name: Temporarily disable "Do not allow bypassing the above settings" branch protection 28 | uses: benjefferies/branch-protection-bot@master 29 | if: always() 30 | with: 31 | access_token: ${{ secrets.ACCESS_TOKEN }} 32 | branch: ${{ github.event.repository.default_branch }} 33 | 34 | - name: Deploy 35 | run: | 36 | mvn release:prepare -B 37 | mvn release:perform -B 38 | 39 | - name: Enable "Do not allow bypassing the above settings" branch protection 40 | uses: benjefferies/branch-protection-bot@master 41 | if: always() # Force to always run this step to ensure "Do not allow bypassing the above settings" is always turned back on 42 | with: 43 | access_token: ${{ secrets.ACCESS_TOKEN }} 44 | owner: benjefferies 45 | repo: branch-protection-bot 46 | branch: ${{ github.event.repository.default_branch }} 47 | ``` 48 | 49 | #### Inputs 50 | 51 | ##### `access_token` 52 | 53 | **Required** Github access token. https://help.github.com/en/github/authenticating-to-github/creating-a-personal-access-token-for-the-command-line. See [issue](https://github.com/benjefferies/branch-protection-bot/issues/9#issuecomment-1637223088) for required permissions 54 | 55 | ##### `owner` 56 | 57 | For example benjefferies for https://github.com/benjefferies/branch-protection-bot. If not set with repo GITHUB_REPOSITORY variable will be used 58 | 59 | ##### `repo` 60 | 61 | For example branch-protection-bot for https://github.com/benjefferies/branch-protection-bot. If not set with repo GITHUB_REPOSITORY variable will be used 62 | 63 | ##### `branch` 64 | 65 | Branch name. Default `"master"` 66 | 67 | ##### `retries` 68 | 69 | Number of times to retry before exiting. Default `5`. 70 | 71 | ##### `enforce_admins` 72 | 73 | If you want to pin the state of `Do not allow bypassing the above settings` for a step in the workflow. 74 | 75 | #### Outputs 76 | 77 | ##### `initial_status` 78 | 79 | Output the current branch protection status of `Do not allow bypassing the above settings` prior to any change. 80 | You can retrieve it from any next step in your job using: `${{ steps.disable_include_admins.outputs.initial_status }}`. 81 | This would help you to restore the initial setting this way: 82 | 83 | ```yaml 84 | steps: 85 | - name: "Temporarily disable 'Do not allow bypassing the above settings' default branch protection" 86 | id: disable_include_admins 87 | uses: benjefferies/branch-protection-bot@master 88 | if: always() 89 | with: 90 | access_token: ${{ secrets.ACCESS_TOKEN }} 91 | branch: ${{ github.event.repository.default_branch }} 92 | enforce_admins: false 93 | 94 | - ... 95 | 96 | - name: "Restore 'Do not allow bypassing the above settings' default branch protection" 97 | uses: benjefferies/branch-protection-bot@master 98 | if: always() # Force to always run this step to ensure "Do not allow bypassing the above settings" is always turned back on 99 | with: 100 | access_token: ${{ secrets.ACCESS_TOKEN }} 101 | branch: ${{ github.event.repository.default_branch }} 102 | enforce_admins: ${{ steps.disable_include_admins.outputs.initial_status }} 103 | ``` 104 | 105 | ## Github repository settings 106 | The Bot account must be an administrator. 107 | -------------------------------------------------------------------------------- /action.yml: -------------------------------------------------------------------------------- 1 | # action.yml 2 | name: 'Branch Protection Bot' 3 | author: https://github.com/benjefferies 4 | description: 'A bot tool to temporarily disable and re-enable "Include administrators" option in branch protection' 5 | branding: 6 | color: blue 7 | icon: unlock 8 | inputs: 9 | access_token: 10 | description: 'Github access token. https://help.github.com/en/github/authenticating-to-github/creating-a-personal-access-token-for-the-command-line' 11 | required: true 12 | owner: 13 | description: 'For example benjefferies for https://github.com/benjefferies/branch-protection-bot. If not set with repo GITHUB_REPOSITORY variable will be used' 14 | required: false 15 | repo: 16 | description: 'For example branch-protection-bot for https://github.com/benjefferies/branch-protection-bot. If not set with repo GITHUB_REPOSITORY variable will be used' 17 | required: false 18 | branch: 19 | description: 'Branch name' 20 | required: false 21 | default: 'master' 22 | retries: 23 | description: 'Number of times to retry before exiting' 24 | required: false 25 | default: 5 26 | enforce_admins: 27 | description: 'Flag to explicitly enable or disable "Include administrators"' 28 | required: false 29 | outputs: 30 | initial_status: 31 | description: "Output the current branch protection status of 'Include administrators' prior to any change" 32 | 33 | runs: 34 | using: 'docker' 35 | image: 'Dockerfile' 36 | env: 37 | ACCESS_TOKEN: ${{ inputs.access_token }} 38 | OWNER: ${{ inputs.owner }} 39 | REPO: ${{ inputs.repo }} 40 | BRANCH: ${{ inputs.branch }} 41 | RETRIES: ${{ inputs.retries }} 42 | ENFORCE_ADMINS: ${{ inputs.enforce_admins }} 43 | -------------------------------------------------------------------------------- /run.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | from time import sleep 3 | 4 | import configargparse 5 | from github3 import login 6 | from github3.exceptions import NotFoundError, GitHubException 7 | 8 | 9 | def toggle_enforce_admin(options): 10 | access_token, owner, repo_name, branch_name, retries, github_repository = options.access_token, options.owner, options.repo, options.branch, int(options.retries), options.github_repository 11 | if not owner and not repo_name and github_repository and "/" in github_repository: 12 | owner = github_repository.split("/")[0] 13 | repo_name = github_repository.split("/")[1] 14 | 15 | if owner == '' or repo_name == '': 16 | print('Owner and repo or GITHUB_REPOSITORY not set') 17 | raise RuntimeError 18 | enforce_admins = bool(strtobool(options.enforce_admins)) if options.enforce_admins is not None and not options.enforce_admins == '' else None 19 | # or using an access token 20 | print(f"Getting branch protection settings for {owner}/{repo_name}") 21 | protection = get_protection(access_token, branch_name, owner, repo_name) 22 | if protection is None: 23 | print("Branch is not protected. Skipping") 24 | return 25 | 26 | print(f"Enforce admins branch protection enabled? {protection.enforce_admins.enabled}") 27 | # save the current status for use later on if desired 28 | print(f"\"name=initial_status::{protection.enforce_admins.enabled}\" >> $GITHUB_OUTPUT") 29 | print(f"Setting enforce admins branch protection to {enforce_admins if enforce_admins is not None else not protection.enforce_admins.enabled}") 30 | for i in range(retries): 31 | try: 32 | if enforce_admins is False: 33 | disable(protection) 34 | return 35 | elif enforce_admins is True: 36 | enable(protection) 37 | return 38 | elif protection.enforce_admins.enabled: 39 | disable(protection) 40 | return 41 | elif not protection.enforce_admins.enabled: 42 | enable(protection) 43 | return 44 | except GitHubException: 45 | print(f"Failed to set enforce admins to {not protection.enforce_admins.enabled}. Retrying...") 46 | sleep(i ** 2) # Exponential back-off 47 | 48 | print(f"Failed to set enforce admins to {not protection.enforce_admins.enabled}.") 49 | exit(1) 50 | 51 | 52 | def get_protection(access_token, branch_name, owner, repo_name): 53 | gh = login(token=access_token) 54 | if gh is None: 55 | raise RuntimeError("Could not login. Have you provided credentials?") 56 | 57 | try: 58 | repo = gh.repository(owner, repo_name) 59 | except NotFoundError: 60 | print(f"Could not find repo https://github.com/{owner}/{repo_name}") 61 | raise 62 | branch = repo.branch(branch_name) 63 | try: 64 | protection = branch.protection() 65 | return protection 66 | except NotFoundError: 67 | print(f"Could not find branch protection for {owner}/{repo_name}/{branch_name}") 68 | 69 | 70 | def strtobool(val): 71 | """ 72 | Convert a string representation of truth to True or False. 73 | 74 | True values are 'y', 'yes', 't', 'true', 'on', and '1'; false values 75 | are 'n', 'no', 'f', 'false', 'off', and '0'. Raises ValueError if 76 | 'val' is anything else. 77 | """ 78 | val = val.lower() 79 | if val in ('y', 'yes', 't', 'true', 'on', '1'): 80 | return True 81 | elif val in ('n', 'no', 'f', 'false', 'off', '0'): 82 | return False 83 | else: 84 | raise ValueError(f"Invalid truth value {val}") 85 | 86 | 87 | def enable(protection): 88 | protection.enforce_admins.enable() 89 | 90 | 91 | def disable(protection): 92 | protection.enforce_admins.disable() 93 | 94 | 95 | if __name__ == '__main__': 96 | p = configargparse.ArgParser() 97 | p.add_argument('-t', '--access-token', env_var='ACCESS_TOKEN', required=True, help='Github access token. https://help.github.com/en/github/authenticating-to-github/creating-a-personal-access-token-for-the-command-line') 98 | p.add_argument('-o', '--owner', env_var='OWNER', required=False, default='', help='Owner. For example benjefferies for https://github.com/benjefferies/branch-protection-bot') 99 | p.add_argument('-r', '--repo', env_var='REPO', required=False, default='', help='Repo. For example branch-protection-bot for https://github.com/benjefferies/branch-protection-bot') 100 | p.add_argument('--github_repository', env_var='GITHUB_REPOSITORY', required=False, default='', help='Owner and repo. For example benjefferies/branch-protection-bot for https://github.com/benjefferies/branch-protection-bot') 101 | p.add_argument('-b', '--branch', env_var='BRANCH', default='master', help='Branch name') 102 | p.add_argument('--retries', env_var='RETRIES', default=5, help='Number of times to retry before exiting') 103 | p.add_argument('--enforce-admins', env_var='ENFORCE_ADMINS', default=None, help='Flag to explicitly enable or disable "Include administrators"') 104 | 105 | toggle_enforce_admin(p.parse_args()) 106 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [flake8] 2 | ignore = E501 3 | max-line-length = 88 4 | max-complexity = 18 5 | select = B,C,E,F,W,T4 6 | -------------------------------------------------------------------------------- /test_run.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | from unittest.mock import patch 3 | 4 | from run import toggle_enforce_admin 5 | 6 | 7 | class TestRun(unittest.TestCase): 8 | 9 | @patch('run.login') 10 | @patch('run.enable') 11 | def test_should_always_enable_force_admins(self, enable, login): 12 | # Given 13 | options = DotDict({ 14 | 'retries': 1, 15 | 'enforce_admins': 'true', 16 | 'owner': 'benjefferies', 17 | 'repo': 'branch-bot-protection', 18 | }) 19 | 20 | # When 21 | toggle_enforce_admin(options) 22 | 23 | # Then 24 | enable.assert_called_once() 25 | login.return_value.repository.assert_called_once_with('benjefferies', 'branch-bot-protection') 26 | 27 | @patch('run.enable') 28 | def test_should_always_enable_force_admins_when_enabled(self, enable): 29 | # Given 30 | options = DotDict({ 31 | 'retries': 1, 32 | 'enforce_admins': 'true', 33 | 'github_repository': 'benjefferies/branch-bot-protection' 34 | }) 35 | 36 | # When 37 | with patch('run.login') as mock: 38 | mock.return_value\ 39 | .repository.return_value\ 40 | .branch.return_value\ 41 | .protection.return_value\ 42 | .enforce_admins.enabled = True 43 | toggle_enforce_admin(options) 44 | 45 | # Then 46 | enable.assert_called_once() 47 | 48 | @patch('run.login') 49 | @patch('run.disable') 50 | def test_should_always_disable_force_admins(self, disable, login): 51 | # Given 52 | options = DotDict({ 53 | 'retries': 1, 54 | 'enforce_admins': 'false', 55 | 'github_repository': 'benjefferies/branch-bot-protection' 56 | }) 57 | 58 | # When 59 | toggle_enforce_admin(options) 60 | 61 | # Then 62 | disable.assert_called_once() 63 | 64 | @patch('run.disable') 65 | def test_should_always_disable_force_admins_when_disabled(self, disable): 66 | # Given 67 | options = DotDict({ 68 | 'retries': 1, 69 | 'enforce_admins': 'false', 70 | 'github_repository': 'benjefferies/branch-bot-protection' 71 | }) 72 | 73 | # When 74 | with patch('run.login') as mock: 75 | mock.return_value\ 76 | .repository.return_value\ 77 | .branch.return_value\ 78 | .protection.return_value\ 79 | .enforce_admins.enabled = False 80 | toggle_enforce_admin(options) 81 | 82 | # Then 83 | disable.assert_called_once() 84 | 85 | @patch('run.disable') 86 | def test_should_disable_force_admins(self, disable): 87 | # Given 88 | options = DotDict({ 89 | 'retries': 1, 90 | 'github_repository': 'benjefferies/branch-bot-protection' 91 | }) 92 | 93 | # When 94 | with patch('run.login') as mock: 95 | mock.return_value\ 96 | .repository.return_value\ 97 | .branch.return_value\ 98 | .protection.return_value\ 99 | .enforce_admins.enabled = True 100 | toggle_enforce_admin(options) 101 | 102 | # Then 103 | disable.assert_called_once() 104 | 105 | @patch('run.enable') 106 | def test_should_enable_force_admins(self, enable): 107 | # Given 108 | options = DotDict({ 109 | 'retries': 1, 110 | 'github_repository': 'benjefferies/branch-bot-protection' 111 | }) 112 | 113 | # When 114 | with patch('run.login') as mock: 115 | mock.return_value\ 116 | .repository.return_value\ 117 | .branch.return_value\ 118 | .protection.return_value\ 119 | .enforce_admins.enabled = False 120 | toggle_enforce_admin(options) 121 | 122 | # Then 123 | enable.assert_called_once() 124 | 125 | @patch('run.login') 126 | @patch('run.enable') 127 | def test_should_enable_force_admins_using_github_repository_environment_variables(self, enable, login): 128 | # Given 129 | options = DotDict({ 130 | 'retries': 1, 131 | 'enforce_admins': 'true', 132 | 'github_repository': 'benjefferies/branch-bot-protection' 133 | }) 134 | 135 | # When 136 | toggle_enforce_admin(options) 137 | 138 | # Then 139 | enable.assert_called_once() 140 | login.return_value.repository.assert_called_once_with('benjefferies', 'branch-bot-protection') 141 | 142 | def test_should_error_when_no_github_repository_or_owner_and_repo(self): 143 | # Given 144 | options = DotDict({ 145 | 'retries': 1, 146 | 'enforce_admins': 'true', 147 | }) 148 | 149 | # When 150 | def to_error(): 151 | toggle_enforce_admin(options) 152 | 153 | # Then 154 | self.assertRaises(RuntimeError, to_error) 155 | 156 | @patch('run.login') 157 | @patch('run.enable') 158 | def test_should_use_owner_repo_over_github_repository(self, enable, login): 159 | # Given 160 | options = DotDict({ 161 | 'retries': 1, 162 | 'enforce_admins': 'true', 163 | 'owner': 'benjefferies', 164 | 'repo': 'branch-protection-bot', 165 | 'github_repository': 'other/repo' 166 | }) 167 | 168 | # When 169 | toggle_enforce_admin(options) 170 | 171 | # Then 172 | enable.assert_called_once() 173 | login.return_value.repository.assert_called_once_with('benjefferies', 'branch-protection-bot') 174 | 175 | 176 | class DotDict(dict): 177 | def __getattr__(self, key): 178 | return self[key] if key in self else '' 179 | 180 | def __setattr__(self, key, val): 181 | if key in self.__dict__: 182 | self.__dict__[key] = val 183 | else: 184 | self[key] = val 185 | --------------------------------------------------------------------------------