diff --git a/.github/workflows/_reusable-check-api-for-breaking-changes.yml b/.github/workflows/_reusable-check-api-for-breaking-changes.yml index 66f4539e..e03cc666 100644 --- a/.github/workflows/_reusable-check-api-for-breaking-changes.yml +++ b/.github/workflows/_reusable-check-api-for-breaking-changes.yml @@ -21,7 +21,6 @@ jobs: uses: actions/setup-python@v5 with: python-version: x # any version - check-latest: true - name: Install package to check run: | pip install --upgrade . diff --git a/.github/workflows/_reusable-publish-api-comparison.yml b/.github/workflows/_reusable-publish-api-comparison.yml new file mode 100644 index 00000000..4fbbe677 --- /dev/null +++ b/.github/workflows/_reusable-publish-api-comparison.yml @@ -0,0 +1,83 @@ +--- +name: Publish API Breaking Change Check Results +on: + workflow_call: +permissions: + checks: write + pull-requests: write +jobs: + publish-test-results: + runs-on: ubuntu-latest + if: ${{ github.event.workflow_run.event == 'pull_request' && !contains(fromJSON('["skipped", "cancelled", "failed"]'), github.event.workflow_run.conclusion) }} + steps: + - name: Download and Extract Artifacts + uses: dawidd6/action-download-artifact@v6 + with: + run_id: ${{ github.event.workflow_run.id }} + name: breaking_changes + path: artifacts + - name: Check for breaking changes + run: | + if grep -Pzl '\n```\n```' artifacts/breaking_changes.md; then + echo "BREAKING_CHANGES=false" >> $GITHUB_ENV + else + echo "BREAKING_CHANGES=true" >> $GITHUB_ENV + fi + - name: Fetch PR number + id: pr + uses: actions/github-script@v7 + with: + script: | + const maxAttempts = 5; + let attempt = 0; + let pullRequestNumber; + while (attempt < maxAttempts) { + try { + const response = await github.rest.search.issuesAndPullRequests({ + q: 'repo:${{ github.repository }} is:pr sha:${{ github.event.workflow_run.head_sha }}', + per_page: 1, + }); + const items = response.data.items; + if (items.length < 1) { + throw new Error('No PRs found'); + } + pullRequestNumber = items[0].number; + console.info("Pull request number is", pullRequestNumber); + break; // Exit loop on success + } catch (error) { + console.error(`Attempt ${attempt + 1} failed:`, error.message); + if (attempt < maxAttempts - 1) { // Check if not last attempt + console.log(`Waiting for 2 minutes before retrying...`); + await new Promise(resolve => setTimeout(resolve, 120000)); // Wait for 2 minutes + } + } + attempt++; + } + if (!pullRequestNumber) { + core.setFailed("Failed to fetch PR number after 5 attempts"); + } + return pullRequestNumber; + - name: Publish API Breaking Changes Check Results + uses: marocchino/sticky-pull-request-comment@v2 + if: ${{ env.BREAKING_CHANGES == 'true' }} + with: + header: breaking-api-changes + number: ${{ steps.pr.outputs.result }} + recreate: true + path: artifacts/breaking_changes.md + - name: Add workflow link to comment + if: ${{ env.BREAKING_CHANGES == 'true' }} + uses: marocchino/sticky-pull-request-comment@v2 + with: + header: breaking-api-changes + number: ${{ steps.pr.outputs.result }} + append: true + message: |- +

Link to workflow run

