├── .github ├── CODEOWNERS ├── linters │ ├── .hadolint.yaml │ ├── .isort.cfg │ ├── .shellcheckrc │ ├── .jscpd.json │ ├── .flake8 │ ├── .markdown-lint.yml │ ├── .mypy.ini │ ├── .textlintrc │ ├── .yaml-lint.yml │ └── .python-lint ├── ISSUE_TEMPLATE │ ├── config.yml │ ├── feature_request.yml │ └── bug_report.yml ├── workflows │ ├── docker-image.yml │ ├── pr-title.yml │ ├── auto-labeler.yml │ ├── stale.yaml │ ├── python-ci.yml │ ├── super-linter.yml │ ├── scorecard.yml │ └── release.yml ├── dependabot.yml ├── pull_request_template.md └── release-drafter.yml ├── requirements.txt ├── .coveragerc ├── requirements-test.txt ├── .vscode └── launch.json ├── .env-example ├── action.yml ├── Dockerfile ├── Makefile ├── LICENSE ├── .gitignore ├── auth.py ├── test_auth.py ├── test_open_contrib_pr.py ├── CONTRIBUTING.md ├── CODE_OF_CONDUCT.md ├── open_contrib_pr.py ├── CONTRIBUTING-template.md ├── env.py ├── test_env.py └── README.md /.github/CODEOWNERS: -------------------------------------------------------------------------------- 1 | * @github/ospo-github-actions 2 | -------------------------------------------------------------------------------- /.github/linters/.hadolint.yaml: -------------------------------------------------------------------------------- 1 | ignored: 2 | - DL3008 3 | -------------------------------------------------------------------------------- /.github/linters/.isort.cfg: -------------------------------------------------------------------------------- 1 | [isort] 2 | profile = black 3 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | github3.py==4.0.1 2 | python-dotenv==1.1.0 3 | -------------------------------------------------------------------------------- /.coveragerc: -------------------------------------------------------------------------------- 1 | [run] 2 | omit = 3 | # omit test files 4 | test_*.py 5 | -------------------------------------------------------------------------------- /.github/linters/.shellcheckrc: -------------------------------------------------------------------------------- 1 | # Don't suggest [ -n "$VAR" ] over [ ! -z "$VAR" ] 2 | disable=SC2129 3 | -------------------------------------------------------------------------------- /.github/linters/.jscpd.json: -------------------------------------------------------------------------------- 1 | { 2 | "threshold": 25, 3 | "ignore": ["test*"], 4 | "absolute": true 5 | } 6 | -------------------------------------------------------------------------------- /.github/linters/.flake8: -------------------------------------------------------------------------------- 1 | [flake8] 2 | max-line-length = 150 3 | exclude = venv,.venv,.git,__pycache__ 4 | extend-ignore = C901 5 | statistics = True 6 | -------------------------------------------------------------------------------- /.github/linters/.markdown-lint.yml: -------------------------------------------------------------------------------- 1 | --- 2 | # line length 3 | MD013: false 4 | # singe h1 5 | MD025: false 6 | # duplicate headers 7 | MD024: false 8 | -------------------------------------------------------------------------------- /.github/linters/.mypy.ini: -------------------------------------------------------------------------------- 1 | [mypy] 2 | disable_error_code = attr-defined, import-not-found 3 | 4 | [mypy-github3.*] 5 | ignore_missing_imports = True 6 | -------------------------------------------------------------------------------- /.github/linters/.textlintrc: -------------------------------------------------------------------------------- 1 | { 2 | "filters": { 3 | "comments": true 4 | }, 5 | "rules": { 6 | "terminology": { 7 | "severity": "warning" 8 | } 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /requirements-test.txt: -------------------------------------------------------------------------------- 1 | black==25.1.0 2 | flake8==7.2.0 3 | mypy==1.15.0 4 | mypy-extensions==1.1.0 5 | pylint==3.3.6 6 | pytest==8.3.5 7 | pytest-cov==6.1.1 8 | types-requests==2.32.0.20250328 9 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0.2.0", 3 | "configurations": [ 4 | { 5 | "name": "Python: Current File", 6 | "type": "python", 7 | "request": "launch", 8 | "program": "${file}", 9 | "console": "integratedTerminal" 10 | } 11 | ] 12 | } 13 | -------------------------------------------------------------------------------- /.env-example: -------------------------------------------------------------------------------- 1 | GH_ACTOR = "" 2 | GH_ENTERPRISE_URL = "" 3 | GH_TOKEN = "" 4 | ORGANIZATION = "" 5 | PR_TITLE = "" 6 | PR_BODY = "" 7 | REPOS_JSON_LOCATION = "" 8 | 9 | # GITHUB APP 10 | GH_APP_ID = "" 11 | GH_INSTALLATION_ID = "" 12 | GH_PRIVATE_KEY = "" 13 | GITHUB_APP_ENTERPRISE_ONLY = "" 14 | -------------------------------------------------------------------------------- /action.yml: -------------------------------------------------------------------------------- 1 | --- 2 | name: "Automatic Contrib PRs" 3 | author: "github" 4 | description: "A GitHub Action that opens adds CONTRIBUTING.md file in repositories that dont have them." 5 | runs: 6 | using: "docker" 7 | image: "docker://ghcr.io/github/automatic-contrib-prs:v2" 8 | branding: 9 | icon: "book" 10 | color: "black" 11 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/config.yml: -------------------------------------------------------------------------------- 1 | blank_issues_enabled: false 2 | 3 | contact_links: 4 | - name: Ask a question 5 | url: https://github.com/github/automatic-contrib-prs/discussions/new 6 | about: Ask a question or start a discussion 7 | - name: GitHub OSPO GitHub Action Overall Issue 8 | url: https://github.com/github/github-ospo/issues/new 9 | about: File issue for multiple GitHub OSPO GitHub Actions 10 | -------------------------------------------------------------------------------- /.github/workflows/docker-image.yml: -------------------------------------------------------------------------------- 1 | --- 2 | name: Docker Image CI 3 | 4 | on: 5 | push: 6 | branches: [main] 7 | pull_request: 8 | branches: [main] 9 | 10 | permissions: 11 | contents: read 12 | 13 | jobs: 14 | build: 15 | runs-on: ubuntu-latest 16 | steps: 17 | - uses: actions/checkout@v4.2.2 18 | - name: Build the Docker image 19 | run: docker build . --file Dockerfile --platform linux/amd64 --tag automatic-contributors-pr:"$(date +%s)" 20 | -------------------------------------------------------------------------------- /.github/workflows/pr-title.yml: -------------------------------------------------------------------------------- 1 | ## Reference: https://github.com/amannn/action-semantic-pull-request 2 | --- 3 | name: "Lint PR Title" 4 | on: 5 | pull_request_target: 6 | types: [opened, reopened, edited, synchronize] 7 | permissions: 8 | contents: read 9 | jobs: 10 | main: 11 | permissions: 12 | contents: read 13 | pull-requests: read 14 | statuses: write 15 | uses: github/ospo-reusable-workflows/.github/workflows/pr-title.yaml@10cfc2f9be5fce5e90150dfbffc7c0f4e68108ab 16 | secrets: 17 | github-token: ${{ secrets.GITHUB_TOKEN }} 18 | -------------------------------------------------------------------------------- /.github/workflows/auto-labeler.yml: -------------------------------------------------------------------------------- 1 | --- 2 | name: Auto Labeler 3 | on: 4 | # pull_request_target event is required for autolabeler to support all PRs including forks 5 | pull_request_target: 6 | types: [opened, reopened, edited, synchronize] 7 | permissions: 8 | contents: read 9 | jobs: 10 | main: 11 | permissions: 12 | contents: read 13 | pull-requests: write 14 | uses: github/ospo-reusable-workflows/.github/workflows/auto-labeler.yaml@10cfc2f9be5fce5e90150dfbffc7c0f4e68108ab 15 | with: 16 | config-name: release-drafter.yml 17 | secrets: 18 | github-token: ${{ secrets.GITHUB_TOKEN }} 19 | -------------------------------------------------------------------------------- /.github/workflows/stale.yaml: -------------------------------------------------------------------------------- 1 | name: "Close stale issues" 2 | on: 3 | schedule: 4 | - cron: "30 1 * * *" 5 | permissions: 6 | contents: read 7 | jobs: 8 | stale: 9 | runs-on: ubuntu-latest 10 | permissions: 11 | issues: write 12 | pull-requests: read 13 | steps: 14 | - uses: actions/stale@v9.1.0 15 | with: 16 | stale-issue-message: "This issue is stale because it has been open 21 days with no activity. Remove stale label or comment or this will be closed in 14 days." 17 | close-issue-message: "This issue was closed because it has been stalled for 35 days with no activity." 18 | days-before-stale: 21 19 | days-before-close: 14 20 | days-before-pr-close: -1 21 | exempt-issue-labels: keep 22 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | #checkov:skip=CKV_DOCKER_2 2 | #checkov:skip=CKV_DOCKER_3 3 | FROM python:3.13-slim@sha256:21e39cf1815802d4c6f89a0d3a166cc67ce58f95b6d1639e68a394c99310d2e5 4 | 5 | WORKDIR /action/workspace 6 | COPY requirements.txt CONTRIBUTING-template.md open_contrib_pr.py /action/workspace/ 7 | 8 | RUN python3 -m pip install --no-cache-dir -r requirements.txt \ 9 | && apt-get -y update \ 10 | && apt-get -y install --no-install-recommends git=1:2.39.5-0+deb12u2 \ 11 | && rm -rf /var/lib/apt/lists/* 12 | 13 | CMD ["/action/workspace/open_contrib_pr.py"] 14 | ENTRYPOINT ["python3", "-u"] 15 | 16 | # To run ineractive debug on the docker container 17 | # 1. Comment out the above CMD and ENTRYPOINT lines 18 | # 2. Uncomment the ENTRYPOINT line below 19 | 20 | #ENTRYPOINT ["bash"] 21 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | .PHONY: test 2 | test: 3 | pytest -v --cov=. --cov-config=.coveragerc --cov-fail-under=60 --cov-report term-missing 4 | 5 | .PHONY: clean 6 | clean: 7 | rm -rf .pytest_cache .coverage __pycache__ 8 | 9 | .PHONY: lint 10 | lint: 11 | # stop the build if there are Python syntax errors or undefined names 12 | flake8 . --config=.github/linters/.flake8 --count --select=E9,F63,F7,F82 --show-source 13 | # exit-zero treats all errors as warnings. The GitHub editor is 127 chars wide 14 | flake8 . --config=.github/linters/.flake8 --count --exit-zero --max-complexity=15 --max-line-length=150 15 | isort --settings-file=.github/linters/.isort.cfg . 16 | pylint --rcfile=.github/linters/.python-lint --fail-under=9.0 *.py 17 | mypy --config-file=.github/linters/.mypy.ini *.py 18 | black . 19 | -------------------------------------------------------------------------------- /.github/workflows/python-ci.yml: -------------------------------------------------------------------------------- 1 | --- 2 | name: Python package 3 | 4 | on: 5 | push: 6 | branches: [main] 7 | pull_request: 8 | branches: [main] 9 | 10 | permissions: 11 | contents: read 12 | 13 | jobs: 14 | build: 15 | runs-on: ubuntu-latest 16 | strategy: 17 | matrix: 18 | python-version: ["3.10", "3.11", "3.12"] 19 | 20 | steps: 21 | - uses: actions/checkout@v4.2.2 22 | - name: Set up Python ${{ matrix.python-version }} 23 | uses: actions/setup-python@v5.6.0 24 | with: 25 | python-version: ${{ matrix.python-version }} 26 | - name: Install dependencies 27 | run: | 28 | python -m pip install --upgrade pip 29 | pip install -r requirements.txt -r requirements-test.txt 30 | - name: Lint 31 | run: | 32 | make lint 33 | - name: Test with pytest 34 | run: | 35 | make test 36 | -------------------------------------------------------------------------------- /.github/workflows/super-linter.yml: -------------------------------------------------------------------------------- 1 | --- 2 | name: Lint Code Base 3 | 4 | on: 5 | pull_request: 6 | branches: [main] 7 | 8 | permissions: 9 | contents: read 10 | 11 | jobs: 12 | build: 13 | name: Lint Code Base 14 | runs-on: ubuntu-latest 15 | 16 | permissions: 17 | contents: read 18 | packages: read 19 | statuses: write 20 | 21 | steps: 22 | - name: Checkout Code 23 | uses: actions/checkout@v4.2.2 24 | with: 25 | fetch-depth: 0 26 | - name: Install dependencies 27 | run: | 28 | python -m pip install --upgrade pip 29 | pip install -r requirements.txt -r requirements-test.txt 30 | - name: Lint Code Base 31 | uses: super-linter/super-linter@4e8a7c2bf106c4c766c816b35ec612638dc9b6b2 32 | env: 33 | DEFAULT_BRANCH: main 34 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 35 | GITHUB_ACTIONS_COMMAND_ARGS: -shellcheck= 36 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | --- 2 | version: 2 3 | updates: 4 | - package-ecosystem: "pip" 5 | directory: "/" 6 | schedule: 7 | interval: "weekly" 8 | commit-message: 9 | prefix: "chore(deps)" 10 | groups: 11 | dependencies: 12 | applies-to: version-updates 13 | update-types: 14 | - "minor" 15 | - "patch" 16 | - package-ecosystem: "github-actions" 17 | directory: "/" 18 | schedule: 19 | interval: "weekly" 20 | commit-message: 21 | prefix: "chore(deps)" 22 | groups: 23 | dependencies: 24 | applies-to: version-updates 25 | update-types: 26 | - "minor" 27 | - "patch" 28 | - package-ecosystem: "docker" 29 | directory: "/" 30 | schedule: 31 | interval: "weekly" 32 | commit-message: 33 | prefix: "chore(deps)" 34 | groups: 35 | dependencies: 36 | applies-to: version-updates 37 | update-types: 38 | - "minor" 39 | - "patch" 40 | -------------------------------------------------------------------------------- /.github/pull_request_template.md: -------------------------------------------------------------------------------- 1 | # Pull Request 2 | 3 | 11 | 12 | ## Proposed Changes 13 | 14 | 15 | 16 | ## Readiness Checklist 17 | 18 | ### Author/Contributor 19 | 20 | - [ ] If documentation is needed for this change, has that been included in this pull request 21 | - [ ] run `make lint` and fix any issues that you have introduced 22 | - [ ] run `make test` and ensure you have test coverage for the lines you are introducing 23 | - [ ] If publishing new data to the public (scorecards, security scan results, code quality results, live dashboards, etc.), please request review from `@jeffrey-luszcz` 24 | 25 | ### Reviewer 26 | 27 | - [ ] Label as either `bug`, `documentation`, `enhancement`, `infrastructure`, `maintenance` or `breaking` 28 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 GitHub 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 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.yml: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | description: Suggest an idea for this project 4 | labels: 5 | - enhancement 6 | body: 7 | - type: textarea 8 | attributes: 9 | label: Is your feature request related to a problem? 10 | description: A clear and concise description of what the problem is. Please describe. 11 | placeholder: | 12 | Ex. I'm always frustrated when [...] 13 | validations: 14 | required: false 15 | 16 | - type: textarea 17 | attributes: 18 | label: Describe the solution you'd like 19 | description: A clear and concise description of what you want to happen. 20 | validations: 21 | required: true 22 | 23 | - type: textarea 24 | attributes: 25 | label: Describe alternatives you've considered 26 | description: A clear and concise description of any alternative solutions or features you've considered. 27 | validations: 28 | required: false 29 | 30 | - type: textarea 31 | attributes: 32 | label: Additional context 33 | description: Add any other context or screenshots about the feature request here. 34 | validations: 35 | required: false 36 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.yml: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | description: Create a report to help us improve 4 | labels: 5 | - bug 6 | body: 7 | - type: textarea 8 | attributes: 9 | label: Describe the bug 10 | description: A clear and concise description of what the bug is. 11 | validations: 12 | required: true 13 | 14 | - type: textarea 15 | attributes: 16 | label: To Reproduce 17 | description: Steps to reproduce the behavior 18 | placeholder: | 19 | 1. Go to '...' 20 | 2. Click on '....' 21 | 3. Scroll down to '....' 22 | 4. See error 23 | validations: 24 | required: true 25 | 26 | - type: textarea 27 | attributes: 28 | label: Expected behavior 29 | description: A clear and concise description of what you expected to happen. 30 | validations: 31 | required: true 32 | 33 | - type: textarea 34 | attributes: 35 | label: Screenshots 36 | description: If applicable, add screenshots to help explain your problem. 37 | validations: 38 | required: false 39 | 40 | - type: textarea 41 | attributes: 42 | label: Additional context 43 | description: Add any other context about the problem here. 44 | validations: 45 | required: false 46 | -------------------------------------------------------------------------------- /.github/linters/.yaml-lint.yml: -------------------------------------------------------------------------------- 1 | --- 2 | rules: 3 | braces: 4 | level: warning 5 | min-spaces-inside: 0 6 | max-spaces-inside: 0 7 | min-spaces-inside-empty: 1 8 | max-spaces-inside-empty: 5 9 | brackets: 10 | level: warning 11 | min-spaces-inside: 0 12 | max-spaces-inside: 0 13 | min-spaces-inside-empty: 1 14 | max-spaces-inside-empty: 5 15 | colons: 16 | level: warning 17 | max-spaces-before: 0 18 | max-spaces-after: 1 19 | commas: 20 | level: warning 21 | max-spaces-before: 0 22 | min-spaces-after: 1 23 | max-spaces-after: 1 24 | comments: disable 25 | comments-indentation: disable 26 | document-end: disable 27 | document-start: 28 | level: warning 29 | present: true 30 | empty-lines: 31 | level: warning 32 | max: 2 33 | max-start: 0 34 | max-end: 0 35 | hyphens: 36 | level: warning 37 | max-spaces-after: 1 38 | indentation: 39 | level: warning 40 | spaces: consistent 41 | indent-sequences: true 42 | check-multi-line-strings: false 43 | key-duplicates: enable 44 | line-length: 45 | level: warning 46 | max: 1024 47 | allow-non-breakable-words: true 48 | allow-non-breakable-inline-mappings: true 49 | new-line-at-end-of-file: disable 50 | new-lines: 51 | type: unix 52 | trailing-spaces: disable 53 | -------------------------------------------------------------------------------- /.github/workflows/scorecard.yml: -------------------------------------------------------------------------------- 1 | --- 2 | name: Scorecard supply-chain security 3 | on: 4 | workflow_dispatch: 5 | # For Branch-Protection check (for repo branch protection or rules). 6 | # Only the default branch is supported. See 7 | # https://github.com/ossf/scorecard/blob/main/docs/checks.md#branch-protection 8 | branch_protection_rule: 9 | # To guarantee Maintained check is occasionally updated. See 10 | # https://github.com/ossf/scorecard/blob/main/docs/checks.md#maintained 11 | schedule: 12 | - cron: "29 11 * * 6" 13 | push: 14 | branches: [main] 15 | 16 | permissions: read-all 17 | 18 | jobs: 19 | analysis: 20 | name: Merge to Main Scorecard analysis 21 | runs-on: ubuntu-latest 22 | permissions: 23 | security-events: write 24 | id-token: write 25 | 26 | steps: 27 | - name: "Checkout code" 28 | uses: actions/checkout@v4.2.2 29 | with: 30 | persist-credentials: false 31 | 32 | - name: "Run analysis" 33 | uses: ossf/scorecard-action@f49aabe0b5af0936a0987cfb85d86b75731b0186 # v2.4.1 34 | with: 35 | results_file: results.sarif 36 | results_format: sarif 37 | publish_results: true 38 | - name: "Upload artifact" 39 | uses: actions/upload-artifact@v4.6.2 40 | with: 41 | name: SARIF file 42 | path: results.sarif 43 | retention-days: 5 44 | - name: "Upload to code-scanning" 45 | uses: github/codeql-action/upload-sarif@28deaeda66b76a05916b6923827895f2b14ab387 # v3.28.16 46 | with: 47 | sarif_file: results.sarif 48 | -------------------------------------------------------------------------------- /.github/release-drafter.yml: -------------------------------------------------------------------------------- 1 | --- 2 | name-template: "v$RESOLVED_VERSION" 3 | tag-template: "v$RESOLVED_VERSION" 4 | template: | 5 | # Changelog 6 | $CHANGES 7 | 8 | See details of [all code changes](https://github.com/$OWNER/$REPOSITORY/compare/$PREVIOUS_TAG...v$RESOLVED_VERSION) since previous release 9 | 10 | categories: 11 | - title: "🚀 Features" 12 | labels: 13 | - "feature" 14 | - "enhancement" 15 | - title: "🐛 Bug Fixes" 16 | labels: 17 | - "fix" 18 | - "bugfix" 19 | - "bug" 20 | - title: "🧰 Maintenance" 21 | labels: 22 | - "infrastructure" 23 | - "automation" 24 | - "documentation" 25 | - "dependencies" 26 | - "maintenance" 27 | - "revert" 28 | - title: "🏎 Performance" 29 | label: "performance" 30 | change-template: "- $TITLE @$AUTHOR (#$NUMBER)" 31 | version-resolver: 32 | major: 33 | labels: 34 | - "breaking" 35 | - "major" 36 | minor: 37 | labels: 38 | - "enhancement" 39 | - "feature" 40 | - "minor" 41 | patch: 42 | labels: 43 | - "documentation" 44 | - "fix" 45 | - "maintenance" 46 | - "patch" 47 | default: patch 48 | autolabeler: 49 | - label: "automation" 50 | title: 51 | - "/^(build|ci|perf|refactor|test).*/i" 52 | - label: "enhancement" 53 | title: 54 | - "/^(style).*/i" 55 | - label: "documentation" 56 | title: 57 | - "/^(docs).*/i" 58 | - label: "feature" 59 | title: 60 | - "/^(feat).*/i" 61 | - label: "fix" 62 | title: 63 | - "/^(fix).*/i" 64 | - label: "infrastructure" 65 | title: 66 | - "/^(infrastructure).*/i" 67 | - label: "maintenance" 68 | title: 69 | - "/^(chore|maintenance).*/i" 70 | - label: "revert" 71 | title: 72 | - "/^(revert).*/i" 73 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | --- 2 | name: Release 3 | on: 4 | workflow_dispatch: 5 | pull_request_target: 6 | types: [closed] 7 | branches: [main] 8 | permissions: 9 | contents: read 10 | jobs: 11 | release: 12 | permissions: 13 | contents: write 14 | pull-requests: read 15 | uses: github/ospo-reusable-workflows/.github/workflows/release.yaml@10cfc2f9be5fce5e90150dfbffc7c0f4e68108ab 16 | with: 17 | publish: true 18 | release-config-name: release-drafter.yml 19 | secrets: 20 | github-token: ${{ secrets.GITHUB_TOKEN }} 21 | release_image: 22 | needs: release 23 | permissions: 24 | contents: read 25 | packages: write 26 | id-token: write 27 | attestations: write 28 | uses: github/ospo-reusable-workflows/.github/workflows/release-image.yaml@10cfc2f9be5fce5e90150dfbffc7c0f4e68108ab 29 | with: 30 | image-name: ${{ github.repository }} 31 | full-tag: ${{ needs.release.outputs.full-tag }} 32 | short-tag: ${{ needs.release.outputs.short-tag }} 33 | secrets: 34 | github-token: ${{ secrets.GITHUB_TOKEN }} 35 | image-registry: ghcr.io 36 | image-registry-username: ${{ github.actor }} 37 | image-registry-password: ${{ secrets.GITHUB_TOKEN }} 38 | release_discussion: 39 | needs: release 40 | permissions: 41 | contents: read 42 | discussions: write 43 | uses: github/ospo-reusable-workflows/.github/workflows/release-discussion.yaml@10cfc2f9be5fce5e90150dfbffc7c0f4e68108ab 44 | with: 45 | full-tag: ${{ needs.release.outputs.full-tag }} 46 | body: ${{ needs.release.outputs.body }} 47 | secrets: 48 | github-token: ${{ secrets.GITHUB_TOKEN }} 49 | discussion-repository-id: ${{ secrets.RELEASE_DISCUSSION_REPOSITORY_ID }} 50 | discussion-category-id: ${{ secrets.RELEASE_DISCUSSION_CATEGORY_ID }} 51 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # secrets 2 | .env 3 | 4 | # Byte-compiled / optimized / DLL files 5 | __pycache__/ 6 | *.py[cod] 7 | *$py.class 8 | 9 | # C extensions 10 | *.so 11 | 12 | # Distribution / packaging 13 | .Python 14 | build/ 15 | develop-eggs/ 16 | dist/ 17 | downloads/ 18 | eggs/ 19 | .eggs/ 20 | lib/ 21 | lib64/ 22 | parts/ 23 | sdist/ 24 | var/ 25 | wheels/ 26 | pip-wheel-metadata/ 27 | share/python-wheels/ 28 | *.egg-info/ 29 | .installed.cfg 30 | *.egg 31 | MANIFEST 32 | 33 | # PyInstaller 34 | # Usually these files are written by a python script from a template 35 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 36 | *.manifest 37 | *.spec 38 | 39 | # Installer logs 40 | pip-log.txt 41 | pip-delete-this-directory.txt 42 | 43 | # Unit test / coverage reports 44 | htmlcov/ 45 | .tox/ 46 | .nox/ 47 | .coverage 48 | .coverage.* 49 | .cache 50 | nosetests.xml 51 | coverage.xml 52 | *.cover 53 | *.py,cover 54 | .hypothesis/ 55 | .pytest_cache/ 56 | 57 | # Translations 58 | *.mo 59 | *.pot 60 | 61 | # Django stuff: 62 | *.log 63 | local_settings.py 64 | db.sqlite3 65 | db.sqlite3-journal 66 | 67 | # Flask stuff: 68 | instance/ 69 | .webassets-cache 70 | 71 | # Scrapy stuff: 72 | .scrapy 73 | 74 | # Sphinx documentation 75 | docs/_build/ 76 | 77 | # PyBuilder 78 | target/ 79 | 80 | # Jupyter Notebook 81 | .ipynb_checkpoints 82 | 83 | # IPython 84 | profile_default/ 85 | ipython_config.py 86 | 87 | # pyenv 88 | .python-version 89 | 90 | # pipenv 91 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 92 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 93 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 94 | # install all needed dependencies. 95 | #Pipfile.lock 96 | 97 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow 98 | __pypackages__/ 99 | 100 | # Celery stuff 101 | celerybeat-schedule 102 | celerybeat.pid 103 | 104 | # SageMath parsed files 105 | *.sage.py 106 | 107 | # Environments 108 | .env 109 | .venv 110 | env/ 111 | venv/ 112 | ENV/ 113 | env.bak/ 114 | venv.bak/ 115 | 116 | # Spyder project settings 117 | .spyderproject 118 | .spyproject 119 | 120 | # Rope project settings 121 | .ropeproject 122 | 123 | # mkdocs documentation 124 | /site 125 | 126 | # mypy 127 | .mypy_cache/ 128 | .dmypy.json 129 | dmypy.json 130 | 131 | # Pyre type checker 132 | .pyre/ 133 | -------------------------------------------------------------------------------- /auth.py: -------------------------------------------------------------------------------- 1 | """This is the module that contains functions related to authenticating to GitHub with a personal access token.""" 2 | 3 | import github3 4 | import requests 5 | 6 | 7 | def auth_to_github( 8 | token: str | None, 9 | gh_app_id: int | None, 10 | gh_app_installation_id: int | None, 11 | gh_app_private_key_bytes: bytes, 12 | ghe: str | None, 13 | gh_app_enterprise_only: bool, 14 | ) -> github3.GitHub: 15 | """ 16 | Connect to GitHub.com or GitHub Enterprise, depending on env variables. 17 | 18 | Args: 19 | token (str): the GitHub personal access token 20 | gh_app_id (int | None): the GitHub App ID 21 | gh_app_installation_id (int | None): the GitHub App Installation ID 22 | gh_app_private_key_bytes (bytes): the GitHub App Private Key 23 | ghe (str): the GitHub Enterprise URL 24 | gh_app_enterprise_only (bool): Set this to true if the GH APP is created on GHE and needs to communicate with GHE api only 25 | 26 | Returns: 27 | github3.GitHub: the GitHub connection object 28 | """ 29 | if gh_app_id and gh_app_private_key_bytes and gh_app_installation_id: 30 | if ghe and gh_app_enterprise_only: 31 | gh = github3.github.GitHubEnterprise(url=ghe) 32 | else: 33 | gh = github3.github.GitHub() 34 | gh.login_as_app_installation( 35 | gh_app_private_key_bytes, gh_app_id, gh_app_installation_id 36 | ) 37 | github_connection = gh 38 | elif ghe and token: 39 | github_connection = github3.github.GitHubEnterprise(url=ghe, token=token) 40 | elif token: 41 | github_connection = github3.login(token=token) 42 | else: 43 | raise ValueError( 44 | "GH_TOKEN or the set of [GH_APP_ID, GH_APP_INSTALLATION_ID, GH_APP_PRIVATE_KEY] environment variables are not set" 45 | ) 46 | 47 | if not github_connection: 48 | raise ValueError("Unable to authenticate to GitHub") 49 | return github_connection # type: ignore 50 | 51 | 52 | def get_github_app_installation_token( 53 | ghe: str | None, 54 | gh_app_id: int | None, 55 | gh_app_private_key_bytes: bytes, 56 | gh_app_installation_id: int | None, 57 | ) -> str | None: 58 | """ 59 | Get a GitHub App Installation token. 60 | API: https://docs.github.com/en/apps/creating-github-apps/authenticating-with-a-github-app/authenticating-as-a-github-app-installation 61 | 62 | Args: 63 | ghe (str): the GitHub Enterprise endpoint 64 | gh_app_id (str): the GitHub App ID 65 | gh_app_private_key_bytes (bytes): the GitHub App Private Key 66 | gh_app_installation_id (str): the GitHub App Installation ID 67 | 68 | Returns: 69 | str: the GitHub App token 70 | """ 71 | jwt_headers = github3.apps.create_jwt_headers(gh_app_private_key_bytes, gh_app_id) 72 | api_endpoint = f"{ghe}/api/v3" if ghe else "https://api.github.com" 73 | url = f"{api_endpoint}/app/installations/{gh_app_installation_id}/access_tokens" 74 | 75 | try: 76 | response = requests.post(url, headers=jwt_headers, json=None, timeout=5) 77 | response.raise_for_status() 78 | except requests.exceptions.RequestException as e: 79 | print(f"Request failed: {e}") 80 | return None 81 | return response.json().get("token") 82 | -------------------------------------------------------------------------------- /test_auth.py: -------------------------------------------------------------------------------- 1 | """Test cases for the auth module.""" 2 | 3 | import unittest 4 | from unittest.mock import MagicMock, patch 5 | 6 | import auth 7 | import requests 8 | 9 | 10 | class TestAuth(unittest.TestCase): 11 | """ 12 | Test case for the auth module. 13 | """ 14 | 15 | @patch("github3.login") 16 | def test_auth_to_github_with_token(self, mock_login): 17 | """ 18 | Test the auth_to_github function when the token is provided. 19 | """ 20 | mock_login.return_value = "Authenticated to GitHub.com" 21 | 22 | result = auth.auth_to_github("token", "", "", b"", "", False) 23 | 24 | self.assertEqual(result, "Authenticated to GitHub.com") 25 | 26 | def test_auth_to_github_without_token(self): 27 | """ 28 | Test the auth_to_github function when the token is not provided. 29 | Expect a ValueError to be raised. 30 | """ 31 | with self.assertRaises(ValueError) as context_manager: 32 | auth.auth_to_github("", "", "", b"", "", False) 33 | the_exception = context_manager.exception 34 | self.assertEqual( 35 | str(the_exception), 36 | "GH_TOKEN or the set of [GH_APP_ID, GH_APP_INSTALLATION_ID, GH_APP_PRIVATE_KEY] environment variables are not set", 37 | ) 38 | 39 | @patch("github3.github.GitHubEnterprise") 40 | def test_auth_to_github_with_ghe(self, mock_ghe): 41 | """ 42 | Test the auth_to_github function when the GitHub Enterprise URL is provided. 43 | """ 44 | mock_ghe.return_value = "Authenticated to GitHub Enterprise" 45 | result = auth.auth_to_github( 46 | "token", "", "", b"", "https://github.example.com", False 47 | ) 48 | 49 | self.assertEqual(result, "Authenticated to GitHub Enterprise") 50 | 51 | @patch("github3.github.GitHubEnterprise") 52 | def test_auth_to_github_with_ghe_and_ghe_app(self, mock_ghe): 53 | """ 54 | Test the auth_to_github function when the GitHub Enterprise URL is provided and the app was created in GitHub Enterprise URL. 55 | """ 56 | mock = mock_ghe.return_value 57 | mock.login_as_app_installation = MagicMock(return_value=True) 58 | result = auth.auth_to_github( 59 | "", "123", "123", b"123", "https://github.example.com", True 60 | ) 61 | mock.login_as_app_installation.assert_called_once() 62 | self.assertEqual(result, mock) 63 | 64 | @patch("github3.github.GitHub") 65 | def test_auth_to_github_with_app(self, mock_gh): 66 | """ 67 | Test the auth_to_github function when app credentials are provided 68 | """ 69 | mock = mock_gh.return_value 70 | mock.login_as_app_installation = MagicMock(return_value=True) 71 | result = auth.auth_to_github( 72 | "", "123", "123", b"123", "https://github.example.com", False 73 | ) 74 | mock.login_as_app_installation.assert_called_once() 75 | self.assertEqual(result, mock) 76 | 77 | @patch("github3.apps.create_jwt_headers", MagicMock(return_value="gh_token")) 78 | @patch("requests.post") 79 | def test_get_github_app_installation_token(self, mock_post): 80 | """ 81 | Test the get_github_app_installation_token function. 82 | """ 83 | dummy_token = "dummytoken" 84 | mock_response = MagicMock() 85 | mock_response.raise_for_status.return_value = None 86 | mock_response.json.return_value = {"token": dummy_token} 87 | mock_post.return_value = mock_response 88 | 89 | result = auth.get_github_app_installation_token( 90 | b"ghe", "gh_private_token", "gh_app_id", "gh_installation_id" 91 | ) 92 | 93 | self.assertEqual(result, dummy_token) 94 | 95 | @patch("github3.apps.create_jwt_headers", MagicMock(return_value="gh_token")) 96 | @patch("auth.requests.post") 97 | def test_get_github_app_installation_token_request_failure(self, mock_post): 98 | """ 99 | Test the get_github_app_installation_token function returns None when the request fails. 100 | """ 101 | # Mock the post request to raise a RequestException 102 | mock_post.side_effect = requests.exceptions.RequestException("Request failed") 103 | 104 | # Call the function with test data 105 | result = auth.get_github_app_installation_token( 106 | ghe="https://api.github.com", 107 | gh_app_id=12345, 108 | gh_app_private_key_bytes=b"private_key", 109 | gh_app_installation_id=678910, 110 | ) 111 | 112 | # Assert that the result is None 113 | self.assertIsNone(result) 114 | 115 | @patch("github3.login") 116 | def test_auth_to_github_invalid_credentials(self, mock_login): 117 | """ 118 | Test the auth_to_github function raises correct ValueError 119 | when credentials are present but incorrect. 120 | """ 121 | mock_login.return_value = None 122 | with self.assertRaises(ValueError) as context_manager: 123 | auth.auth_to_github("not_a_valid_token", "", "", b"", "", False) 124 | 125 | the_exception = context_manager.exception 126 | self.assertEqual( 127 | str(the_exception), 128 | "Unable to authenticate to GitHub", 129 | ) 130 | 131 | 132 | if __name__ == "__main__": 133 | unittest.main() 134 | -------------------------------------------------------------------------------- /test_open_contrib_pr.py: -------------------------------------------------------------------------------- 1 | """Tests for the open_contrib_pr.py functions.""" 2 | 3 | import unittest 4 | from unittest.mock import MagicMock, mock_open, patch 5 | 6 | import github3 7 | from open_contrib_pr import clone_repository, create_pull_request, get_repos_json 8 | 9 | 10 | class TestOpenContribPR(unittest.TestCase): 11 | """Test case for the open_contrib_pr module.""" 12 | 13 | @patch( 14 | "builtins.open", 15 | new_callable=mock_open, 16 | read_data='{"repos": ["repo1", "repo2"]}', 17 | ) 18 | @patch("os.system") 19 | def test_get_repos_json(self, mock_system, mock_file): 20 | """ 21 | Test the get_repos_json function. 22 | """ 23 | gh_actor = "test_actor" 24 | repos_json_location = "test_location" 25 | token = "test_token" 26 | endpoint = "test_endpoint" 27 | 28 | expected_repos = {"repos": ["repo1", "repo2"]} 29 | 30 | result = get_repos_json(gh_actor, repos_json_location, token, endpoint) 31 | 32 | mock_system.assert_called_once_with( 33 | f"git clone https://{gh_actor}:{token}@{endpoint}/{repos_json_location}" 34 | ) 35 | mock_file.assert_called_once_with( 36 | str(repos_json_location), "r", encoding="utf-8" 37 | ) 38 | self.assertEqual(result, expected_repos) 39 | 40 | 41 | class TestCloneRepository(unittest.TestCase): 42 | """Test case for the clone_repository function.""" 43 | 44 | @patch("os.system") 45 | def test_clone_repository_success(self, mock_system): 46 | """ 47 | Test the clone_repository function when the clone is successful. 48 | """ 49 | mock_system.return_value = 0 # Simulate successful clone 50 | 51 | result = clone_repository( 52 | gh_actor="test_actor", 53 | token="test_token", 54 | endpoint="test_endpoint", 55 | repo={"full_name": "test_actor/test_repo", "name": "test_repo"}, 56 | ) 57 | 58 | mock_system.assert_called_once_with( 59 | "git clone https://test_actor:test_token@test_endpoint/test_actor/test_repo" 60 | ) 61 | self.assertEqual(result, "test_repo") 62 | 63 | @patch("os.system") 64 | def test_clone_repository_failure(self, mock_system): 65 | """ 66 | Test the clone_repository function when the clone fails. 67 | """ 68 | mock_system.side_effect = OSError("Clone failed") # Simulate clone failure 69 | 70 | result = clone_repository( 71 | gh_actor="test_actor", 72 | token="test_token", 73 | endpoint="test_endpoint", 74 | repo={"full_name": "test_actor/test_repo", "name": "test_repo"}, 75 | ) 76 | 77 | mock_system.assert_called_once_with( 78 | "git clone https://test_actor:test_token@test_endpoint/test_actor/test_repo" 79 | ) 80 | self.assertIsNone(result) 81 | 82 | 83 | class TestCreatePullRequest(unittest.TestCase): 84 | """Test case for the create_pull_request function.""" 85 | 86 | def test_create_pull_request_success(self): 87 | """ 88 | Test the create_pull_request function when the pull request is created successfully. 89 | """ 90 | github_connection = MagicMock() 91 | github_connection.repository.return_value = MagicMock() 92 | 93 | create_pull_request( 94 | organization="test_org", 95 | pr_body="Test PR body", 96 | pr_title="Test PR title", 97 | github_connection=github_connection, 98 | repo_name="test_repo", 99 | branch_name="test_branch", 100 | default_branch="main", 101 | ) 102 | 103 | github_connection.repository.return_value.create_pull.assert_called_once_with( 104 | title="Test PR title", body="Test PR body", head="test_branch", base="main" 105 | ) 106 | 107 | def test_create_pull_exceptions(self): 108 | """ 109 | Test the create_pull_request function when an exception occurs. 110 | """ 111 | github_connection = MagicMock() 112 | github_connection.repository.return_value = MagicMock() 113 | for exception, message in [ 114 | ( 115 | github3.exceptions.UnprocessableEntity(MagicMock()), 116 | "Pull request already exists", 117 | ), 118 | (github3.exceptions.ForbiddenError(MagicMock()), "Pull request failed"), 119 | (github3.exceptions.NotFoundError(MagicMock()), "Pull request failed"), 120 | (github3.exceptions.ConnectionError(MagicMock()), "Pull request failed"), 121 | ]: 122 | github_connection.repository.return_value.create_pull.side_effect = ( 123 | exception 124 | ) 125 | with patch("builtins.print") as mock_print: 126 | create_pull_request( 127 | organization="test_org", 128 | pr_body="Test PR body", 129 | pr_title="Test PR title", 130 | github_connection=github_connection, 131 | repo_name="test_repo", 132 | branch_name="test_branch", 133 | default_branch="main", 134 | ) 135 | mock_print.assert_called_once_with(message) 136 | 137 | 138 | if __name__ == "__main__": 139 | unittest.main() 140 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | # Contributing to automatic-contrib-prs 5 | 6 | First off, thanks for taking the time to contribute! :heart: 7 | 8 | All types of contributions are encouraged and valued. See the [Table of Contents](#table-of-contents) for different ways to help and details about how this project handles them. Please make sure to read the relevant section before making your contribution. It will make it a lot easier for us project owners and smooth out the experience for all involved. The team looks forward to your contributions. :tada: 9 | 10 | 11 | 12 | ## Table of Contents 13 | 14 | - [I Have a Question](#i-have-a-question) 15 | - [I Want To Contribute](#i-want-to-contribute) 16 | - [Reporting Bugs](#reporting-bugs) 17 | - [Suggesting Enhancements](#suggesting-enhancements) 18 | - [Releases](#releases) 19 | 20 | ## I Have a Question 21 | 22 | Before you ask a question, it is best to search for existing [Issues](https://github.com/github/automatic-contrib-prs/issues) that might help you. In case you have found a suitable issue and still need clarification, you can write your question in this issue. 23 | 24 | If you then still feel the need to ask a question and need clarification, we recommend the following: 25 | 26 | - Open an [Issue](https://github.com/github/automatic-contrib-prs/issues/new). 27 | - Provide as much context as you can about what you're running into. 28 | - Provide project and platform versions (nodejs, npm, etc), depending on what seems relevant. 29 | 30 | We will then take care of the issue as soon as possible. 31 | 32 | ## I Want To Contribute 33 | 34 | > ### Legal Notice 35 | > 36 | > When contributing to this project, you must agree that you have authored 100% of the content, that you have the necessary rights to the content and that the content you contribute may be provided under the project license. 37 | 38 | ## Reporting Bugs 39 | 40 | 41 | 42 | ### Before Submitting a Bug Report 43 | 44 | A good bug report shouldn't leave others needing to chase you up for more information. Therefore, we ask you to investigate carefully, collect information and describe the issue in detail in your report. Please complete the following steps in advance to help us fix any potential bug as fast as possible. 45 | 46 | - Make sure that you are using the latest version. 47 | - Determine if your bug is really a bug and not an error on your side e.g. using incompatible environment components/versions (Make sure that you have read the documentation. If you are looking for support, you might want to check [this section](#i-have-a-question)). 48 | - To see if other users have experienced (and potentially already solved) the same issue you are having, check if there is not already a bug report existing for your bug or error in the [bug tracker](https://github.com/github/automatic-contrib-prs/issues). 49 | - Collect information about the bug: 50 | - Stack trace (Traceback) 51 | - OS, Platform and Version (Windows, Linux, macOS, x86, ARM) 52 | - Version of the interpreter, compiler, SDK, runtime environment, package manager, depending on what seems relevant. 53 | - Possibly your input and the output 54 | - Can you reliably reproduce the issue? And can you also reproduce it with older versions? 55 | 56 | 57 | 58 | ### How Do I Submit a Good Bug Report? 59 | 60 | Please submit a bug report using our [GitHub Issues template](https://github.com/github/automatic-contrib-prs/issues/new?template=bug_report.yml). 61 | 62 | ## Suggesting Enhancements 63 | 64 | This section guides you through submitting an enhancement suggestion for automatic-contrib-prs, **including completely new features and minor improvements to existing functionality**. Following these guidelines will help maintainers and the community to understand your suggestion and find related suggestions. 65 | 66 | 67 | 68 | ### Before Submitting an Enhancement 69 | 70 | - Make sure that you are using the latest version. 71 | - Read the documentation carefully and find out if the functionality is already covered, maybe by an individual configuration. 72 | - Perform a [search](https://github.com/github/automatic-contrib-prs/issues) to see if the enhancement has already been suggested. If it has, add a comment to the existing issue instead of opening a new one. 73 | - Find out whether your idea fits with the scope and aims of the project. It's up to you to make a strong case to convince the project's developers of the merits of this feature or to develop the feature yourself and contribute it to the project. 74 | 75 | 76 | 77 | ### How Do I Submit a Good Enhancement Suggestion? 78 | 79 | Please submit an enhancement suggestion using our [GitHub Issues template](https://github.com/github/automatic-contrib-prs/issues/new?template=feature_request.yml). 80 | 81 | ### Pull Request Standards 82 | 83 | We are using [Conventional Commits](https://www.conventionalcommits.org/en/v1.0.0/) to standardize our pull request titles. This allows us to automatically generate labels and changelogs and follow semantic versioning. Please follow the commit message format when creating a pull request. What pull request title prefixes are expected are in the [pull_request_template.md](.github/pull_request_template.md) that is shown when creating a pull request. 84 | 85 | ## Releases 86 | 87 | Releases are automated if a pull request is labelled with our [SemVer related labels](.github/release-drafter.yml) or with the `vuln` or `release` labels. 88 | 89 | You can also manually initiate a release you can do so through the GitHub Actions UI. If you have permissions to do so, you can navigate to the [Actions tab](https://github.com/github/automatic-contrib-prs/actions/workflows/release.yml) and select the `Run workflow` button. This will allow you to select the branch to release from and the version to release. 90 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | 4 | 5 | ## Our Pledge 6 | 7 | We as members, contributors, and leaders pledge to make participation in our 8 | community a harassment-free experience for everyone, regardless of age, body 9 | size, visible or invisible disability, ethnicity, sex characteristics, gender 10 | identity and expression, level of experience, education, socio-economic status, 11 | nationality, personal appearance, race, religion, or sexual identity 12 | and orientation. 13 | 14 | We pledge to act and interact in ways that contribute to an open, welcoming, 15 | diverse, inclusive, and healthy community. 16 | 17 | ## Our Standards 18 | 19 | Examples of behavior that contributes to a positive environment for our 20 | community include: 21 | 22 | - Demonstrating empathy and kindness toward other people 23 | - Being respectful of differing opinions, viewpoints, and experiences 24 | - Giving and gracefully accepting constructive feedback 25 | - Accepting responsibility and apologizing to those affected by our mistakes, 26 | and learning from the experience 27 | - Focusing on what is best not just for us as individuals, but for the 28 | overall community 29 | 30 | Examples of unacceptable behavior include: 31 | 32 | - The use of sexualized language or imagery, and sexual attention or 33 | advances of any kind 34 | - Trolling, insulting or derogatory comments, and personal or political attacks 35 | - Public or private harassment 36 | - Publishing others' private information, such as a physical or email 37 | address, without their explicit permission 38 | - Other conduct which could reasonably be considered inappropriate in a 39 | professional setting 40 | 41 | ## Enforcement Responsibilities 42 | 43 | Community leaders are responsible for clarifying and enforcing our standards of 44 | acceptable behavior and will take appropriate and fair corrective action in 45 | response to any behavior that they deem inappropriate, threatening, offensive, 46 | or harmful. 47 | 48 | Community leaders have the right and responsibility to remove, edit, or reject 49 | comments, commits, code, wiki edits, issues, and other contributions that are 50 | not aligned to this Code of Conduct, and will communicate reasons for moderation 51 | decisions when appropriate. 52 | 53 | ## Scope 54 | 55 | This Code of Conduct applies within all community spaces, and also applies when 56 | an individual is officially representing the community in public spaces. 57 | Examples of representing our community include using an official email address, 58 | posting via an official social media account, or acting as an appointed 59 | representative at an online or offline event. 60 | 61 | ## Enforcement 62 | 63 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 64 | reported to the community leaders responsible for enforcement at 65 | . 66 | All complaints will be reviewed and investigated promptly and fairly. 67 | 68 | All community leaders are obligated to respect the privacy and security of the 69 | reporter of any incident. 70 | 71 | ## Enforcement Guidelines 72 | 73 | Community leaders will follow these Community Impact Guidelines in determining 74 | the consequences for any action they deem in violation of this Code of Conduct: 75 | 76 | ### 1. Correction 77 | 78 | **Community Impact**: Use of inappropriate language or other behavior deemed 79 | unprofessional or unwelcome in the community. 80 | 81 | **Consequence**: A private, written warning from community leaders, providing 82 | clarity around the nature of the violation and an explanation of why the 83 | behavior was inappropriate. A public apology may be requested. 84 | 85 | ### 2. Warning 86 | 87 | **Community Impact**: A violation through a single incident or series 88 | of actions. 89 | 90 | **Consequence**: A warning with consequences for continued behavior. No 91 | interaction with the people involved, including unsolicited interaction with 92 | those enforcing the Code of Conduct, for a specified period of time. This 93 | includes avoiding interactions in community spaces as well as external channels 94 | like social media. Violating these terms may lead to a temporary or 95 | permanent ban. 96 | 97 | ### 3. Temporary Ban 98 | 99 | **Community Impact**: A serious violation of community standards, including 100 | sustained inappropriate behavior. 101 | 102 | **Consequence**: A temporary ban from any sort of interaction or public 103 | communication with the community for a specified period of time. No public or 104 | private interaction with the people involved, including unsolicited interaction 105 | with those enforcing the Code of Conduct, is allowed during this period. 106 | Violating these terms may lead to a permanent ban. 107 | 108 | ### 4. Permanent Ban 109 | 110 | **Community Impact**: Demonstrating a pattern of violation of community 111 | standards, including sustained inappropriate behavior, harassment of an 112 | individual, or aggression toward or disparagement of classes of individuals. 113 | 114 | **Consequence**: A permanent ban from any sort of public interaction within 115 | the community. 116 | 117 | ## Attribution 118 | 119 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], 120 | version 2.0, available at 121 | [https://www.contributor-covenant.org/version/2/0/code_of_conduct.html](https://www.contributor-covenant.org/version/2/0/code_of_conduct.html). 122 | 123 | Community Impact Guidelines were inspired by [Mozilla's code of conduct 124 | enforcement ladder](https://github.com/mozilla/diversity). 125 | 126 | [homepage]: https://www.contributor-covenant.org 127 | 128 | For answers to common questions about this code of conduct, see the FAQ at 129 | [https://www.contributor-covenant.org/faq](https://www.contributor-covenant.org/faq). Translations are available at 130 | [https://www.contributor-covenant.org/translations](https://www.contributor-covenant.org/translations). 131 | 132 | 133 | -------------------------------------------------------------------------------- /open_contrib_pr.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | """Automatically open a pull request for repositories that have no CONTRIBUTING.md file""" 3 | 4 | import json 5 | import os 6 | from time import sleep 7 | 8 | import auth 9 | import env 10 | import github3 11 | 12 | 13 | def get_repos_json(gh_actor, repos_json_location, token, endpoint): 14 | """ 15 | Get the list of repositories from the JSON file. 16 | 17 | Args: 18 | gh_actor (str): The GitHub actor (username). 19 | repos_json_location (str): The location of the JSON file containing the repositories. 20 | token (str): The GitHub personal access token. 21 | endpoint (str): The GitHub endpoint. 22 | 23 | Returns: 24 | dict: A dictionary containing the list of repositories. 25 | """ 26 | os.system(f"git clone https://{gh_actor}:{token}@{endpoint}/{repos_json_location}") 27 | with open(str(repos_json_location), "r", encoding="utf-8") as repos_file: 28 | innersource_repos = json.loads(repos_file.read()) 29 | return innersource_repos 30 | 31 | 32 | def main(): # pragma: no cover 33 | """ 34 | Automatically open a pull request for repositories that have no CONTRIBUTING.md 35 | file from a list of repositories in a JSON file 36 | """ 37 | env_vars = env.get_env_vars() 38 | gh_actor = env_vars.gh_actor 39 | organization = env_vars.organization 40 | pr_body = env_vars.pr_body 41 | pr_title = env_vars.pr_title 42 | repos_json_location = env_vars.repos_json_location 43 | token = env_vars.gh_token 44 | gh_app_id = env_vars.gh_app_id 45 | gh_app_installation_id = env_vars.gh_app_installation_id 46 | gh_app_private_key_bytes = env_vars.gh_app_private_key_bytes 47 | ghe = env_vars.gh_enterprise_url 48 | gh_app_enterprise_only = env_vars.gh_app_enterprise_only 49 | 50 | # Auth to GitHub.com 51 | github_connection = auth.auth_to_github( 52 | token, 53 | gh_app_id, 54 | gh_app_installation_id, 55 | gh_app_private_key_bytes, 56 | ghe, 57 | gh_app_enterprise_only, 58 | ) 59 | 60 | if not token and gh_app_id and gh_app_installation_id and gh_app_private_key_bytes: 61 | token = auth.get_github_app_installation_token( 62 | ghe, gh_app_id, gh_app_private_key_bytes, gh_app_installation_id 63 | ) 64 | 65 | endpoint = ghe.removeprefix("https://") if ghe else "github.com" 66 | 67 | os.system("git config --global user.name 'GitHub Actions'") 68 | os.system(f"git config --global user.email 'no-reply@{endpoint}'") 69 | 70 | # Get innersource repos from organization 71 | innersource_repos = get_repos_json(gh_actor, repos_json_location, token, endpoint) 72 | 73 | for repo in innersource_repos: 74 | print(repo["name"]) 75 | # check if the repo has a contributing.md file 76 | try: 77 | if repo["_InnerSourceMetadata"]["guidelines"] == "CONTRIBUTING.md": 78 | continue 79 | except KeyError: 80 | # clone the repo 81 | repo_name = clone_repository(gh_actor, token, endpoint, repo) 82 | if not repo_name: 83 | continue 84 | 85 | # checkout a branch called contributing-doc 86 | branch_name = "contributing-doc" 87 | os.chdir(f"{repo_name}") 88 | os.system(f"git checkout -b {branch_name}") 89 | 90 | # copy, customize, and git add the template file 91 | os.system("cp /action/workspace/CONTRIBUTING-template.md CONTRIBUTING.md") 92 | os.system(f"sed -i 's/Project-Name/{repo_name}/g' CONTRIBUTING.md") 93 | os.system("git add CONTRIBUTING.md") 94 | # git commit that file 95 | os.system( 96 | "git commit -m'Request to add a document outlining how to contribute'" 97 | ) 98 | # git push the branch 99 | os.system(f"git push -u origin {branch_name}") 100 | # open a PR from that branch to the default branch 101 | default_branch = repo["default_branch"] 102 | # create the pull request 103 | create_pull_request( 104 | organization, 105 | pr_body, 106 | pr_title, 107 | github_connection, 108 | repo_name, 109 | branch_name, 110 | default_branch, 111 | ) 112 | # Clean up repository dir 113 | os.chdir("../") 114 | os.system(f"rm -rf {repo_name}") 115 | 116 | # rate limit to 20 repos per hour 117 | print("Waiting 3 minutes so as not to exceed API limits") 118 | sleep(180) 119 | 120 | 121 | def create_pull_request( 122 | organization, 123 | pr_body, 124 | pr_title, 125 | github_connection, 126 | repo_name, 127 | branch_name, 128 | default_branch, 129 | ): 130 | """Create a pull request.""" 131 | repository_object = github_connection.repository(organization, repo_name) 132 | try: 133 | repository_object.create_pull( 134 | title=pr_title, 135 | body=pr_body, 136 | head=branch_name, 137 | base=default_branch, 138 | ) 139 | except github3.exceptions.UnprocessableEntity: 140 | print("Pull request already exists") 141 | except github3.exceptions.ForbiddenError: 142 | print("Pull request failed") 143 | except github3.exceptions.NotFoundError: 144 | print("Pull request failed") 145 | except github3.exceptions.ConnectionError: 146 | print("Pull request failed") 147 | except Exception as e: # pylint: disable=broad-exception-caught 148 | print(e) 149 | 150 | 151 | def clone_repository(gh_actor, token, endpoint, repo): 152 | """Clone the repository and return the name of the repository.""" 153 | repo_full_name = repo["full_name"] 154 | repo_name = repo["name"] 155 | try: 156 | os.system(f"git clone https://{gh_actor}:{token}@{endpoint}/{repo_full_name}") 157 | except OSError as e: 158 | print(f"Failed to clone repository: {e}") 159 | return None 160 | return repo_name 161 | 162 | 163 | if __name__ == "__main__": 164 | main() # pragma: no cover 165 | -------------------------------------------------------------------------------- /CONTRIBUTING-template.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | # Contributing to Project-Name 5 | 6 | First off, thanks for taking the time to contribute! :heart: 7 | 8 | All types of contributions are encouraged and valued. See the [Table of Contents](#table-of-contents) for different ways to help and details about how this project handles them. Please make sure to read the relevant section before making your contribution. It will make it a lot easier for us project owners and smooth out the experience for all involved. The team looks forward to your contributions. :tada: 9 | 10 | 11 | 12 | ## Table of Contents 13 | 14 | - [I Have a Question](#i-have-a-question) 15 | - [I Want To Contribute](#i-want-to-contribute) 16 | - [Reporting Bugs](#reporting-bugs) 17 | - [Suggesting Enhancements](#suggesting-enhancements) 18 | 19 | ## I Have a Question 20 | 21 | Before you ask a question, it is best to search for existing [Issues](https://github.com/github/Project-Name/issues) that might help you. In case you have found a suitable issue and still need clarification, you can write your question in this issue. 22 | 23 | If you then still feel the need to ask a question and need clarification, we recommend the following: 24 | 25 | - Open an [Issue](https://github.com/github/Project-Name/issues/new). 26 | - Provide as much context as you can about what you're running into. 27 | - Provide project and platform versions (nodejs, npm, etc), depending on what seems relevant. 28 | 29 | We will then take care of the issue as soon as possible. 30 | 31 | ## I Want To Contribute 32 | 33 | > ### Legal Notice 34 | > 35 | > When contributing to this project, you must agree that you have authored 100% of the content, that you have the necessary rights to the content and that the content you contribute may be provided under the project license. 36 | 37 | ## Reporting Bugs 38 | 39 | 40 | 41 | ### Before Submitting a Bug Report 42 | 43 | A good bug report shouldn't leave others needing to chase you up for more information. Therefore, we ask you to investigate carefully, collect information and describe the issue in detail in your report. Please complete the following steps in advance to help us fix any potential bug as fast as possible. 44 | 45 | - Make sure that you are using the latest version. 46 | - Determine if your bug is really a bug and not an error on your side e.g. using incompatible environment components/versions (Make sure that you have read the documentation. If you are looking for support, you might want to check [this section](#i-have-a-question)). 47 | - To see if other users have experienced (and potentially already solved) the same issue you are having, check if there is not already a bug report existing for your bug or error in the [bug tracker](https://github.com/github/Project-Name/issues). 48 | - Collect information about the bug: 49 | - Stack trace (Traceback) 50 | - OS, Platform and Version (Windows, Linux, macOS, x86, ARM) 51 | - Version of the interpreter, compiler, SDK, runtime environment, package manager, depending on what seems relevant. 52 | - Possibly your input and the output 53 | - Can you reliably reproduce the issue? And can you also reproduce it with older versions? 54 | 55 | 56 | 57 | ### How Do I Submit a Good Bug Report? 58 | 59 | We use GitHub issues to track bugs and errors. If you run into an issue with the project: 60 | 61 | - Open an [Issue](https://github.com/github/Project-Name/issues/new). (Since we can't be sure at this point whether it is a bug or not, we ask you not to talk about a bug yet and not to label the issue.) 62 | - Explain the behavior you would expect and the actual behavior. 63 | - Please provide as much context as possible and describe the _reproduction steps_ that someone else can follow to recreate the issue on their own. This usually includes your code. For good bug reports you should isolate the problem and create a reduced test case. 64 | - Provide the information you collected in the previous section. 65 | 66 | Once it's filed: 67 | 68 | - The project team will label the issue accordingly. 69 | - A team member will try to reproduce the issue with your provided steps. If there are no reproduction steps or no obvious way to reproduce the issue, the team will ask you for those steps and mark the issue as `needs-repro`. Bugs with the `needs-repro` tag will not be addressed until they are reproduced. 70 | - If the team is able to reproduce the issue, it will be marked `needs-fix`, as well as possibly other tags (such as `critical`), and the issue will be left to be implemented by someone. 71 | 72 | ## Suggesting Enhancements 73 | 74 | This section guides you through submitting an enhancement suggestion for Project-Name, **including completely new features and minor improvements to existing functionality**. Following these guidelines will help maintainers and the community to understand your suggestion and find related suggestions. 75 | 76 | 77 | 78 | ### Before Submitting an Enhancement 79 | 80 | - Make sure that you are using the latest version. 81 | - Read the documentation carefully and find out if the functionality is already covered, maybe by an individual configuration. 82 | - Perform a [search](https://github.com/github/Project-Name/issues) to see if the enhancement has already been suggested. If it has, add a comment to the existing issue instead of opening a new one. 83 | - Find out whether your idea fits with the scope and aims of the project. It's up to you to make a strong case to convince the project's developers of the merits of this feature or to develop the feature yourself and contribute it to the project. 84 | 85 | 86 | 87 | ### How Do I Submit a Good Enhancement Suggestion? 88 | 89 | Enhancement suggestions are tracked as [GitHub issues](https://github.com/github/Project-Name/issues). 90 | 91 | - Use a **clear and descriptive title** for the issue to identify the suggestion. 92 | - Provide a **step-by-step description of the suggested enhancement** in as many details as possible. 93 | - **Describe the current behavior** and **explain which behavior you expected to see instead** and why. At this point you can also tell which alternatives do not work for you. 94 | - You may want to **include screenshots and animated GIFs** which help you demonstrate the steps or point out the part which the suggestion is related to. 95 | - **Explain why this enhancement would be useful** to most Project-Name users. 96 | -------------------------------------------------------------------------------- /env.py: -------------------------------------------------------------------------------- 1 | """ 2 | Sets up the environment variables for the action. 3 | """ 4 | 5 | import os 6 | from os.path import dirname, join 7 | 8 | from dotenv import load_dotenv 9 | 10 | MAX_TITLE_LENGTH = 70 11 | MAX_BODY_LENGTH = 65536 12 | 13 | 14 | def get_bool_env_var(env_var_name: str, default: bool = False) -> bool: 15 | """Get a boolean environment variable. 16 | 17 | Args: 18 | env_var_name: The name of the environment variable to retrieve. 19 | default: The default value to return if the environment variable is not set. 20 | 21 | Returns: 22 | The value of the environment variable as a boolean. 23 | """ 24 | ev = os.environ.get(env_var_name, "") 25 | if ev == "" and default: 26 | return default 27 | return ev.strip().lower() == "true" 28 | 29 | 30 | def get_int_env_var(env_var_name: str, default: int = -1) -> int | None: 31 | """Get an integer environment variable. 32 | 33 | Args: 34 | env_var_name: The name of the environment variable to retrieve. 35 | 36 | Returns: 37 | The value of the environment variable as an integer or None. 38 | """ 39 | default_place_holder = -1 40 | env_var = os.environ.get(env_var_name, "") 41 | if default == default_place_holder and not env_var.strip(): 42 | return None 43 | try: 44 | return int(env_var) 45 | except ValueError: 46 | return default if default > default_place_holder else None 47 | 48 | 49 | class EnvVars: 50 | # pylint: disable=too-many-instance-attributes 51 | """ 52 | Environment variables 53 | 54 | Attributes: 55 | gh_app_id (int | None): The GitHub App ID to use for authentication 56 | gh_app_installation_id (int | None): The GitHub App Installation ID to use for authentication 57 | gh_app_private_key_bytes (bytes): The GitHub App Private Key as bytes to use for authentication 58 | gh_app_enterprise_only (bool): Set this to true if the GH APP is created on GHE and needs to communicate with GHE api only 59 | ghe (str): The GitHub Enterprise URL to use for authentication 60 | gh_token (str | None): GitHub personal access token (PAT) for API authentication 61 | gh_actor (str): The GitHub actor to use for authentication 62 | organization (str): The GitHub organization to use for the PR 63 | pr_body (str): The PR body to use for the PR 64 | pr_title (str): The PR title to use for the PR 65 | repos_json_location (str): The location of the repos.json file 66 | """ 67 | 68 | def __init__( 69 | self, 70 | gh_actor: str | None, 71 | gh_app_id: int | None, 72 | gh_app_installation_id: int | None, 73 | gh_app_private_key_bytes: bytes, 74 | gh_app_enterprise_only: bool, 75 | gh_enterprise_url: str | None, 76 | gh_token: str | None, 77 | organization: str | None, 78 | pr_body: str | None, 79 | pr_title: str | None, 80 | repos_json_location: str, 81 | ): 82 | self.gh_actor = gh_actor 83 | self.gh_app_id = gh_app_id 84 | self.gh_app_installation_id = gh_app_installation_id 85 | self.gh_app_private_key_bytes = gh_app_private_key_bytes 86 | self.gh_app_enterprise_only = gh_app_enterprise_only 87 | self.gh_enterprise_url = gh_enterprise_url 88 | self.gh_token = gh_token 89 | self.organization = organization 90 | self.pr_body = pr_body 91 | self.pr_title = pr_title 92 | self.repos_json_location = repos_json_location 93 | 94 | def __repr__(self): 95 | return ( 96 | f"EnvVars(" 97 | f"{self.gh_actor}," 98 | f"{self.gh_app_id}," 99 | f"{self.gh_app_installation_id}," 100 | f"{self.gh_app_private_key_bytes}," 101 | f"{self.gh_app_enterprise_only}" 102 | f"{self.gh_enterprise_url}," 103 | f"{self.gh_token}," 104 | f"{self.organization}," 105 | f"{self.pr_body}," 106 | f"{self.pr_title}," 107 | f"{self.repos_json_location})" 108 | ) 109 | 110 | 111 | def get_env_vars(test: bool = False) -> EnvVars: 112 | """ 113 | Get the environment variables for use in the action. 114 | 115 | Args: 116 | test (bool): Whether or not to load the environment variables from a .env file (default: False) 117 | 118 | Returns: 119 | gh_actor (str): The GitHub actor to use for authentication 120 | gh_app_id (int | None): The GitHub App ID to use for authentication 121 | gh_app_installation_id (int | None): The GitHub App Installation ID to use for authentication 122 | gh_app_private_key_bytes (bytes): The GitHub App Private Key as bytes to use for authentication 123 | gh_app_enterprise_only (bool): Set this to true if the GH APP is created on GHE and needs to communicate with GHE api only 124 | gh_enterprise_url (str): The GitHub Enterprise URL to use for authentication 125 | gh_token (str | None): The GitHub token to use for authentication 126 | organization (str): The GitHub organization to use for the PR 127 | pr_body (str): The PR body to use for the PR 128 | pr_title (str): The PR title to use for the PR 129 | repos_json_location (str): The location of the repos.json file 130 | """ 131 | if not test: 132 | # Load from .env file if it exists 133 | dotenv_path = join(dirname(__file__), ".env") 134 | load_dotenv(dotenv_path) 135 | 136 | gh_actor = os.getenv("GH_ACTOR", "nobody") 137 | gh_app_id = get_int_env_var("GH_APP_ID") 138 | gh_app_private_key_bytes = os.environ.get("GH_APP_PRIVATE_KEY", "").encode("utf8") 139 | gh_app_installation_id = get_int_env_var("GH_APP_INSTALLATION_ID") 140 | gh_app_enterprise_only = get_bool_env_var("GITHUB_APP_ENTERPRISE_ONLY") 141 | 142 | if gh_app_id and (not gh_app_private_key_bytes or not gh_app_installation_id): 143 | raise ValueError( 144 | "GH_APP_ID set and GH_APP_INSTALLATION_ID or GH_APP_PRIVATE_KEY variable not set" 145 | ) 146 | 147 | gh_token = os.getenv("GH_TOKEN") 148 | if ( 149 | not gh_app_id 150 | and not gh_app_private_key_bytes 151 | and not gh_app_installation_id 152 | and not gh_token 153 | ): 154 | raise ValueError("GH_TOKEN environment variable not set") 155 | 156 | gh_enterprise_url = os.getenv("GH_ENTERPRISE_URL", default="").strip() 157 | 158 | organization = os.getenv("ORGANIZATION") 159 | if not organization: 160 | raise ValueError("ORGANIZATION environment variable not set") 161 | 162 | pr_title = os.getenv("PR_TITLE", "chore: Add new CONTRIBUTING.md file") 163 | # make sure that title is a string with less than 70 characters 164 | if len(pr_title) > MAX_TITLE_LENGTH: 165 | raise ValueError("PR_TITLE environment variable is too long. Max 70 characters") 166 | 167 | pr_body = os.getenv( 168 | "PR_BODY", 169 | "Add file that specifies the processes and procedures for new contributors to make a new contribution", 170 | ) 171 | # make sure that body is a string with less than 65536 characters 172 | if len(pr_body) > MAX_BODY_LENGTH: 173 | raise ValueError("BODY environment variable is too long. Max 65536 characters") 174 | 175 | repos_json_location = os.getenv("REPOS_JSON_LOCATION", default="repos.json").strip() 176 | 177 | return EnvVars( 178 | gh_actor, 179 | gh_app_id, 180 | gh_app_installation_id, 181 | gh_app_private_key_bytes, 182 | gh_app_enterprise_only, 183 | gh_enterprise_url, 184 | gh_token, 185 | organization, 186 | pr_body, 187 | pr_title, 188 | repos_json_location, 189 | ) 190 | -------------------------------------------------------------------------------- /test_env.py: -------------------------------------------------------------------------------- 1 | """Test the get_env_vars function""" 2 | 3 | import os 4 | import random 5 | import string 6 | import unittest 7 | from unittest.mock import patch 8 | 9 | from env import MAX_BODY_LENGTH, MAX_TITLE_LENGTH, EnvVars, get_env_vars 10 | 11 | BODY = "example CONTRIBUTING file contents" 12 | ORGANIZATION = "Organization01" 13 | REPOS_JSON_LOCATION = "repos.json" 14 | TITLE = "New CONTRIBUTING file" 15 | TOKEN = "Token01" 16 | 17 | 18 | class TestEnv(unittest.TestCase): 19 | """Test the get_env_vars function""" 20 | 21 | def setUp(self): 22 | env_keys = [ 23 | "GH_ACTOR", 24 | "GH_APP_ID", 25 | "GH_APP_INSTALLATION_ID", 26 | "GH_APP_PRIVATE_KEY", 27 | "GITHUB_APP_ENTERPRISE_ONLY", 28 | "GH_ENTERPRISE_URL", 29 | "GH_TOKEN", 30 | "ORGANIZATION", 31 | "PR_BODY", 32 | "PR_TITLE", 33 | "REPOS_JSON_LOCATION", 34 | ] 35 | for key in env_keys: 36 | if key in os.environ: 37 | del os.environ[key] 38 | 39 | @patch.dict( 40 | os.environ, 41 | { 42 | "GH_ACTOR": "", 43 | "GH_APP_ID": "", 44 | "GH_APP_INSTALLATION_ID": "", 45 | "GH_APP_PRIVATE_KEY": "", 46 | "GITHUB_APP_ENTERPRISE_ONLY": "", 47 | "GH_ENTERPRISE_URL": "", 48 | "GH_TOKEN": TOKEN, 49 | "ORGANIZATION": ORGANIZATION, 50 | "PR_BODY": BODY, 51 | "PR_TITLE": TITLE, 52 | }, 53 | ) 54 | def test_get_env_vars_with_token(self): 55 | """Test that all environment variables are set correctly using a token""" 56 | expected_result = EnvVars( 57 | "", 58 | None, 59 | None, 60 | b"", 61 | False, 62 | "", 63 | TOKEN, 64 | ORGANIZATION, 65 | BODY, 66 | TITLE, 67 | REPOS_JSON_LOCATION, 68 | ) 69 | result = get_env_vars(True) 70 | self.assertEqual(str(result), str(expected_result)) 71 | 72 | @patch.dict( 73 | os.environ, 74 | { 75 | "GH_ACTOR": "", 76 | "GH_APP_ID": "12345", 77 | "GH_APP_INSTALLATION_ID": "678910", 78 | "GH_APP_PRIVATE_KEY": "hello", 79 | "GITHUB_APP_ENTERPRISE_ONLY": "", 80 | "GH_ENTERPRISE_URL": "", 81 | "GH_TOKEN": "", 82 | "ORGANIZATION": ORGANIZATION, 83 | "PR_BODY": BODY, 84 | "PR_TITLE": TITLE, 85 | }, 86 | clear=True, 87 | ) 88 | def test_get_env_vars_with_github_app(self): 89 | """Test that all environment variables are set correctly using github app authentication""" 90 | expected_result = EnvVars( 91 | "", 92 | 12345, 93 | 678910, 94 | b"hello", 95 | False, 96 | "", 97 | "", 98 | ORGANIZATION, 99 | BODY, 100 | TITLE, 101 | REPOS_JSON_LOCATION, 102 | ) 103 | result = get_env_vars(True) 104 | self.assertEqual(str(result), str(expected_result)) 105 | 106 | @patch.dict( 107 | os.environ, 108 | { 109 | "GH_ACTOR": "testactor", 110 | "GH_APP_ID": "", 111 | "GH_APP_INSTALLATION_ID": "", 112 | "GH_APP_PRIVATE_KEY": "", 113 | "GITHUB_APP_ENTERPRISE_ONLY": "", 114 | "GH_ENTERPRISE_URL": "testghe", 115 | "GH_TOKEN": TOKEN, 116 | "ORGANIZATION": ORGANIZATION, 117 | "PR_BODY": BODY, 118 | "PR_TITLE": TITLE, 119 | "REPOS_JSON_LOCATION": "test/repos.json", 120 | }, 121 | ) 122 | def test_get_env_vars_optional_values(self): 123 | """Test that optional values are set to their default values if not provided""" 124 | expected_result = EnvVars( 125 | "testactor", 126 | None, 127 | None, 128 | b"", 129 | False, 130 | "testghe", 131 | TOKEN, 132 | ORGANIZATION, 133 | BODY, 134 | TITLE, 135 | "test/repos.json", 136 | ) 137 | result = get_env_vars(True) 138 | self.assertEqual(str(result), str(expected_result)) 139 | 140 | @patch.dict(os.environ, {}) 141 | def test_get_env_vars_missing_all_authentication(self): 142 | """Test that an error is raised if required authentication environment variables are not set""" 143 | with self.assertRaises(ValueError) as context_manager: 144 | get_env_vars() 145 | the_exception = context_manager.exception 146 | self.assertEqual( 147 | str(the_exception), 148 | "GH_TOKEN environment variable not set", 149 | ) 150 | 151 | @patch.dict( 152 | os.environ, 153 | { 154 | "GH_TOKEN": TOKEN, 155 | }, 156 | ) 157 | def test_get_env_vars_missing_organization(self): 158 | """Test that an error is raised if required organization environment variables is not set""" 159 | with self.assertRaises(ValueError) as context_manager: 160 | get_env_vars() 161 | the_exception = context_manager.exception 162 | self.assertEqual( 163 | str(the_exception), 164 | "ORGANIZATION environment variable not set", 165 | ) 166 | 167 | @patch.dict( 168 | os.environ, 169 | { 170 | "ORGANIZATION": "my_organization", 171 | "GH_APP_ID": "12345", 172 | "GH_APP_INSTALLATION_ID": "", 173 | "GH_APP_PRIVATE_KEY": "", 174 | "GH_TOKEN": "", 175 | }, 176 | clear=True, 177 | ) 178 | def test_get_env_vars_auth_with_github_app_installation_missing_inputs(self): 179 | """Test that an error is raised when there are missing inputs for the gh app""" 180 | with self.assertRaises(ValueError) as context_manager: 181 | get_env_vars(True) 182 | the_exception = context_manager.exception 183 | self.assertEqual( 184 | str(the_exception), 185 | "GH_APP_ID set and GH_APP_INSTALLATION_ID or GH_APP_PRIVATE_KEY variable not set", 186 | ) 187 | 188 | @patch.dict( 189 | os.environ, 190 | { 191 | "ORGANIZATION": "", 192 | "GH_TOKEN": "test", 193 | }, 194 | clear=True, 195 | ) 196 | def test_get_env_vars_no_organization_set(self): 197 | """Test that an error is raised whenthere are missing inputs for the gh app""" 198 | with self.assertRaises(ValueError) as context_manager: 199 | get_env_vars(True) 200 | the_exception = context_manager.exception 201 | self.assertEqual( 202 | str(the_exception), 203 | "ORGANIZATION environment variable not set", 204 | ) 205 | 206 | @patch.dict( 207 | os.environ, 208 | { 209 | "ORGANIZATION": "my_organization", 210 | "GH_TOKEN": "test", 211 | "PR_TITLE": "".join( 212 | random.choices(string.ascii_letters, k=MAX_TITLE_LENGTH + 1) 213 | ), 214 | }, 215 | clear=True, 216 | ) 217 | def test_get_env_vars_pr_title_too_long(self): 218 | """Test that an error is raised when the PR_TITLE env variable has more than MAX_TITLE_LENGTH characters""" 219 | with self.assertRaises(ValueError) as context_manager: 220 | get_env_vars(True) 221 | the_exception = context_manager.exception 222 | self.assertEqual( 223 | str(the_exception), 224 | f"PR_TITLE environment variable is too long. Max {MAX_TITLE_LENGTH} characters", 225 | ) 226 | 227 | @patch.dict( 228 | os.environ, 229 | { 230 | "ORGANIZATION": "my_organization", 231 | "GH_TOKEN": "test", 232 | "PR_BODY": "".join( 233 | random.choices(string.ascii_letters, k=MAX_BODY_LENGTH + 1) 234 | ), 235 | }, 236 | clear=True, 237 | ) 238 | def test_get_env_vars_pr_body_too_long(self): 239 | """Test that an error is raised when the PR_BODY env variable has more than MAX_BODY_LENGTH characters""" 240 | with self.assertRaises(ValueError) as context_manager: 241 | get_env_vars(True) 242 | the_exception = context_manager.exception 243 | self.assertEqual( 244 | str(the_exception), 245 | f"BODY environment variable is too long. Max {MAX_BODY_LENGTH} characters", 246 | ) 247 | 248 | 249 | if __name__ == "__main__": 250 | unittest.main() 251 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # automatic-contrib-prs 2 | 3 | [![.github/workflows/linter.yml](https://github.com/github/automatic-contrib-prs/actions/workflows/super-linter.yml/badge.svg)](https://github.com/github/automatic-contrib-prs/actions/workflows/super-linter.yml) 4 | [![CodeQL](https://github.com/github/automatic-contrib-prs/actions/workflows/github-code-scanning/codeql/badge.svg)](https://github.com/github/automatic-contrib-prs/actions/workflows/github-code-scanning/codeql) 5 | [![Docker Image CI](https://github.com/github/automatic-contrib-prs/actions/workflows/docker-image.yml/badge.svg)](https://github.com/github/automatic-contrib-prs/actions/workflows/docker-image.yml) 6 | [![OpenSSF Scorecard](https://api.scorecard.dev/projects/github.com/github/automatic-contrib-prs/badge)](https://scorecard.dev/viewer/?uri=github.com/github/automatic-contrib-prs) 7 | 8 | Automatically open a pull request for repositories that have no `CONTRIBUTING.md` file for a targeted set of repositories. 9 | 10 | ## What this repository does 11 | 12 | This code is for a GitHub Action that opens pull requests in the repositories that have a specified repository topic and also don't have a `CONTRIBUTING.md` file. 13 | 14 | ## Support 15 | 16 | If you need support using this project or have questions about it, please [open up an issue in this repository](https://github.com/github/automatic-contrib-prs/issues). Requests made directly to GitHub staff or support team will be redirected here to open an issue. GitHub SLA's and support/services contracts do not apply to this repository. 17 | 18 | ### OSPO GitHub Actions as a Whole 19 | 20 | All feedback regarding our GitHub Actions, as a whole, should be communicated through [issues on our github-ospo repository](https://github.com/github/github-ospo/issues/new). 21 | 22 | ## Why would someone do this 23 | 24 | It is desirable, for example, for all Open Source and InnerSource projects to have a `CONTRIBUTING.md` file that specifies for new contributors what the processes and procedures are for making a new contribution. This has been done in some large GitHub customers organizations. 25 | 26 | ## How it does this 27 | 28 | - It pulls a list of labelled repositories from a `repos.json` which can be generated by the [InnerSource-Crawler GitHub Action](https://github.com/marketplace/actions/innersource-crawler). 29 | - It opens a pull request in each of those repositories which adds the `CONTRIBUTING.md` file with some template contents. 30 | 31 | ## Use as a GitHub Action 32 | 33 | 1. Create a repository to host this GitHub Action or select an existing repository. 34 | 1. Create the env values from the sample workflow below (`GH_TOKEN`, `GH_ACTOR`, `PR_TITLE`, `PR_BODY`, and `ORGANIZATION`) with your information as repository secrets. More info on creating secrets can be found [here](https://docs.github.com/en/actions/security-guides/encrypted-secrets). 35 | Note: Your GitHub token will need to have read/write access to all the repositories in the `repos.json` file. 36 | 1. Copy the below example workflow to your repository and put it in the `.github/workflows/` directory with the file extension `.yml` (ie. `.github/workflows/auto-contrib-file.yml`) 37 | 38 | ### Configuration 39 | 40 | Below are the allowed configuration options: 41 | 42 | #### Authentication 43 | 44 | This action can be configured to authenticate with GitHub App Installation or Personal Access Token (PAT). If all configuration options are provided, the GitHub App Installation configuration has precedence. You can choose one of the following methods to authenticate: 45 | 46 | ##### GitHub App Installation 47 | 48 | | field | required | default | description | 49 | | ---------------------------- | -------- | ------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | 50 | | `GH_APP_ID` | True | `""` | GitHub Application ID. See [documentation](https://docs.github.com/en/apps/creating-github-apps/authenticating-with-a-github-app/about-authentication-with-a-github-app) for more details. | 51 | | `GH_APP_INSTALLATION_ID` | True | `""` | GitHub Application Installation ID. See [documentation](https://docs.github.com/en/apps/creating-github-apps/authenticating-with-a-github-app/about-authentication-with-a-github-app) for more details. | 52 | | `GH_APP_PRIVATE_KEY` | True | `""` | GitHub Application Private Key. See [documentation](https://docs.github.com/en/apps/creating-github-apps/authenticating-with-a-github-app/about-authentication-with-a-github-app) for more details. | 53 | | `GITHUB_APP_ENTERPRISE_ONLY` | False | `false` | Set this input to `true` if your app is created in GHE and communicates with GHE. | 54 | 55 | ##### Personal Access Token (PAT) 56 | 57 | | field | required | default | description | 58 | | ---------- | -------- | ------- | --------------------------------------------------------------------------------------------------------------------- | 59 | | `GH_TOKEN` | True | `""` | The GitHub Token used to scan the repository. Must have read access to all repository you are interested in scanning. | 60 | 61 | #### Other Configuration Options 62 | 63 | | field | required | default | description | 64 | | --------------------- | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | --------------------------------------------------------------------------------------------------------------------------------------- | 65 | | `GH_ENTERPRISE_URL` | False | "" | The `GH_ENTERPRISE_URL` is used to connect to an enterprise server instance of GitHub. github.com users should not enter anything here. | 66 | | `PR_TITLE` | False | "Enable Dependabot" | The title of the issue or pull request that will be created if dependabot could be enabled. | 67 | | `PR_BODY` | False | **Pull Request:** "Dependabot could be enabled for this repository. Please enable it by merging this pull request so that we can keep our dependencies up to date and secure." **Issue:** "Please update the repository to include a Dependabot configuration file. This will ensure our dependencies remain updated and secure.Follow the guidelines in [creating Dependabot configuration files](https://docs.github.com/en/code-security/dependabot/dependabot-version-updates/configuration-options-for-the-dependabot.yml-file) to set it up properly.Here's an example of the code:" | The body of the issue or pull request that will be created if dependabot could be enabled. | 68 | | `REPOS_JSON_LOCATION` | False | "Create dependabot.yaml" | The commit message for the pull request that will be created if dependabot could be enabled. | 69 | 70 | ### Example workflow 71 | 72 | ```yaml 73 | name: Find proper repos and open CONTRIBUTING.md prs 74 | 75 | on: 76 | workflow_dispatch: 77 | 78 | permissions: 79 | contents: read 80 | 81 | jobs: 82 | build: 83 | name: Open CONTRIBUTING.md in OSS if it doesnt exist 84 | runs-on: ubuntu-latest 85 | permissions: 86 | contents: read 87 | pull-requests: write 88 | 89 | steps: 90 | - name: Checkout code 91 | uses: actions/checkout@v4 92 | 93 | - name: Find OSS repository in organization 94 | uses: docker://ghcr.io/zkoppert/innersource-crawler:v1 95 | env: 96 | GH_TOKEN: ${{ secrets.GH_TOKEN }} 97 | ORGANIZATION: ${{ secrets.ORGANIZATION }} 98 | TOPIC: open-source 99 | 100 | - name: Open pull requests in OSS repository that are missing contrib files 101 | uses: docker://ghcr.io/github/automatic-contrib-prs:v2 102 | env: 103 | GH_TOKEN: ${{ secrets.GH_TOKEN }} 104 | ORGANIZATION: ${{ secrets.ORGANIZATION }} 105 | GH_ACTOR: ${{ secrets.GH_ACTOR }} 106 | PR_TITLE: ${{ secrets.PR_TITLE }} 107 | PR_BODY: ${{ secrets.PR_BODY }} 108 | ``` 109 | 110 | #### Using GitHub app 111 | 112 | ```yaml 113 | name: Find proper repos and open CONTRIBUTING.md prs 114 | 115 | on: 116 | workflow_dispatch: 117 | 118 | permissions: 119 | contents: read 120 | 121 | jobs: 122 | build: 123 | name: Open CONTRIBUTING.md in OSS if it doesnt exist 124 | runs-on: ubuntu-latest 125 | permissions: 126 | contents: read 127 | pull-requests: write 128 | 129 | steps: 130 | - name: Checkout code 131 | uses: actions/checkout@v4 132 | 133 | - name: Find OSS repository in organization 134 | uses: docker://ghcr.io/zkoppert/innersource-crawler:v1 135 | env: 136 | GH_TOKEN: ${{ secrets.GH_TOKEN }} 137 | ORGANIZATION: ${{ secrets.ORGANIZATION }} 138 | TOPIC: open-source 139 | 140 | - name: Open pull requests in OSS repository that are missing contrib files 141 | uses: docker://ghcr.io/github/automatic-contrib-prs:v2 142 | env: 143 | GH_APP_ID: ${{ secrets.GH_APP_ID }} 144 | GH_APP_INSTALLATION_ID: ${{ secrets.GH_APP_INSTALLATION_ID }} 145 | GH_APP_PRIVATE_KEY: ${{ secrets.GH_APP_PRIVATE_KEY }} 146 | # GITHUB_APP_ENTERPRISE_ONLY: True --> Set to true when created GHE App needs to communicate with GHE api 147 | GH_ENTERPRISE_URL: ${{ github.server_url }} 148 | # GH_TOKEN: ${{ secrets.GH_TOKEN }} --> the token input is not used if the github app inputs are set 149 | ORGANIZATION: ${{ secrets.ORGANIZATION }} 150 | GH_ACTOR: ${{ secrets.GH_ACTOR }} 151 | PR_TITLE: ${{ secrets.PR_TITLE }} 152 | PR_BODY: ${{ secrets.PR_BODY }} 153 | ``` 154 | 155 | ## Scaling for large organizations 156 | 157 | - GitHub Actions workflows have time limits currently set at 72 hours per run. If you are operating on more than 1400 repos or so with this action, it will take several runs to complete. 158 | 159 | ## Contributions 160 | 161 | We would :heart: contributions to improve this action. Please see [CONTRIBUTING.md](./CONTRIBUTING.md) for how to get involved. 162 | 163 | ## Instructions to run locally without Docker 164 | 165 | - Clone the repository or open a codespace 166 | - Create a personal access token with read only permissions 167 | - Copy the `.env-example` file to `.env` 168 | - Edit the `.env` file by adding your Personal Access Token to it and the desired organization, pull request title and body, and actor (GitHub username) 169 | - Install dependencies `python3 -m pip install -r requirements.txt` 170 | - Run the code `python3 open_contrib_pr.py` 171 | - After running locally this will have changed your git config user.name and user.email so those should be reset for this repository 172 | 173 | ## Docker debug instructions 174 | 175 | - Install Docker and make sure docker engine is running 176 | - cd to the repository 177 | - Edit the Dockerfile to enable interactive docker debug as instructed in the comments of the file 178 | - `docker build -t test .` 179 | - `docker run -it test` 180 | - Now you should be at a command prompt inside your docker container and you can begin debugging 181 | 182 | ## License 183 | 184 | [MIT](./LICENSE) 185 | 186 | ## More OSPO Tools 187 | 188 | Looking for more resources for your open source program office (OSPO)? Check out the [`github-ospo`](https://github.com/github/github-ospo) repository for a variety of tools designed to support your needs. 189 | -------------------------------------------------------------------------------- /.github/linters/.python-lint: -------------------------------------------------------------------------------- 1 | [MAIN] 2 | 3 | # Analyse import fallback blocks. This can be used to support both Python 2 and 4 | # 3 compatible code, which means that the block might have code that exists 5 | # only in one or another interpreter, leading to false positives when analysed. 6 | analyse-fallback-blocks=no 7 | 8 | # Clear in-memory caches upon conclusion of linting. Useful if running pylint 9 | # in a server-like mode. 10 | clear-cache-post-run=no 11 | 12 | # Load and enable all available extensions. Use --list-extensions to see a list 13 | # all available extensions. 14 | #enable-all-extensions= 15 | 16 | # In error mode, messages with a category besides ERROR or FATAL are 17 | # suppressed, and no reports are done by default. Error mode is compatible with 18 | # disabling specific errors. 19 | #errors-only= 20 | 21 | # Always return a 0 (non-error) status code, even if lint errors are found. 22 | # This is primarily useful in continuous integration scripts. 23 | #exit-zero= 24 | 25 | # A comma-separated list of package or module names from where C extensions may 26 | # be loaded. Extensions are loading into the active Python interpreter and may 27 | # run arbitrary code. 28 | extension-pkg-allow-list= 29 | 30 | # A comma-separated list of package or module names from where C extensions may 31 | # be loaded. Extensions are loading into the active Python interpreter and may 32 | # run arbitrary code. (This is an alternative name to extension-pkg-allow-list 33 | # for backward compatibility.) 34 | extension-pkg-whitelist= 35 | 36 | # Return non-zero exit code if any of these messages/categories are detected, 37 | # even if score is above --fail-under value. Syntax same as enable. Messages 38 | # specified are enabled, while categories only check already-enabled messages. 39 | fail-on= 40 | 41 | # Specify a score threshold under which the program will exit with error. 42 | fail-under=10 43 | 44 | # Interpret the stdin as a python script, whose filename needs to be passed as 45 | # the module_or_package argument. 46 | #from-stdin= 47 | 48 | # Files or directories to be skipped. They should be base names, not paths. 49 | ignore=CVS, 50 | .git, 51 | __pycache__, 52 | venv, 53 | .venv, 54 | 55 | # Add files or directories matching the regular expressions patterns to the 56 | # ignore-list. The regex matches against paths and can be in Posix or Windows 57 | # format. Because '\\' represents the directory delimiter on Windows systems, 58 | # it can't be used as an escape character. 59 | ignore-paths= 60 | 61 | # Files or directories matching the regular expression patterns are skipped. 62 | # The regex matches against base names, not paths. The default value ignores 63 | # Emacs file locks 64 | ignore-patterns=^\.# 65 | 66 | # List of module names for which member attributes should not be checked 67 | # (useful for modules/projects where namespaces are manipulated during runtime 68 | # and thus existing member attributes cannot be deduced by static analysis). It 69 | # supports qualified module names, as well as Unix pattern matching. 70 | ignored-modules= 71 | 72 | # Python code to execute, usually for sys.path manipulation such as 73 | # pygtk.require(). 74 | #init-hook= 75 | 76 | # Use multiple processes to speed up Pylint. Specifying 0 will auto-detect the 77 | # number of processors available to use, and will cap the count on Windows to 78 | # avoid hangs. 79 | jobs=1 80 | 81 | # Control the amount of potential inferred values when inferring a single 82 | # object. This can help the performance when dealing with large functions or 83 | # complex, nested conditions. 84 | limit-inference-results=100 85 | 86 | # List of plugins (as comma separated values of python module names) to load, 87 | # usually to register additional checkers. 88 | load-plugins= 89 | 90 | # Pickle collected data for later comparisons. 91 | persistent=yes 92 | 93 | # Minimum Python version to use for version dependent checks. Will default to 94 | # the version used to run pylint. 95 | py-version=3.11 96 | 97 | # Discover python modules and packages in the file system subtree. 98 | recursive=no 99 | 100 | # Add paths to the list of the source roots. Supports globbing patterns. The 101 | # source root is an absolute path or a path relative to the current working 102 | # directory used to determine a package namespace for modules located under the 103 | # source root. 104 | source-roots= 105 | 106 | # When enabled, pylint would attempt to guess common misconfiguration and emit 107 | # user-friendly hints instead of false-positive error messages. 108 | suggestion-mode=yes 109 | 110 | # Allow loading of arbitrary C extensions. Extensions are imported into the 111 | # active Python interpreter and may run arbitrary code. 112 | unsafe-load-any-extension=no 113 | 114 | # In verbose mode, extra non-checker-related info will be displayed. 115 | #verbose= 116 | 117 | 118 | [BASIC] 119 | 120 | # Naming style matching correct argument names. 121 | argument-naming-style=snake_case 122 | 123 | # Regular expression matching correct argument names. Overrides argument- 124 | # naming-style. If left empty, argument names will be checked with the set 125 | # naming style. 126 | #argument-rgx= 127 | 128 | # Naming style matching correct attribute names. 129 | attr-naming-style=snake_case 130 | 131 | # Regular expression matching correct attribute names. Overrides attr-naming- 132 | # style. If left empty, attribute names will be checked with the set naming 133 | # style. 134 | #attr-rgx= 135 | 136 | # Bad variable names which should always be refused, separated by a comma. 137 | bad-names=foo, 138 | bar, 139 | baz, 140 | toto, 141 | tutu, 142 | tata 143 | 144 | # Bad variable names regexes, separated by a comma. If names match any regex, 145 | # they will always be refused 146 | bad-names-rgxs= 147 | 148 | # Naming style matching correct class attribute names. 149 | class-attribute-naming-style=any 150 | 151 | # Regular expression matching correct class attribute names. Overrides class- 152 | # attribute-naming-style. If left empty, class attribute names will be checked 153 | # with the set naming style. 154 | #class-attribute-rgx= 155 | 156 | # Naming style matching correct class constant names. 157 | class-const-naming-style=UPPER_CASE 158 | 159 | # Regular expression matching correct class constant names. Overrides class- 160 | # const-naming-style. If left empty, class constant names will be checked with 161 | # the set naming style. 162 | #class-const-rgx= 163 | 164 | # Naming style matching correct class names. 165 | class-naming-style=PascalCase 166 | 167 | # Regular expression matching correct class names. Overrides class-naming- 168 | # style. If left empty, class names will be checked with the set naming style. 169 | #class-rgx= 170 | 171 | # Naming style matching correct constant names. 172 | const-naming-style=UPPER_CASE 173 | 174 | # Regular expression matching correct constant names. Overrides const-naming- 175 | # style. If left empty, constant names will be checked with the set naming 176 | # style. 177 | #const-rgx= 178 | 179 | # Minimum line length for functions/classes that require docstrings, shorter 180 | # ones are exempt. 181 | docstring-min-length=-1 182 | 183 | # Naming style matching correct function names. 184 | function-naming-style=snake_case 185 | 186 | # Regular expression matching correct function names. Overrides function- 187 | # naming-style. If left empty, function names will be checked with the set 188 | # naming style. 189 | #function-rgx= 190 | 191 | # Good variable names which should always be accepted, separated by a comma. 192 | good-names=i, 193 | j, 194 | k, 195 | ex, 196 | Run, 197 | _ 198 | 199 | # Good variable names regexes, separated by a comma. If names match any regex, 200 | # they will always be accepted 201 | good-names-rgxs= 202 | 203 | # Include a hint for the correct naming format with invalid-name. 204 | include-naming-hint=no 205 | 206 | # Naming style matching correct inline iteration names. 207 | inlinevar-naming-style=any 208 | 209 | # Regular expression matching correct inline iteration names. Overrides 210 | # inlinevar-naming-style. If left empty, inline iteration names will be checked 211 | # with the set naming style. 212 | #inlinevar-rgx= 213 | 214 | # Naming style matching correct method names. 215 | method-naming-style=snake_case 216 | 217 | # Regular expression matching correct method names. Overrides method-naming- 218 | # style. If left empty, method names will be checked with the set naming style. 219 | #method-rgx= 220 | 221 | # Naming style matching correct module names. 222 | module-naming-style=snake_case 223 | 224 | # Regular expression matching correct module names. Overrides module-naming- 225 | # style. If left empty, module names will be checked with the set naming style. 226 | #module-rgx= 227 | 228 | # Colon-delimited sets of names that determine each other's naming style when 229 | # the name regexes allow several styles. 230 | name-group= 231 | 232 | # Regular expression which should only match function or class names that do 233 | # not require a docstring. 234 | no-docstring-rgx=^_ 235 | 236 | # List of decorators that produce properties, such as abc.abstractproperty. Add 237 | # to this list to register other decorators that produce valid properties. 238 | # These decorators are taken in consideration only for invalid-name. 239 | property-classes=abc.abstractproperty 240 | 241 | # Regular expression matching correct type alias names. If left empty, type 242 | # alias names will be checked with the set naming style. 243 | #typealias-rgx= 244 | 245 | # Regular expression matching correct type variable names. If left empty, type 246 | # variable names will be checked with the set naming style. 247 | #typevar-rgx= 248 | 249 | # Naming style matching correct variable names. 250 | variable-naming-style=snake_case 251 | 252 | # Regular expression matching correct variable names. Overrides variable- 253 | # naming-style. If left empty, variable names will be checked with the set 254 | # naming style. 255 | #variable-rgx= 256 | 257 | 258 | [CLASSES] 259 | 260 | # Warn about protected attribute access inside special methods 261 | check-protected-access-in-special-methods=no 262 | 263 | # List of method names used to declare (i.e. assign) instance attributes. 264 | defining-attr-methods=__init__, 265 | __new__, 266 | setUp, 267 | asyncSetUp, 268 | __post_init__ 269 | 270 | # List of member names, which should be excluded from the protected access 271 | # warning. 272 | exclude-protected=_asdict,_fields,_replace,_source,_make,os._exit 273 | 274 | # List of valid names for the first argument in a class method. 275 | valid-classmethod-first-arg=cls 276 | 277 | # List of valid names for the first argument in a metaclass class method. 278 | valid-metaclass-classmethod-first-arg=mcs 279 | 280 | 281 | [DESIGN] 282 | 283 | # List of regular expressions of class ancestor names to ignore when counting 284 | # public methods (see R0903) 285 | exclude-too-few-public-methods= 286 | 287 | # List of qualified class names to ignore when counting class parents (see 288 | # R0901) 289 | ignored-parents= 290 | 291 | # Maximum number of arguments for function / method. 292 | max-args=5 293 | 294 | # Maximum number of attributes for a class (see R0902). 295 | max-attributes=7 296 | 297 | # Maximum number of boolean expressions in an if statement (see R0916). 298 | max-bool-expr=5 299 | 300 | # Maximum number of branch for function / method body. 301 | max-branches=12 302 | 303 | # Maximum number of locals for function / method body. 304 | max-locals=15 305 | 306 | # Maximum number of parents for a class (see R0901). 307 | max-parents=7 308 | 309 | # Maximum number of public methods for a class (see R0904). 310 | max-public-methods=20 311 | 312 | # Maximum number of return / yield for function / method body. 313 | max-returns=6 314 | 315 | # Maximum number of statements in function / method body. 316 | max-statements=50 317 | 318 | # Minimum number of public methods for a class (see R0903). 319 | min-public-methods=2 320 | 321 | 322 | [EXCEPTIONS] 323 | 324 | # Exceptions that will emit a warning when caught. 325 | overgeneral-exceptions=builtins.BaseException,builtins.Exception 326 | 327 | 328 | [FORMAT] 329 | 330 | # Expected format of line ending, e.g. empty (any line ending), LF or CRLF. 331 | expected-line-ending-format= 332 | 333 | # Regexp for a line that is allowed to be longer than the limit. 334 | ignore-long-lines=^\s*(# )??$ 335 | 336 | # Number of spaces of indent required inside a hanging or continued line. 337 | indent-after-paren=4 338 | 339 | # String used as indentation unit. This is usually " " (4 spaces) or "\t" (1 340 | # tab). 341 | indent-string=' ' 342 | 343 | # Maximum number of characters on a single line. 344 | max-line-length=100 345 | 346 | # Maximum number of lines in a module. 347 | max-module-lines=1000 348 | 349 | # Allow the body of a class to be on the same line as the declaration if body 350 | # contains single statement. 351 | single-line-class-stmt=no 352 | 353 | # Allow the body of an if to be on the same line as the test if there is no 354 | # else. 355 | single-line-if-stmt=no 356 | 357 | 358 | [IMPORTS] 359 | 360 | # List of modules that can be imported at any level, not just the top level 361 | # one. 362 | allow-any-import-level= 363 | 364 | # Allow explicit reexports by alias from a package __init__. 365 | allow-reexport-from-package=no 366 | 367 | # Allow wildcard imports from modules that define __all__. 368 | allow-wildcard-with-all=no 369 | 370 | # Deprecated modules which should not be used, separated by a comma. 371 | deprecated-modules= 372 | 373 | # Output a graph (.gv or any supported image format) of external dependencies 374 | # to the given file (report RP0402 must not be disabled). 375 | ext-import-graph= 376 | 377 | # Output a graph (.gv or any supported image format) of all (i.e. internal and 378 | # external) dependencies to the given file (report RP0402 must not be 379 | # disabled). 380 | import-graph= 381 | 382 | # Output a graph (.gv or any supported image format) of internal dependencies 383 | # to the given file (report RP0402 must not be disabled). 384 | int-import-graph= 385 | 386 | # Force import order to recognize a module as part of the standard 387 | # compatibility libraries. 388 | known-standard-library= 389 | 390 | # Force import order to recognize a module as part of a third party library. 391 | known-third-party=enchant 392 | 393 | # Couples of modules and preferred modules, separated by a comma. 394 | preferred-modules= 395 | 396 | 397 | [LOGGING] 398 | 399 | # The type of string formatting that logging methods do. `old` means using % 400 | # formatting, `new` is for `{}` formatting. 401 | logging-format-style=old 402 | 403 | # Logging modules to check that the string format arguments are in logging 404 | # function parameter format. 405 | logging-modules=logging 406 | 407 | 408 | [MESSAGES CONTROL] 409 | 410 | # Only show warnings with the listed confidence levels. Leave empty to show 411 | # all. Valid levels: HIGH, CONTROL_FLOW, INFERENCE, INFERENCE_FAILURE, 412 | # UNDEFINED. 413 | confidence=HIGH, 414 | CONTROL_FLOW, 415 | INFERENCE, 416 | INFERENCE_FAILURE, 417 | UNDEFINED 418 | 419 | # Disable the message, report, category or checker with the given id(s). You 420 | # can either give multiple identifiers separated by comma (,) or put this 421 | # option multiple times (only on the command line, not in the configuration 422 | # file where it should appear only once). You can also use "--disable=all" to 423 | # disable everything first and then re-enable specific checks. For example, if 424 | # you want to run only the similarities checker, you can use "--disable=all 425 | # --enable=similarities". If you want to run only the classes checker, but have 426 | # no Warning level messages displayed, use "--disable=all --enable=classes 427 | # --disable=W". 428 | disable=bad-inline-option, 429 | deprecated-pragma, 430 | duplicate-code, 431 | locally-disabled, 432 | file-ignored, 433 | import-error, 434 | line-too-long, 435 | raw-checker-failed, 436 | suppressed-message, 437 | too-few-public-methods, 438 | too-many-arguments, 439 | too-many-function-args, 440 | too-many-branches, 441 | too-many-locals, 442 | too-many-nested-blocks, 443 | too-many-positional-arguments, 444 | too-many-statements, 445 | useless-suppression, 446 | use-symbolic-message-instead, 447 | use-implicit-booleaness-not-comparison-to-string, 448 | use-implicit-booleaness-not-comparison-to-zero, 449 | wrong-import-order 450 | 451 | # Enable the message, report, category or checker with the given id(s). You can 452 | # either give multiple identifier separated by comma (,) or put this option 453 | # multiple time (only on the command line, not in the configuration file where 454 | # it should appear only once). See also the "--disable" option for examples. 455 | enable= 456 | 457 | 458 | [METHOD_ARGS] 459 | 460 | # List of qualified names (i.e., library.method) which require a timeout 461 | # parameter e.g. 'requests.api.get,requests.api.post' 462 | timeout-methods=requests.api.delete,requests.api.get,requests.api.head,requests.api.options,requests.api.patch,requests.api.post,requests.api.put,requests.api.request 463 | 464 | 465 | [MISCELLANEOUS] 466 | 467 | # List of note tags to take in consideration, separated by a comma. 468 | notes=FIXME, 469 | XXX, 470 | TODO 471 | 472 | # Regular expression of note tags to take in consideration. 473 | notes-rgx= 474 | 475 | 476 | [REFACTORING] 477 | 478 | # Maximum number of nested blocks for function / method body 479 | max-nested-blocks=5 480 | 481 | # Complete name of functions that never returns. When checking for 482 | # inconsistent-return-statements if a never returning function is called then 483 | # it will be considered as an explicit return statement and no message will be 484 | # printed. 485 | never-returning-functions=sys.exit,argparse.parse_error 486 | 487 | 488 | [REPORTS] 489 | 490 | # Python expression which should return a score less than or equal to 10. You 491 | # have access to the variables 'fatal', 'error', 'warning', 'refactor', 492 | # 'convention', and 'info' which contain the number of messages in each 493 | # category, as well as 'statement' which is the total number of statements 494 | # analyzed. This score is used by the global evaluation report (RP0004). 495 | evaluation=max(0, 0 if fatal else 10.0 - ((float(5 * error + warning + refactor + convention) / statement) * 10)) 496 | 497 | # Template used to display messages. This is a python new-style format string 498 | # used to format the message information. See doc for all details. 499 | msg-template= 500 | 501 | # Set the output format. Available formats are: text, parseable, colorized, 502 | # json2 (improved json format), json (old json format) and msvs (visual 503 | # studio). You can also give a reporter class, e.g. 504 | # mypackage.mymodule.MyReporterClass. 505 | #output-format= 506 | 507 | # Tells whether to display a full report or only the messages. 508 | reports=no 509 | 510 | # Activate the evaluation score. 511 | score=yes 512 | 513 | 514 | [SIMILARITIES] 515 | 516 | # Comments are removed from the similarity computation 517 | ignore-comments=yes 518 | 519 | # Docstrings are removed from the similarity computation 520 | ignore-docstrings=yes 521 | 522 | # Imports are removed from the similarity computation 523 | ignore-imports=yes 524 | 525 | # Signatures are removed from the similarity computation 526 | ignore-signatures=yes 527 | 528 | # Minimum lines number of a similarity. 529 | min-similarity-lines=4 530 | 531 | 532 | [SPELLING] 533 | 534 | # Limits count of emitted suggestions for spelling mistakes. 535 | max-spelling-suggestions=4 536 | 537 | # Spelling dictionary name. No available dictionaries : You need to install 538 | # both the python package and the system dependency for enchant to work. 539 | spelling-dict= 540 | 541 | # List of comma separated words that should be considered directives if they 542 | # appear at the beginning of a comment and should not be checked. 543 | spelling-ignore-comment-directives=fmt: on,fmt: off,noqa:,noqa,nosec,isort:skip,mypy: 544 | 545 | # List of comma separated words that should not be checked. 546 | spelling-ignore-words= 547 | 548 | # A path to a file that contains the private dictionary; one word per line. 549 | spelling-private-dict-file= 550 | 551 | # Tells whether to store unknown words to the private dictionary (see the 552 | # --spelling-private-dict-file option) instead of raising a message. 553 | spelling-store-unknown-words=no 554 | 555 | 556 | [STRING] 557 | 558 | # This flag controls whether inconsistent-quotes generates a warning when the 559 | # character used as a quote delimiter is used inconsistently within a module. 560 | check-quote-consistency=no 561 | 562 | # This flag controls whether the implicit-str-concat should generate a warning 563 | # on implicit string concatenation in sequences defined over several lines. 564 | check-str-concat-over-line-jumps=no 565 | 566 | 567 | [TYPECHECK] 568 | 569 | # List of decorators that produce context managers, such as 570 | # contextlib.contextmanager. Add to this list to register other decorators that 571 | # produce valid context managers. 572 | contextmanager-decorators=contextlib.contextmanager 573 | 574 | # List of members which are set dynamically and missed by pylint inference 575 | # system, and so shouldn't trigger E1101 when accessed. Python regular 576 | # expressions are accepted. 577 | generated-members= 578 | 579 | # Tells whether to warn about missing members when the owner of the attribute 580 | # is inferred to be None. 581 | ignore-none=yes 582 | 583 | # This flag controls whether pylint should warn about no-member and similar 584 | # checks whenever an opaque object is returned when inferring. The inference 585 | # can return multiple potential results while evaluating a Python object, but 586 | # some branches might not be evaluated, which results in partial inference. In 587 | # that case, it might be useful to still emit no-member and other checks for 588 | # the rest of the inferred objects. 589 | ignore-on-opaque-inference=yes 590 | 591 | # List of symbolic message names to ignore for Mixin members. 592 | ignored-checks-for-mixins=no-member, 593 | not-async-context-manager, 594 | not-context-manager, 595 | attribute-defined-outside-init 596 | 597 | # List of class names for which member attributes should not be checked (useful 598 | # for classes with dynamically set attributes). This supports the use of 599 | # qualified names. 600 | ignored-classes=optparse.Values,thread._local,_thread._local,argparse.Namespace 601 | 602 | # Show a hint with possible names when a member name was not found. The aspect 603 | # of finding the hint is based on edit distance. 604 | missing-member-hint=yes 605 | 606 | # The minimum edit distance a name should have in order to be considered a 607 | # similar match for a missing member name. 608 | missing-member-hint-distance=1 609 | 610 | # The total number of similar names that should be taken in consideration when 611 | # showing a hint for a missing member. 612 | missing-member-max-choices=1 613 | 614 | # Regex pattern to define which classes are considered mixins. 615 | mixin-class-rgx=.*[Mm]ixin 616 | 617 | # List of decorators that change the signature of a decorated function. 618 | signature-mutators= 619 | 620 | 621 | [VARIABLES] 622 | 623 | # List of additional names supposed to be defined in builtins. Remember that 624 | # you should avoid defining new builtins when possible. 625 | additional-builtins= 626 | 627 | # Tells whether unused global variables should be treated as a violation. 628 | allow-global-unused-variables=yes 629 | 630 | # List of names allowed to shadow builtins 631 | allowed-redefined-builtins= 632 | 633 | # List of strings which can identify a callback function by name. A callback 634 | # name must start or end with one of those strings. 635 | callbacks=cb_, 636 | _cb 637 | 638 | # A regular expression matching the name of dummy variables (i.e. expected to 639 | # not be used). 640 | dummy-variables-rgx=_+$|(_[a-zA-Z0-9_]*[a-zA-Z0-9]+?$)|dummy|^ignored_|^unused_ 641 | 642 | # Argument names that match this expression will be ignored. 643 | ignored-argument-names=_.*|^ignored_|^unused_ 644 | 645 | # Tells whether we should check for unused import in __init__ files. 646 | init-import=no 647 | 648 | # List of qualified module names which can have objects that can redefine 649 | # builtins. 650 | redefining-builtins-modules=six.moves,past.builtins,future.builtins,builtins,io 651 | --------------------------------------------------------------------------------