diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 0e5b7f4..cf1f49b 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -1 +1 @@ -* @jmeridth @zkoppert +* @jmeridth @sutterj @zkoppert diff --git a/.github/dependabot.yml b/.github/dependabot.yml index db56316..e34ca6e 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -4,7 +4,7 @@ updates: - package-ecosystem: "pip" directory: "/" schedule: - interval: "daily" + interval: "weekly" commit-message: prefix: "chore(deps)" groups: @@ -16,7 +16,7 @@ updates: - package-ecosystem: "github-actions" directory: "/" schedule: - interval: "daily" + interval: "weekly" commit-message: prefix: "chore(deps)" groups: @@ -28,7 +28,7 @@ updates: - package-ecosystem: "docker" directory: "/" schedule: - interval: "daily" + interval: "weekly" commit-message: prefix: "chore(deps)" groups: diff --git a/.github/linters/.jscpd.json b/.github/linters/.jscpd.json index 4120747..109c6c9 100644 --- a/.github/linters/.jscpd.json +++ b/.github/linters/.jscpd.json @@ -1,7 +1,5 @@ { "threshold": 50, - "ignore": [ - "test*" - ], + "ignore": ["test*"], "absolute": true } diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md index d75e0ae..5044773 100644 --- a/.github/pull_request_template.md +++ b/.github/pull_request_template.md @@ -1,5 +1,6 @@ # Pull Request - ## Proposed Changes + ## Readiness Checklist @@ -18,6 +20,7 @@ examples: "feat: add new logger" or "fix: remove unused imports" - [ ] If documentation is needed for this change, has that been included in this pull request - [ ] run `make lint` and fix any issues that you have introduced - [ ] run `make test` and ensure you have test coverage for the lines you are introducing +- [ ] If publishing new data to the public (scorecards, security scan results, code quality results, live dashboards, etc.), please request review from `@jeffrey-luszcz` ### Reviewer diff --git a/.github/release-drafter.yml b/.github/release-drafter.yml index ca2240e..0c09091 100644 --- a/.github/release-drafter.yml +++ b/.github/release-drafter.yml @@ -1,6 +1,6 @@ --- -name-template: 'v$RESOLVED_VERSION' -tag-template: 'v$RESOLVED_VERSION' +name-template: "v$RESOLVED_VERSION" +tag-template: "v$RESOLVED_VERSION" template: | # Changelog $CHANGES @@ -8,61 +8,61 @@ template: | See details of [all code changes](https://github.com/github/contributors/compare/$PREVIOUS_TAG...v$RESOLVED_VERSION) since previous release categories: - - title: '🚀 Features' + - title: "🚀 Features" labels: - - 'feature' - - 'enhancement' - - title: '🐛 Bug Fixes' + - "feature" + - "enhancement" + - title: "🐛 Bug Fixes" labels: - - 'fix' - - 'bugfix' - - 'bug' - - title: '🧰 Maintenance' + - "fix" + - "bugfix" + - "bug" + - title: "🧰 Maintenance" labels: - - 'infrastructure' - - 'automation' - - 'documentation' - - 'dependencies' - - 'maintenance' - - 'revert' - - title: '🏎 Performance' - label: 'performance' -change-template: '- $TITLE @$AUTHOR (#$NUMBER)' + - "infrastructure" + - "automation" + - "documentation" + - "dependencies" + - "maintenance" + - "revert" + - title: "🏎 Performance" + label: "performance" +change-template: "- $TITLE @$AUTHOR (#$NUMBER)" version-resolver: major: labels: - - 'breaking' + - "breaking" minor: labels: - - 'enhancement' - - 'fix' + - "enhancement" + - "fix" patch: labels: - - 'documentation' - - 'maintenance' + - "documentation" + - "maintenance" default: patch autolabeler: - - label: 'automation' + - label: "automation" title: - - '/^(build|ci|perf|refactor|test).*/i' - - label: 'enhancement' + - "/^(build|ci|perf|refactor|test).*/i" + - label: "enhancement" title: - - '/^(style).*/i' - - label: 'documentation' + - "/^(style).*/i" + - label: "documentation" title: - - '/^(docs).*/i' - - label: 'feature' + - "/^(docs).*/i" + - label: "feature" title: - - '/^(feat).*/i' - - label: 'fix' + - "/^(feat).*/i" + - label: "fix" title: - - '/^(fix).*/i' - - label: 'infrastructure' + - "/^(fix).*/i" + - label: "infrastructure" title: - - '/^(infrastructure).*/i' - - label: 'maintenance' + - "/^(infrastructure).*/i" + - label: "maintenance" title: - - '/^(chore|maintenance).*/i' - - label: 'revert' + - "/^(chore|maintenance).*/i" + - label: "revert" title: - - '/^(revert).*/i' + - "/^(revert).*/i" diff --git a/.github/workflows/auto-labeler.yml b/.github/workflows/auto-labeler.yml index 8f9a5af..9fe305e 100644 --- a/.github/workflows/auto-labeler.yml +++ b/.github/workflows/auto-labeler.yml @@ -1,28 +1,24 @@ --- - name: Auto Labeler +name: Auto Labeler - on: - # pull_request event is required only for autolabeler - pull_request: - # Only following types are handled by the action, but one can default to all as well - types: [opened, reopened, synchronize] - # pull_request_target event is required for autolabeler to support PRs from forks - pull_request_target: - types: [opened, reopened, synchronize] +on: + # pull_request_target event is required for autolabeler to support all PRs including forks + pull_request_target: + types: [opened, reopened, edited, synchronize] - permissions: - contents: read +permissions: + contents: read - jobs: - main: - permissions: - contents: write - pull-requests: write - name: Auto label pull requests - runs-on: ubuntu-latest - steps: - - uses: release-drafter/release-drafter@3f0f87098bd6b5c5b9a36d49c41d998ea58f9348 - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - with: - config-name: release-drafter.yml +jobs: + main: + permissions: + contents: write + pull-requests: write + name: Auto label pull requests + runs-on: ubuntu-latest + steps: + - uses: release-drafter/release-drafter@3f0f87098bd6b5c5b9a36d49c41d998ea58f9348 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + with: + config-name: release-drafter.yml diff --git a/.github/workflows/contributors_report.yaml b/.github/workflows/contributors_report.yaml index 00cc164..b41e1a4 100644 --- a/.github/workflows/contributors_report.yaml +++ b/.github/workflows/contributors_report.yaml @@ -3,7 +3,7 @@ name: Monthly contributor report on: workflow_dispatch: schedule: - - cron: '3 2 1 * *' + - cron: "3 2 1 * *" permissions: contents: read @@ -30,7 +30,7 @@ jobs: echo "END_DATE=$end_date" >> "$GITHUB_ENV" - name: Run contributor action - uses: github/contributors@fa291c69abb946173a963a32f20ee29e8a7b6775 + uses: github/contributors@135b0430e856ade27175cbd1d4e1e11b0dd8ef95 env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} START_DATE: ${{ env.START_DATE }} diff --git a/.github/workflows/docker-ci.yml b/.github/workflows/docker-ci.yml index 4869567..9b196b3 100644 --- a/.github/workflows/docker-ci.yml +++ b/.github/workflows/docker-ci.yml @@ -3,9 +3,9 @@ name: Docker Image CI on: push: - branches: main + branches: [main] pull_request: - branches: main + branches: [main] permissions: contents: read @@ -14,6 +14,6 @@ jobs: build: runs-on: ubuntu-latest steps: - - uses: actions/checkout@44c2b7a8a4ea60a981eaca3cf939b5f4305c123b # v4.1.5 + - uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7 - name: Build the Docker image run: docker build . --file Dockerfile --platform linux/amd64 diff --git a/.github/workflows/major-version-updater.yml b/.github/workflows/major-version-updater.yml index ad80de1..23271bb 100644 --- a/.github/workflows/major-version-updater.yml +++ b/.github/workflows/major-version-updater.yml @@ -3,7 +3,7 @@ name: Major Version Updater # Whenever a new release is made, push a major version tag on: release: - types: published + types: [published] permissions: contents: read @@ -15,7 +15,7 @@ jobs: contents: write steps: - name: Checkout Repo - uses: actions/checkout@44c2b7a8a4ea60a981eaca3cf939b5f4305c123b # v4.1.5 + uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7 - name: version id: version diff --git a/.github/workflows/pr-title.yml b/.github/workflows/pr-title.yml index 50a04a8..08a6625 100644 --- a/.github/workflows/pr-title.yml +++ b/.github/workflows/pr-title.yml @@ -4,10 +4,7 @@ name: "Lint PR Title" on: pull_request_target: - types: - - opened - - edited - - synchronize + types: [opened, reopened, edited, synchronize] permissions: contents: read @@ -20,7 +17,7 @@ jobs: name: Validate PR title runs-on: ubuntu-latest steps: - - uses: amannn/action-semantic-pull-request@cfb60706e18bc85e8aec535e3c577abe8f70378e + - uses: amannn/action-semantic-pull-request@0723387faaf9b38adef4775cd42cfd5155ed6017 env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} with: diff --git a/.github/workflows/python-ci.yml b/.github/workflows/python-ci.yml index cf0bca0..d997950 100644 --- a/.github/workflows/python-ci.yml +++ b/.github/workflows/python-ci.yml @@ -6,9 +6,9 @@ name: Python package on: push: - branches: main + branches: [main] pull_request: - branches: main + branches: [main] permissions: contents: read @@ -20,9 +20,9 @@ jobs: matrix: python-version: [3.11, 3.12] steps: - - uses: actions/checkout@44c2b7a8a4ea60a981eaca3cf939b5f4305c123b # v4.1.5 + - uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7 - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@82c7e631bb3cdc910f68e0081d67478d79c6982d + uses: actions/setup-python@39cd14951b08e74b54015e9e001cdefcf80e669f with: python-version: ${{ matrix.python-version }} - name: Install dependencies diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 6398697..a5eeeef 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -1,94 +1,92 @@ --- - name: Release +name: Release - on: - workflow_dispatch: - pull_request: - types: - - closed - branches: - - main +on: + workflow_dispatch: + pull_request_target: + types: [closed] + branches: [main] - permissions: - contents: read +permissions: + contents: read - jobs: - create_release: - # release if - # manual deployment OR - # merged to main and labelled with release labels - if: | - (github.event_name == 'workflow_dispatch') || - (github.event.pull_request.merged == true && - (contains(github.event.pull_request.labels.*.name, 'breaking') || - contains(github.event.pull_request.labels.*.name, 'enhancement') || - contains(github.event.pull_request.labels.*.name, 'vuln') || - contains(github.event.pull_request.labels.*.name, 'release'))) - outputs: - full-tag: ${{ steps.release-drafter.outputs.tag_name }} - short-tag: ${{ steps.get_tag_name.outputs.SHORT_TAG }} - body: ${{ steps.release-drafter.outputs.body }} - runs-on: ubuntu-latest - permissions: - contents: write - pull-requests: read - steps: - - uses: release-drafter/release-drafter@3f0f87098bd6b5c5b9a36d49c41d998ea58f9348 - id: release-drafter - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - with: - config-name: release-drafter.yml - publish: true - - name: Get the short tag - id: get_tag_name - run: | - short_tag=$(echo ${{ steps.release-drafter.outputs.tag_name }} | cut -d. -f1) - echo "SHORT_TAG=$short_tag" >> $GITHUB_OUTPUT - create_action_images: - needs: create_release - runs-on: ubuntu-latest - permissions: - packages: write - env: - REGISTRY: ghcr.io - IMAGE_NAME: ${{ github.repository }} - steps: - - name: Set up Docker Buildx - uses: docker/setup-buildx-action@d70bba72b1f3fd22344832f00baa16ece964efeb - - name: Log in to the Container registry - uses: docker/login-action@e92390c5fb421da1463c202d546fed0ec5c39f20 - with: - registry: ${{ env.REGISTRY }} - username: ${{ github.actor }} - password: ${{ secrets.GITHUB_TOKEN }} - - uses: actions/checkout@44c2b7a8a4ea60a981eaca3cf939b5f4305c123b # v4.1.5 - - name: Push Docker Image - if: ${{ success() }} - uses: docker/build-push-action@2cdde995de11925a030ce8070c3d77a52ffcf1c0 - with: - context: . - file: ./Dockerfile - push: true - tags: | - ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:latest - ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ needs.create_release.outputs.full-tag }} - ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ needs.create_release.outputs.short-tag }} - platforms: linux/amd64 - provenance: false - sbom: false - create_discussion: - needs: create_release - runs-on: ubuntu-latest - permissions: - discussions: write - steps: - - name: Create an announcement discussion for release - uses: abirismyname/create-discussion@6e6ef67e5eeb042343ef8b3d8d0f5d545cbdf024 - env: - GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} - with: - title: ${{ needs.create_release.outputs.full-tag }} - body: ${{ needs.create_release.outputs.body }} - repository-id: ${{ secrets.RELEASE_DISCUSSION_REPOSITORY_ID }} - category-id: ${{ secrets.RELEASE_DISCUSSION_CATEGORY_ID }} +jobs: + create_release: + # release if + # manual deployment OR + # merged to main and labelled with release labels + if: | + (github.event_name == 'workflow_dispatch') || + (github.event.pull_request.merged == true && + (contains(github.event.pull_request.labels.*.name, 'breaking') || + contains(github.event.pull_request.labels.*.name, 'feature') || + contains(github.event.pull_request.labels.*.name, 'vuln') || + contains(github.event.pull_request.labels.*.name, 'release'))) + outputs: + full-tag: ${{ steps.release-drafter.outputs.tag_name }} + short-tag: ${{ steps.get_tag_name.outputs.SHORT_TAG }} + body: ${{ steps.release-drafter.outputs.body }} + runs-on: ubuntu-latest + permissions: + contents: write + pull-requests: read + steps: + - uses: release-drafter/release-drafter@3f0f87098bd6b5c5b9a36d49c41d998ea58f9348 + id: release-drafter + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + with: + config-name: release-drafter.yml + publish: true + - name: Get the short tag + id: get_tag_name + run: | + short_tag=$(echo ${{ steps.release-drafter.outputs.tag_name }} | cut -d. -f1) + echo "SHORT_TAG=$short_tag" >> $GITHUB_OUTPUT + create_action_images: + needs: create_release + runs-on: ubuntu-latest + permissions: + packages: write + env: + REGISTRY: ghcr.io + IMAGE_NAME: ${{ github.repository }} + steps: + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@988b5a0280414f521da01fcc63a27aeeb4b104db + - name: Log in to the Container registry + uses: docker/login-action@9780b0c442fbb1117ed29e0efdff1e18412f7567 + with: + registry: ${{ env.REGISTRY }} + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + - uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7 + - name: Push Docker Image + if: ${{ success() }} + uses: docker/build-push-action@16ebe778df0e7752d2cfcbd924afdbbd89c1a755 + with: + context: . + file: ./Dockerfile + push: true + tags: | + ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:latest + ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ needs.create_release.outputs.full-tag }} + ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ needs.create_release.outputs.short-tag }} + platforms: linux/amd64 + provenance: false + sbom: false + create_discussion: + needs: create_release + runs-on: ubuntu-latest + permissions: + discussions: write + steps: + - name: Create an announcement discussion for release + uses: abirismyname/create-discussion@6e6ef67e5eeb042343ef8b3d8d0f5d545cbdf024 + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + with: + title: ${{ needs.create_release.outputs.full-tag }} + body: ${{ needs.create_release.outputs.body }} + repository-id: ${{ secrets.RELEASE_DISCUSSION_REPOSITORY_ID }} + category-id: ${{ secrets.RELEASE_DISCUSSION_CATEGORY_ID }} diff --git a/.github/workflows/scorecard.yml b/.github/workflows/scorecard.yml index 62b75c5..281794e 100644 --- a/.github/workflows/scorecard.yml +++ b/.github/workflows/scorecard.yml @@ -9,9 +9,9 @@ on: # To guarantee Maintained check is occasionally updated. See # https://github.com/ossf/scorecard/blob/main/docs/checks.md#maintained schedule: - - cron: '29 11 * * 6' + - cron: "29 11 * * 6" push: - branches: ["main"] + branches: [main] permissions: read-all @@ -25,23 +25,23 @@ jobs: steps: - name: "Checkout code" - uses: actions/checkout@44c2b7a8a4ea60a981eaca3cf939b5f4305c123b # v4.1.5 + uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7 with: persist-credentials: false - name: "Run analysis" - uses: ossf/scorecard-action@0864cf19026789058feabb7e87baa5f140aac736 # v2.3.1 + uses: ossf/scorecard-action@62b2cac7ed8198b15735ed49ab1e5cf35480ba46 # v2.4.0 with: results_file: results.sarif results_format: sarif publish_results: true - name: "Upload artifact" - uses: actions/upload-artifact@65462800fd760344b1a7b4382951275a0abb4808 # v4.3.3 + uses: actions/upload-artifact@834a144ee995460fba8ed112a2fc961b36a5ec5a # v4.3.6 with: name: SARIF file path: results.sarif retention-days: 5 - name: "Upload to code-scanning" - uses: github/codeql-action/upload-sarif@d39d31e687223d841ef683f52467bd88e9b21c14 # v3.25.3 + uses: github/codeql-action/upload-sarif@429e1977040da7a23b6822b13c129cd1ba93dbb2 # v3.26.2 with: sarif_file: results.sarif diff --git a/.github/workflows/super-linter.yaml b/.github/workflows/super-linter.yaml index d5018b3..fe4f9da 100644 --- a/.github/workflows/super-linter.yaml +++ b/.github/workflows/super-linter.yaml @@ -3,7 +3,7 @@ name: Lint Code Base on: pull_request: - branches: main + branches: [main] permissions: contents: read @@ -18,7 +18,7 @@ jobs: statuses: write steps: - name: Checkout Code - uses: actions/checkout@44c2b7a8a4ea60a981eaca3cf939b5f4305c123b # v4.1.5 + uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7 with: fetch-depth: 0 - name: Install dependencies @@ -26,7 +26,7 @@ jobs: python -m pip install --upgrade pip pip install -r requirements.txt -r requirements-test.txt - name: Lint Code Base - uses: super-linter/super-linter@4758be622215d0954c8353ee4877ffd60111cf8e + uses: super-linter/super-linter@02a1172d274f021e4c70f66e23f1085eadd1064b env: DEFAULT_BRANCH: main GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.gitignore b/.gitignore index 142fc9f..71ed06e 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,6 @@ # Output files contributors.md +contributors.json # Byte-compiled / optimized / DLL files __pycache__/ diff --git a/.vscode/settings.json b/.vscode/settings.json index 9c93b59..29e2d20 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,11 +1,8 @@ { - "python.testing.pytestArgs": [ - "." - ], - "python.testing.unittestEnabled": false, - "python.testing.pytestEnabled": true, - "[python]": { - "editor.defaultFormatter": "ms-python.black-formatter" - }, - "python.formatting.provider": "none" + "python.testing.pytestArgs": ["."], + "python.testing.unittestEnabled": false, + "python.testing.pytestEnabled": true, + "[python]": { + "editor.defaultFormatter": "ms-python.black-formatter" + } } diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 22fd416..9006f33 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,5 +1,6 @@ + # Contributing to contributors First off, thanks for taking the time to contribute! :heart: @@ -7,6 +8,7 @@ First off, thanks for taking the time to contribute! :heart: 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: + ## Table of Contents - [I Have a Question](#i-have-a-question) @@ -36,6 +38,7 @@ When contributing to this project, you must agree that you have authored 100% of ## Reporting Bugs + ### Before Submitting a Bug Report 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. @@ -51,6 +54,7 @@ A good bug report shouldn't leave others needing to chase you up for more inform - Can you reliably reproduce the issue? And can you also reproduce it with older versions? + ### How Do I Submit a Good Bug Report? Please submit a bug report using our [GitHub Issues template](https://github.com/github/contributors/issues/new?template=bug_report.yml). @@ -60,6 +64,7 @@ Please submit a bug report using our [GitHub Issues template](https://github.com This section guides you through submitting an enhancement suggestion for contributors, **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. + ### Before Submitting an Enhancement - Make sure that you are using the latest version. @@ -68,6 +73,7 @@ This section guides you through submitting an enhancement suggestion for contrib - 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. + ### How Do I Submit a Good Enhancement Suggestion? Please submit an enhancement suggestion using our [GitHub Issues template](https://github.com/github/contributors/issues/new?template=feature_request.yml). @@ -80,4 +86,4 @@ We are using [Conventional Commits](https://www.conventionalcommits.org/en/v1.0. 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. -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/contributors/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. +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/contributors/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. diff --git a/Dockerfile b/Dockerfile index 36b9bdd..721dcd4 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,6 +1,6 @@ #checkov:skip=CKV_DOCKER_2 #checkov:skip=CKV_DOCKER_3 -FROM python:3.12-slim +FROM python:3.12-slim@sha256:59c7332a4a24373861c4a5f0eec2c92b87e3efeb8ddef011744ef9a751b1d11c LABEL com.github.actions.name="contributors" \ com.github.actions.description="GitHub Action that given an organization or repository, produces information about the contributors over the specified time period." \ com.github.actions.icon="users" \ diff --git a/README.md b/README.md index 6dd5c3a..efab3d9 100644 --- a/README.md +++ b/README.md @@ -62,29 +62,29 @@ This action can be configured to authenticate with GitHub App Installation or Pe ##### GitHub App Installation -| field | required | default | description | -|-------------------------------|----------|---------|-------------| -| `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. | -| `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. | -| `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. | +| field | required | default | description | +| ------------------------ | -------- | ------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `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. | +| `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. | +| `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. | ##### Personal Access Token (PAT) -| field | required | default | description | -|-------------------------------|----------|---------|-------------| -| `GH_TOKEN` | True | `""` | The GitHub Token used to scan the repository. Must have read access to all repository you are interested in scanning. | +| field | required | default | description | +| ---------- | -------- | ------- | --------------------------------------------------------------------------------------------------------------------- | +| `GH_TOKEN` | True | `""` | The GitHub Token used to scan the repository. Must have read access to all repository you are interested in scanning. | #### Other Configuration Options -| field | required | default | description | -|---------------------------|----------|----------|-------------| -| `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. | -| `ORGANIZATION` | Required to have `ORGANIZATION` or `REPOSITORY` | | The name of the GitHub organization which you want the contributor information of all repos from. ie. github.com/github would be `github` | -| `REPOSITORY` | Required to have `ORGANIZATION` or `REPOSITORY` | | The name of the repository and organization which you want the contributor information from. ie. `github/contributors` or a comma separated list of multiple repositories `github/contributor,super-linter/super-linter` | -| `START_DATE` | False | Beginning of time | The date from which you want to start gathering contributor information. ie. Aug 1st, 2023 would be `2023-08-01`. | -| `END_DATE` | False | Current Date | The date at which you want to stop gathering contributor information. Must be later than the `START_DATE`. ie. Aug 2nd, 2023 would be `2023-08-02` | -| `SPONSOR_INFO` | False | False | If you want to include sponsor information in the output. This will include the sponsor count and the sponsor URL. This will impact action performance. ie. SPONSOR_INFO = "False" or SPONSOR_INFO = "True" | -| `LINK_TO_PROFILE` | False | True | If you want to link usernames to their GitHub profiles in the output. ie. LINK_TO_PROFILE = "True" or LINK_TO_PROFILE = "False" | +| field | required | default | description | +| ------------------- | ----------------------------------------------- | ----------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | +| `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. | +| `ORGANIZATION` | Required to have `ORGANIZATION` or `REPOSITORY` | | The name of the GitHub organization which you want the contributor information of all repos from. ie. github.com/github would be `github` | +| `REPOSITORY` | Required to have `ORGANIZATION` or `REPOSITORY` | | The name of the repository and organization which you want the contributor information from. ie. `github/contributors` or a comma separated list of multiple repositories `github/contributor,super-linter/super-linter` | +| `START_DATE` | False | Beginning of time | The date from which you want to start gathering contributor information. ie. Aug 1st, 2023 would be `2023-08-01`. | +| `END_DATE` | False | Current Date | The date at which you want to stop gathering contributor information. Must be later than the `START_DATE`. ie. Aug 2nd, 2023 would be `2023-08-02` | +| `SPONSOR_INFO` | False | False | If you want to include sponsor information in the output. This will include the sponsor count and the sponsor URL. This will impact action performance. ie. SPONSOR_INFO = "False" or SPONSOR_INFO = "True" | +| `LINK_TO_PROFILE` | False | True | If you want to link usernames to their GitHub profiles in the output. ie. LINK_TO_PROFILE = "True" or LINK_TO_PROFILE = "False" | **Note**: If `start_date` and `end_date` are specified then the action will determine if the contributor is new. A new contributor is one that has contributed in the date range specified but not before the start date. @@ -99,7 +99,7 @@ name: Monthly contributor report on: workflow_dispatch: schedule: - - cron: '3 2 1 * *' + - cron: "3 2 1 * *" permissions: contents: read @@ -117,14 +117,14 @@ jobs: run: | # Calculate the first day of the previous month start_date=$(date -d "last month" +%Y-%m-01) - + # Calculate the last day of the previous month end_date=$(date -d "$start_date +1 month -1 day" +%Y-%m-%d) - + #Set an environment variable with the date range echo "START_DATE=$start_date" >> "$GITHUB_ENV" echo "END_DATE=$end_date" >> "$GITHUB_ENV" - + - name: Run contributor action uses: github/contributors@v1 env: @@ -133,7 +133,7 @@ jobs: END_DATE: ${{ env.END_DATE }} ORGANIZATION: SPONSOR_INFO: "true" - + - name: Create issue uses: peter-evans/create-issue-from-file@v5 with: @@ -148,16 +148,16 @@ jobs: ```markdown # Contributors -- Date range for contributor list: 2021-01-01 to 2023-10-10 +- Date range for contributor list: 2021-01-01 to 2023-10-10 - Organization: super-linter | Total Contributors | Total Contributions | % new contributors | -| --- | --- | --- | -| 1 | 143 | 0% | +| ------------------ | ------------------- | ------------------ | +| 1 | 143 | 0% | -| Username | Contribution Count | New Contributor | Commits | -| --- | --- | --- | --- | -| @zkoppert | 143 | False | [super-linter/super-linter](https://github.com/super-linter/super-linter/commits?author=zkoppert&since=2021-01-01&until=2023-10-10) | +| Username | All Time Contribution Count | New Contributor | Commits between 2021-01-01 and 2023-10-10 | +| --------- | --------------------------- | --------------- | ----------------------------------------------------------------------------------------------------------------------------------- | +| @zkoppert | 143 | False | [super-linter/super-linter](https://github.com/super-linter/super-linter/commits?author=zkoppert&since=2021-01-01&until=2023-10-10) | ``` ## Example Markdown output with no dates supplied @@ -168,12 +168,12 @@ jobs: - Organization: super-linter | Total Contributors | Total Contributions | % new contributors | -| --- | --- | --- | -| 1 | 1913 | 0% | +| ------------------ | ------------------- | ------------------ | +| 1 | 1913 | 0% | -| Username | Contribution Count | New Contributor | Sponsor URL | Commits | -| --- | --- | --- | --- | --- | -| @zkoppert | 1913 | False | [Sponsor Link](https://github.com/sponsors/zkoppert) | [super-linter/super-linter](https://github.com/super-linter/super-linter/commits?author=zkoppert&since=2021-09-01&until=2023-09-30) | +| Username | All Time Contribution Count | New Contributor | Sponsor URL | Commits between 2021-09-01 and 2023-09-30 | +| --------- | --------------------------- | --------------- | ---------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------- | +| @zkoppert | 1913 | False | [Sponsor Link](https://github.com/sponsors/zkoppert) | [super-linter/super-linter](https://github.com/super-linter/super-linter/commits?author=zkoppert&since=2021-09-01&until=2023-09-30) | ``` ## Local usage without Docker diff --git a/action.yml b/action.yml index 18b4f59..b05a592 100644 --- a/action.yml +++ b/action.yml @@ -1,10 +1,10 @@ --- -name: 'Contributors Action' -author: 'github' -description: 'A GitHub Action to report out contributors and contributions to a repository or organization' +name: "Contributors Action" +author: "github" +description: "A GitHub Action to report out contributors and contributions to a repository or organization" runs: - using: 'docker' - image: 'docker://ghcr.io/github/contributors:v1' + using: "docker" + image: "docker://ghcr.io/github/contributors:v1" branding: - icon: 'users' - color: 'green' + icon: "users" + color: "green" diff --git a/contributors.py b/contributors.py index a074689..6518fbc 100644 --- a/contributors.py +++ b/contributors.py @@ -6,6 +6,7 @@ import auth import contributor_stats import env +import json_writer import markdown @@ -72,7 +73,16 @@ def main(): sponsor_info, link_to_profile, ) - # write_to_json(contributors) + json_writer.write_to_json( + filename="contributors.json", + start_date=start_date, + end_date=end_date, + organization=organization, + repository_list=repository_list, + sponsor_info=sponsor_info, + link_to_profile=link_to_profile, + contributors=contributors, + ) def get_all_contributors( diff --git a/env.py b/env.py index 3cc796b..3842ada 100644 --- a/env.py +++ b/env.py @@ -10,16 +10,20 @@ from dotenv import load_dotenv -def get_bool_env_var(env_var_name: str) -> bool: +def get_bool_env_var(env_var_name: str, default: bool = False) -> bool: """Get a boolean environment variable. Args: env_var_name: The name of the environment variable to retrieve. + default: The default value to return if the environment variable is not set. Returns: The value of the environment variable as a boolean. """ - return os.environ.get(env_var_name, "").strip().lower() == "true" + ev = os.environ.get(env_var_name, "") + if ev == "" and default: + return default + return ev.strip().lower() == "true" def get_int_env_var(env_var_name: str) -> int | None: @@ -43,6 +47,8 @@ def get_int_env_var(env_var_name: str) -> int | None: def validate_date_format(env_var_name: str) -> str: """Validate the date format of the environment variable. + Does nothing if the environment variable is not set. + Args: env_var_name: The name of the environment variable to retrieve. @@ -50,6 +56,10 @@ def validate_date_format(env_var_name: str) -> str: The value of the environment variable as a string. """ date_to_validate = os.getenv(env_var_name, "") + + if not date_to_validate: + return date_to_validate + pattern = "%Y-%m-%d" try: datetime.datetime.strptime(date_to_validate, pattern) @@ -121,8 +131,8 @@ def get_env_vars( start_date = validate_date_format("START_DATE") end_date = validate_date_format("END_DATE") - sponsor_info = get_bool_env_var("SPONSOR_INFO") - link_to_profile = get_bool_env_var("LINK_TO_PROFILE") + sponsor_info = get_bool_env_var("SPONSOR_INFO", False) + link_to_profile = get_bool_env_var("LINK_TO_PROFILE", False) # Separate repositories_str into a list based on the comma separator repositories_list = [] diff --git a/json_writer.py b/json_writer.py new file mode 100644 index 0000000..85dc607 --- /dev/null +++ b/json_writer.py @@ -0,0 +1,78 @@ +""" This module contains a function that writes data to a JSON file. """ + +import json + + +def write_to_json( + contributors, + filename, + start_date, + end_date, + organization, + repository_list, + sponsor_info, + link_to_profile, +): + """Write data to a JSON file. + + Args: + contributors (list): A list of Contributor objects. + filename (str): The name of the JSON file. + start_date (str): The start date of the date range for the contributor list. + end_date (str): The end date of the date range for the contributor list. + organization (str): The organization for which the contributors are being listed. + repository_list (list): A list of repositories for which the contributors are being listed. + sponsor_info (str): A string indicating whether sponsor information should be included. + link_to_profile (str): A string indicating whether a link to the contributor's profile should be included. + + Returns: + None + """ + + # Prepare data for JSON such that it looks like the markdown data. ie. + # { + # "start_date": "2024-03-08", + # "end_date": "2024-03-15", + # "organization": null, + # "repository_list": [ + # "github/stale-repos", + # "github/issue-metrics", + # "github/contributors", + # "github/automatic-contrib-prs", + # "github/evergreen", + # "github/cleanowners" + # ], + # "sponsor_info": false, + # "link_to_profile": false, + # "contributors": [ + # { + # "username": "zkoppert", + # "new_contributor": false, + # "avatar_url": "https://avatars.githubusercontent.com/u/6935431?v=4", + # "contribution_count": 785, + # "commit_url": "https://github.com/github/stale-repos/commits?author=zkoppert&since=2024-03-08&until=2024-03-15, + # "sponsor_info": "" + # }, + # { + # "username": "jmeridth", + # "new_contributor": false, + # "avatar_url": "https://avatars.githubusercontent.com/u/35014?v=4", + # "contribution_count": 94, + # "commit_url": "https://github.com/github/stale-repos/commits?author=jmeridth&since=2024-03-08&until=2024-03-15, + # "sponsor_info": "" + # } + # ] + # } + data = { + "start_date": start_date, + "end_date": end_date, + "organization": organization, + "repository_list": repository_list, + "sponsor_info": sponsor_info, + "link_to_profile": link_to_profile, + "contributors": [contributor.__dict__ for contributor in contributors], + } + + # Write data to a JSON file + with open(filename, "w", encoding="utf-8") as f: + json.dump(data, f, indent=4) diff --git a/markdown.py b/markdown.py index 23cfc56..1e13cec 100644 --- a/markdown.py +++ b/markdown.py @@ -161,12 +161,15 @@ def get_contributor_table( total_contributions (int): The total number of contributions made by all of the contributors. """ - columns = ["Username", "Contribution Count"] + columns = ["Username", "All Time Contribution Count"] if start_date and end_date: columns += ["New Contributor"] if sponsor_info == "true": columns += ["Sponsor URL"] - columns += ["Commits"] + if start_date and end_date: + columns += [f"Commits between {start_date} and {end_date}"] + else: + columns += ["All Commits"] headers = "| " + " | ".join(columns) + " |\n" headers += "| " + " | ".join(["---"] * len(columns)) + " |\n" diff --git a/requirements-test.txt b/requirements-test.txt index 7032eac..6914185 100644 --- a/requirements-test.txt +++ b/requirements-test.txt @@ -1,8 +1,8 @@ -black==24.4.2 -flake8==7.0.0 -mypy==1.10.0 +black==24.8.0 +flake8==7.1.1 +mypy==1.11.1 mypy-extensions==1.0.0 -pylint==3.1.0 -pytest==8.2.0 +pylint==3.2.6 +pytest==8.3.2 pytest-cov==5.0.0 -types-requests==2.31.0.20240406 +types-requests==2.32.0.20240712 diff --git a/requirements.txt b/requirements.txt index 09bf660..c71cc91 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,3 +1,3 @@ github3.py==4.0.1 python-dotenv==1.0.1 -requests==2.31.0 +requests==2.32.3 diff --git a/test_env.py b/test_env.py index 7ca8f99..7632c41 100644 --- a/test_env.py +++ b/test_env.py @@ -136,6 +136,55 @@ def test_get_env_vars_invalid_start_date(self): "START_DATE environment variable not in the format YYYY-MM-DD", ) + @patch.dict( + os.environ, + { + "ORGANIZATION": "org", + "REPOSITORY": "repo,repo2", + "GH_APP_ID": "", + "GH_APP_INSTALLATION_ID": "", + "GH_APP_PRIVATE_KEY": "", + "GH_TOKEN": "token", + "GH_ENTERPRISE_URL": "", + "START_DATE": "", + "END_DATE": "", + "SPONSOR_INFO": "False", + "LINK_TO_PROFILE": "True", + }, + clear=True, + ) + def test_get_env_vars_no_dates(self): + """ + Test the get_env_vars function when all environment variables are set correctly + and start_date and end_date are not set. + """ + + ( + organization, + repository_list, + gh_app_id, + gh_app_installation_id, + gh_app_private_key_bytes, + token, + ghe, + start_date, + end_date, + sponsor_info, + link_to_profile, + ) = env.get_env_vars() + + self.assertEqual(organization, "org") + self.assertEqual(repository_list, ["repo", "repo2"]) + self.assertIsNone(gh_app_id) + self.assertIsNone(gh_app_installation_id) + self.assertEqual(gh_app_private_key_bytes, b"") + self.assertEqual(token, "token") + self.assertEqual(ghe, "") + self.assertEqual(start_date, "") + self.assertEqual(end_date, "") + self.assertFalse(sponsor_info) + self.assertTrue(link_to_profile) + if __name__ == "__main__": unittest.main() diff --git a/test_env_get_bool.py b/test_env_get_bool.py new file mode 100644 index 0000000..3165de1 --- /dev/null +++ b/test_env_get_bool.py @@ -0,0 +1,81 @@ +"""Test the get_bool_env_var function""" + +import os +import unittest +from unittest.mock import patch + +from env import get_bool_env_var + + +class TestEnv(unittest.TestCase): + """Test the get_bool_env_var function""" + + @patch.dict( + os.environ, + { + "TEST_BOOL": "true", + }, + clear=True, + ) + def test_get_bool_env_var_that_exists_and_is_true(self): + """Test that gets a boolean environment variable that exists and is true""" + result = get_bool_env_var("TEST_BOOL", False) + self.assertTrue(result) + + @patch.dict( + os.environ, + { + "TEST_BOOL": "false", + }, + clear=True, + ) + def test_get_bool_env_var_that_exists_and_is_false(self): + """Test that gets a boolean environment variable that exists and is false""" + result = get_bool_env_var("TEST_BOOL", False) + self.assertFalse(result) + + @patch.dict( + os.environ, + { + "TEST_BOOL": "nope", + }, + clear=True, + ) + def test_get_bool_env_var_that_exists_and_is_false_due_to_invalid_value(self): + """Test that gets a boolean environment variable that exists and is false + due to an invalid value + """ + result = get_bool_env_var("TEST_BOOL", False) + self.assertFalse(result) + + @patch.dict( + os.environ, + { + "TEST_BOOL": "false", + }, + clear=True, + ) + def test_get_bool_env_var_that_does_not_exist_and_default_value_returns_true(self): + """Test that gets a boolean environment variable that does not exist + and default value returns: true + """ + result = get_bool_env_var("DOES_NOT_EXIST", True) + self.assertTrue(result) + + @patch.dict( + os.environ, + { + "TEST_BOOL": "true", + }, + clear=True, + ) + def test_get_bool_env_var_that_does_not_exist_and_default_value_returns_false(self): + """Test that gets a boolean environment variable that does not exist + and default value returns: false + """ + result = get_bool_env_var("DOES_NOT_EXIST", False) + self.assertFalse(result) + + +if __name__ == "__main__": + unittest.main() diff --git a/test_json_writer.py b/test_json_writer.py new file mode 100644 index 0000000..1071ea1 --- /dev/null +++ b/test_json_writer.py @@ -0,0 +1,68 @@ +""" Test the write_to_json function in json_writer.py. """ + +import json +import os +import unittest + +from contributor_stats import ContributorStats +from json_writer import write_to_json + + +class TestWriteToJson(unittest.TestCase): + """Test the write_to_json function.""" + + def setUp(self): + """Set up data for the tests.""" + self.filename = "test.json" + self.data = { + "start_date": "2022-01-01", + "end_date": "2022-01-31", + "organization": "test_org", + "repository_list": ["repo1", "repo2"], + "sponsor_info": False, + "link_to_profile": False, + "contributors": [ + { + "username": "test_user", + "new_contributor": False, + "avatar_url": "https://test_url.com", + "contribution_count": 10, + "commit_url": "https://test_commit_url.com", + "sponsor_info": "", + } + ], + } + + def test_write_to_json(self): + """Test that write_to_json writes the correct data to a JSON file.""" + contributors = ( + ContributorStats( + username="test_user", + new_contributor=False, + avatar_url="https://test_url.com", + contribution_count=10, + commit_url="https://test_commit_url.com", + sponsor_info="", + ), + ) + + write_to_json( + contributors=contributors, + filename=self.filename, + start_date=self.data["start_date"], + end_date=self.data["end_date"], + organization=self.data["organization"], + repository_list=self.data["repository_list"], + sponsor_info=self.data["sponsor_info"], + link_to_profile=self.data["link_to_profile"], + ) + with open(self.filename, "r", encoding="utf-8") as f: + result = json.load(f) + self.assertDictEqual(result, self.data) + + def tearDown(self): + os.remove(self.filename) + + +if __name__ == "__main__": + unittest.main() diff --git a/test_markdown.py b/test_markdown.py index ebee539..f4e46cb 100644 --- a/test_markdown.py +++ b/test_markdown.py @@ -60,7 +60,7 @@ def test_write_to_markdown(self, mock_file): "| Total Contributors | Total Contributions | % New Contributors |\n| --- | --- | --- |\n| 2 | 300 | 50.0% |\n\n" ) mock_file().write.assert_any_call( - "| Username | Contribution Count | New Contributor | Commits |\n" + "| Username | All Time Contribution Count | New Contributor | Commits between 2023-01-01 and 2023-01-02 |\n" "| --- | --- | --- | --- |\n" "| @user1 | 100 | False | commit url |\n" "| @user2 | 200 | True | commit url2 |\n" @@ -114,7 +114,7 @@ def test_write_to_markdown_with_sponsors(self, mock_file): "| Total Contributors | Total Contributions | % New Contributors |\n| --- | --- | --- |\n| 2 | 300 | 50.0% |\n\n" ) mock_file().write.assert_any_call( - "| Username | Contribution Count | New Contributor | Sponsor URL | Commits |\n" + "| Username | All Time Contribution Count | New Contributor | Sponsor URL | Commits between 2023-01-01 and 2023-01-02 |\n" "| --- | --- | --- | --- | --- |\n" "| @user1 | 100 | False | [Sponsor Link](sponsor_url_1) | commit url |\n" "| @user2 | 200 | True | not sponsorable | commit url2 |\n" @@ -168,7 +168,7 @@ def test_write_to_markdown_without_link_to_profile(self, mock_file): "| Total Contributors | Total Contributions | % New Contributors |\n| --- | --- | --- |\n| 2 | 300 | 50.0% |\n\n" ) mock_file().write.assert_any_call( - "| Username | Contribution Count | New Contributor | Commits |\n" + "| Username | All Time Contribution Count | New Contributor | Commits between 2023-01-01 and 2023-01-02 |\n" "| --- | --- | --- | --- |\n" "| user1 | 100 | False | commit url |\n" "| user2 | 200 | True | commit url2 |\n"