+ - name: Delete comment if no breaking changes are found + if: ${{ env.BREAKING_CHANGES == 'false' }} + uses: marocchino/sticky-pull-request-comment@v2 + with: + header: breaking-api-changes + number: ${{ steps.pr.outputs.result }} + delete: true diff --git a/.github/workflows/_reusable-sbom-scan.yml b/.github/workflows/_reusable-sbom-scan.yml index 6b7df4e6..ee37167b 100644 --- a/.github/workflows/_reusable-sbom-scan.yml +++ b/.github/workflows/_reusable-sbom-scan.yml @@ -2,14 +2,14 @@ name: Create & Scan SBOM on: workflow_call: +permissions: + security-events: write + contents: write + id-token: write + attestations: write jobs: create-and-scan-sbom: runs-on: ubuntu-latest - permissions: - security-events: write - contents: write - id-token: write - attestations: write steps: - uses: actions/checkout@v4 - uses: actions/setup-python@v5 diff --git a/.github/workflows/pre-commit.yml b/.github/workflows/pre-commit.yml new file mode 100644 index 00000000..fe4d8149 --- /dev/null +++ b/.github/workflows/pre-commit.yml @@ -0,0 +1,25 @@ +--- +name: Run pre-commit +on: + push: + branches: [main] + pull_request: + branches: [main] +# Cancel running jobs for the same workflow and branch. +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: ${{ github.ref != 'refs/heads/main' }} +jobs: + pre-commit: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-python@v5 + with: + python-version-file: pyproject.toml + - name: Install dependencies + run: | + pip install poetry + poetry install + - name: Run pre-commit + run: poetry run pre-commit run --all-files diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 85792999..8c6f70a2 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -4,6 +4,7 @@ default_stages: [pre-commit] ci: autofix_prs: false autoupdate_schedule: quarterly + skip: [check-poetry, pyright, poetry-audit] repos: - repo: https://github.com/pre-commit/pre-commit-hooks rev: 2c9f875913ee60ca25ce70243dc24d5b6415598c # frozen: v4.6.0 @@ -47,7 +48,7 @@ repos: hooks: - id: blacken-docs files: \.(rst|md|markdown|tex)$ - additional_dependencies: [black==24.4.2] # This may need to be updated/removed in the future once ruff supports formatting python code blocks in markdown + additional_dependencies: [black==24.4.2] # This may need to be updated/removed in the future once ruff supports formatting Python code blocks in markdown args: [--line-length=100] - repo: https://github.com/lyz-code/yamlfix rev: 47039c9bf8039e81f092c9777a1bc8be32fb7870 # frozen: 1.16.0 @@ -74,10 +75,30 @@ repos: - mdformat-toc - mdformat-web - mdformat-wikilink + - repo: https://gitlab.com/smop/pre-commit-hooks + rev: df034f88cf92b394e6f00a78fa97a2aa4e270e60 # frozen: v1.0.0 + hooks: + - id: check-poetry - repo: https://github.com/pappasam/toml-sort rev: b9b6210da457c38122995e434b314f4c4a4a923e # frozen: v0.23.1 hooks: - id: toml-sort-fix + - repo: local + hooks: + - id: pyright + name: pyright + entry: pyright + language: system + types: [python] + pass_filenames: false + - id: poetry-audit + name: poetry-audit + entry: poetry + language: system + types: [toml] + pass_filenames: false + always_run: true + args: [audit, --json, --ignore-code=CVE-2019-8341] - repo: https://github.com/astral-sh/ruff-pre-commit rev: 718fbf5fa5fb8cbe6aeac32a863271695104cd5d # frozen: v0.6.0 hooks: diff --git a/README.md b/README.md index 74283a99..ca4e778f 100644 --- a/README.md +++ b/README.md @@ -23,6 +23,10 @@ Python Packaging CI/CD. - [`enforce-community-standards.yml`](./workflows/enforce-community-standards.md) - This workflow will ensure that all necessary files are in place in order to meet the Open Source Community Standards for a repository. +- [`publish-api-comparison.yml`](./workflows/publish-api-comparison.md) + - This workflow will use the output from the + [`check-api-for-breaking-changes.yml`](./workflows/check-api-for-breaking-changes.md) workflow to create a + comment on the Pull Request that introduces the changes with a detailed breakdown of the changes. - [`sbom-scan.yml`](./workflows/sbom-scan.md) - This workflow will create a Software Bill of Materials (SBOM) for the repository using the [`anchore/sbom-action`](https://github.com/anchore/sbom-action) Action and then scan the diff --git a/pyproject.toml b/pyproject.toml index 2c4ad87a..8feddb4e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -19,13 +19,34 @@ poetry-core = "^1.9.0" poetry-plugin-export = "^1.7.1" poetry-pre-commit-plugin = "^0.1.2" pre-commit = "^3.7" -python = "^3.11" +pyright = "1.1.376" +python = "~3.12" requests = "^2.32.3" toml-sort = "^0.23.1" tomli = "^2.0.1" tomli-w = "^1.0.0" yamlfix = "^1.16.0" +[tool.pyright] +ignore = [ + "temp_*.py" +] +pythonPlatform = "All" +reportCallInDefaultInitializer = "error" +reportImplicitOverride = "error" +reportImplicitStringConcatenation = "error" +reportImportCycles = "error" +reportMissingModuleSource = "error" +reportMissingSuperCall = "error" +reportPropertyTypeMismatch = "error" +reportShadowedImports = "error" +reportUninitializedInstanceVariable = "error" +reportUnnecessaryTypeIgnoreComment = "error" +reportUnusedCallResult = "error" +strict = ["**"] +typeCheckingMode = "strict" +useLibraryCodeForTypes = true + [tool.ruff] line-length = 100 output-format = "concise" @@ -77,4 +98,3 @@ all = true in_place = true spaces_before_inline_comment = 2 overrides."tool.poetry.*".inline_arrays = false -overrides."tool.pylint.*".table_keys = false diff --git a/workflows/check-api-for-breaking-changes.md b/workflows/check-api-for-breaking-changes.md index 1e2431b9..1a2aacba 100644 --- a/workflows/check-api-for-breaking-changes.md +++ b/workflows/check-api-for-breaking-changes.md @@ -2,7 +2,10 @@ This workflow will use the [`griffe`](https://mkdocstrings.github.io/griffe/) package to check for any major or breaking changes in a package's API. It requires that the package be using the -`src` package layout. It runs on the `ubuntu-latest` runner label. +`src` package layout. It runs on the `ubuntu-latest` runner label, uses the +default version of Python available on the runner, will install the package from the current +working directory of the calling repository using `pip install --upgrade .`, and will use the latest +compatible version of [`griffe`](https://pypi.org/project/griffe/) to check for changes. It uploads a file called `breaking_changes.md` as a workflow artifact that can be used with the `publish-api-comparison.yml` workflow to post a comment on Pull Requests with details of changed APIs. diff --git a/workflows/publish-api-comparison.md b/workflows/publish-api-comparison.md new file mode 100644 index 00000000..173cd330 --- /dev/null +++ b/workflows/publish-api-comparison.md @@ -0,0 +1,30 @@ +# publish-api-comparison.yml + +This workflow will use the output from the +[`check-api-for-breaking-changes.yml`](./check-api-for-breaking-changes.md) workflow to create a +comment on the Pull Request that introduces the changes with a detailed breakdown of the changes. +The reason this is a separate workflow that is triggered by the `workflow_run` event is to +allow Pull Requests from forks to still be commented on when there are breaking API changes. Due +to the reduced permissions of workflows that are run against Pull Requests from forks, this +workflow must be a separate workflow so that it has the elevated permissions necessary to +create a comment on the Pull Request. + +In order to use this workflow, the following permissions must be set to +`write`: `checks` and `pull-requests`. The workflow calling this reusable workflow must be set to +trigger on a `completed` `workflow_run` event of the workflow that checks for API breaking changes. + +## Example + +```yaml +name: Publish API Breaking Change Check Results +on: + workflow_run: + workflows: [Check Public API for Breaking Changes] + types: [completed] +jobs: + publish-api-comparison: + uses: tektronix/python-package-ci-cd/.github/workflows/_reusable-publish-api-comparison.yml@main # it is recommended to use the latest release tag instead of `main` + permissions: + checks: write + pull-requests: write +``` diff --git a/workflows/sbom-scan.md b/workflows/sbom-scan.md index 19ef5e96..46f43982 100644 --- a/workflows/sbom-scan.md +++ b/workflows/sbom-scan.md @@ -2,7 +2,10 @@ This workflow will create a Software Bill of Materials (SBOM) for the repository using the [`anchore/sbom-action`](https://github.com/anchore/sbom-action) Action and then scan the SBOM -using the [`anchore/scan-action`](https://github.com/anchore/scan-action) Action. +using the [`anchore/scan-action`](https://github.com/anchore/scan-action) Action. It runs on the `ubuntu-latest` runner label, +uses the default version of Python available on the runner, and will use the latest compatible +version of [`poetry`](https://pypi.org/project/poetry/) to generate the lock file for the calling +repository's Python package. In order to use this workflow, the Python package must be using the [Poetry package manager](https://python-poetry.org/). When calling the reusable workflow, the