From dba949616bbc5e9b1ff26acaacd8b5e554053536 Mon Sep 17 00:00:00 2001 From: Nicholas Felt Date: Wed, 21 Aug 2024 10:41:11 -0700 Subject: [PATCH] Add packaging workflows (#9) * feat: Added a contributor_setup.py script * refactor: Switch dependency updater Docker image to be based on alpine * feat: Added the TestPyPI packaging workflow and supporting action * feat: Finished the initial implementation of the reusable workflow for publishing a package * docs: Update main README.md file with summaries of all actions and workflows * ci: Add tests for each action that this repo provides to help protect against breakages * ci: Update final test for all action verifications to pass * docs: Added links to all used Actions in all reusable workflow documentation * refactor: Updated the `update-development-dependencies` action Python code to not use argparse. Also added support for updating dependencies in the main dependency group. * refactor: Updated the `find-unreleased-changelog-items` action Python code to not use argparse * refactor: Updated the `create-unique-testpypi` action Python code to not use argparse * test: Update the workflow that tests the actions to always have dummy data in the changelog check action job --- .../workflows/_reusable-package-release.yml | 269 ++++++++++++++++++ .../workflows/_reusable-package-testpypi.yml | 85 ++++++ ...ate-python-and-pre-commit-dependencies.yml | 80 +++--- .github/workflows/package-release.yml | 34 +++ .github/workflows/test-actions.yml | 141 +++++++++ ...ate-python-and-pre-commit-dependencies.yml | 41 +-- .gitignore | 2 + .pre-commit-config.yaml | 2 +- CHANGELOG.md | 22 +- CONTRIBUTING.md | 2 +- README.md | 21 +- .../create-unique-testpypi-version/Dockerfile | 11 + .../create-unique-testpypi-version/action.yml | 16 ++ .../create-unique-testpypi-version/main.py | 72 +++++ .../create-unique-testpypi-version/readme.md | 46 +++ .../requirements.txt | 18 ++ .../Dockerfile | 11 + .../action.yml | 32 +++ .../find-unreleased-changelog-items/main.py | 126 ++++++++ .../find-unreleased-changelog-items/readme.md | 56 ++++ .../requirements.txt | 1 + .../Dockerfile | 16 +- .../action.yml | 7 +- ...te_development_dependencies.py => main.py} | 183 ++++-------- .../update-development-dependencies/readme.md | 23 +- .../requirements.txt | 2 +- contributor_setup.py | 98 +++++++ pyproject.toml | 19 +- python_semantic_release_templates/.macros.j2 | 26 ++ .../.release_notes.md.j2 | 9 + .../CHANGELOG.md.j2 | 9 + workflows/check-api-for-breaking-changes.md | 13 +- workflows/codeql-analysis.md | 13 +- workflows/enforce-community-standards.md | 13 +- workflows/package-build.md | 15 +- workflows/package-release.md | 132 +++++++++ workflows/package-testpypi.md | 71 +++++ workflows/publish-api-comparison.md | 13 +- workflows/publish-test-results.md | 13 +- workflows/sbom-scan.md | 17 +- workflows/test-code.md | 20 +- workflows/test-docs.md | 16 +- ...date-python-and-pre-commit-dependencies.md | 32 ++- 43 files changed, 1597 insertions(+), 251 deletions(-) create mode 100644 .github/workflows/_reusable-package-release.yml create mode 100644 .github/workflows/_reusable-package-testpypi.yml create mode 100644 .github/workflows/package-release.yml create mode 100644 .github/workflows/test-actions.yml create mode 100644 actions/create-unique-testpypi-version/Dockerfile create mode 100644 actions/create-unique-testpypi-version/action.yml create mode 100644 actions/create-unique-testpypi-version/main.py create mode 100644 actions/create-unique-testpypi-version/readme.md create mode 100644 actions/create-unique-testpypi-version/requirements.txt create mode 100644 actions/find-unreleased-changelog-items/Dockerfile create mode 100644 actions/find-unreleased-changelog-items/action.yml create mode 100644 actions/find-unreleased-changelog-items/main.py create mode 100644 actions/find-unreleased-changelog-items/readme.md create mode 100644 actions/find-unreleased-changelog-items/requirements.txt rename actions/update-development-dependencies/{update_development_dependencies.py => main.py} (57%) create mode 100644 contributor_setup.py create mode 100644 python_semantic_release_templates/.macros.j2 create mode 100644 python_semantic_release_templates/.release_notes.md.j2 create mode 100644 python_semantic_release_templates/CHANGELOG.md.j2 create mode 100644 workflows/package-release.md create mode 100644 workflows/package-testpypi.md diff --git a/.github/workflows/_reusable-package-release.yml b/.github/workflows/_reusable-package-release.yml new file mode 100644 index 00000000..6cc9043a --- /dev/null +++ b/.github/workflows/_reusable-package-release.yml @@ -0,0 +1,269 @@ +--- +name: Publish to GitHub & PyPI +on: + workflow_call: + inputs: + package-name: + description: The name of the package to release. + required: true + type: string + repo-name: + description: The full name of the repository to use to gate uploads, in the + format `owner/repo`. + required: true + type: string + commit-user-name: + description: The name of the user to use when committing changes to the repository. + required: true + type: string + commit-user-email: + description: The email of the user to use when committing changes to the repository. + required: true + type: string + release-level: + description: | + Select the release level: + patch for backward compatible minor changes and bug fixes, + minor for backward compatible larger changes, + major for non-backward compatible changes. + required: true + type: string + build-and-publish-python-package: + description: A boolean value that determines whether to build and publish + the Python package. If set to `false`, the package binaries will not be + built or published to PyPI, TestPyPI, or GitHub Releases. + required: false + default: true + type: boolean + python-versions-array: + description: A valid JSON array of Python versions to validate the installation + with. If `build-and-publish-python-package` is set to `true`, this input + must be provided or the build will fail. + required: false + type: string + operating-systems-array: + description: A valid JSON array of operating system names to validate the + installation on. + required: false + default: '["ubuntu", "windows", "macos"]' + type: string + previous-changelog-filename: + description: The name of the file to copy the contents of the changelog into + for use in the `python-semantic-release` templates. This file will be created + inside of the directory defined by the `[tool.semantic_release.changelog.template_dir]` + key in the `pyproject.toml` file. + required: false + type: string + default: .previous_changelog_for_template.md + previous-release-notes-filename: + description: The name of the file to copy the contents of the `## Unreleased` + section of the changelog into for use in the GitHub Release Notes. This + file will be created inside of the directory defined by the `[tool.semantic_release.changelog.template_dir]` + key in the `pyproject.toml` file. + required: false + type: string + default: .previous_release_notes_for_template.md + secrets: + checkout-token: + description: The token to use for checking out the repository, must have permissions + to write back to the repository. + required: true + ssh-signing-key-private: + description: A private SSH key associated with the account that owns the `checkout-token` + that will be used to sign the commit and tag created by `python-semantic-release`. + required: true + ssh-signing-key-public: + description: The public SSH key linked to the `secrets.ssh-signing-key-private` + key that will be used to sign the commit and tag created by `python-semantic-release`. + required: true +concurrency: + group: pypi (Reusable Workflows) +env: + PACKAGE_NAME: ${{ inputs.package-name }} +jobs: + # Print the inputs to the summary page for easy User Review + print-inputs: + runs-on: ubuntu-latest + permissions: + contents: read + steps: + - uses: actions/checkout@v4 + - name: python-versions-array input missing + if: ${{ inputs.build-and-publish-python-package == true && (inputs.python-versions-array == null || inputs.python-versions-array == '') }} + run: | + echo "The `python-versions-array` input is required when `build-and-publish-python-package` is set to `true`." + exit 1 + - if: ${{ endsWith(github.repository, '/python-package-ci-cd') }} # Run the local action when this is run in the python-package-ci-cd repository + uses: ./actions/find-unreleased-changelog-items + with: + release-level: ${{ inputs.release-level }} + previous-changelog-filename: ${{ inputs.previous-changelog-filename }} + previous-release-notes-filename: ${{ inputs.previous-release-notes-filename }} + - if: ${{ !endsWith(github.repository, '/python-package-ci-cd') }} # Run the public action when this is run outside the python-package-ci-cd repository + uses: tektronix/python-package-ci-cd/actions/find-unreleased-changelog-items@main # TODO: update branch to tag + with: + release-level: ${{ inputs.release-level }} + previous-changelog-filename: ${{ inputs.previous-changelog-filename }} + previous-release-notes-filename: ${{ inputs.previous-release-notes-filename }} + # Update the package version using the python-semantic-release package (https://github.com/python-semantic-release/python-semantic-release) + # This job requires a Personal Access Token (Classic) with + # the public_repo permission. It also needs a private/public + # ssh key pair that can be used for signing. The public key must + # be attached to the account as an SSH signing key. + bump-version: + name: Update package version + needs: [print-inputs] + if: github.repository == inputs.repo-name && github.ref == 'refs/heads/main' + runs-on: ubuntu-latest + environment: package-release-gate + permissions: + id-token: write + contents: write + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + token: ${{ secrets.checkout-token }} + - if: ${{ endsWith(github.repository, '/python-package-ci-cd') }} # Run the local action when this is run in the python-package-ci-cd repository + uses: ./actions/find-unreleased-changelog-items + with: + previous-changelog-filename: ${{ inputs.previous-changelog-filename }} + previous-release-notes-filename: ${{ inputs.previous-release-notes-filename }} + - if: ${{ !endsWith(github.repository, '/python-package-ci-cd') }} # Run the public action when this is run outside the python-package-ci-cd repository + uses: tektronix/python-package-ci-cd/actions/find-unreleased-changelog-items@main # TODO: update branch to tag + with: + previous-changelog-filename: ${{ inputs.previous-changelog-filename }} + previous-release-notes-filename: ${{ inputs.previous-release-notes-filename }} + - name: Python Semantic Release + uses: python-semantic-release/python-semantic-release@v9.8.3 + id: release + with: + force: ${{ inputs.release-level }} + root_options: -v --strict + github_token: ${{ secrets.checkout-token }} + git_committer_email: ${{ vars.commit-user-email }} + git_committer_name: ${{ vars.commit-user-name }} + ssh_public_signing_key: ${{ secrets.ssh-signing-key-public }} + ssh_private_signing_key: ${{ secrets.ssh-signing-key-private }} + outputs: + built-version: ${{ steps.release.outputs.version }} + # Build the newly updated package + pypi-build: + name: Build package + needs: [print-inputs, bump-version] + if: inputs.build-and-publish-python-package && github.repository == inputs.repo-name + && github.ref == 'refs/heads/main' + runs-on: ubuntu-latest + permissions: + id-token: write + attestations: write + steps: + - uses: actions/checkout@v4 + with: + ref: main # Make sure to check out the latest commit on main, not the original commit that triggered the workflow + fetch-depth: 0 + - name: Build package + uses: hynek/build-and-inspect-python-package@v2.8.0 + with: + attest-build-provenance-github: 'true' + # Upload the official package version to TestPyPI + upload-testpypi: + name: Upload package to TestPyPI + needs: [print-inputs, pypi-build] + if: inputs.build-and-publish-python-package && github.repository == inputs.repo-name + && github.ref == 'refs/heads/main' + runs-on: ubuntu-latest + environment: package-testpypi + permissions: + id-token: write + steps: + - name: Download built packages + uses: actions/download-artifact@v4 + with: + name: Packages + path: dist + - name: Upload package to Test PyPI + uses: pypa/gh-action-pypi-publish@v1.9.0 + with: + repository-url: https://test.pypi.org/legacy/ + # Upload the official package version to PyPI + upload-pypi: + name: Upload package to PyPI + needs: [print-inputs, upload-testpypi] + if: inputs.build-and-publish-python-package && github.repository == inputs.repo-name + && github.ref == 'refs/heads/main' + runs-on: ubuntu-latest + environment: package-release + permissions: + id-token: write + steps: + - name: Download built packages + uses: actions/download-artifact@v4 + with: + name: Packages + path: dist + - name: Upload package to PyPI + uses: pypa/gh-action-pypi-publish@v1.9.0 + # Upload the official package binaries to the GitHub Release + upload-github: + name: Upload package to GitHub Release + needs: [print-inputs, upload-pypi] + if: inputs.build-and-publish-python-package && github.repository == inputs.repo-name + && github.ref == 'refs/heads/main' + runs-on: ubuntu-latest + permissions: + id-token: write + contents: write + steps: + - uses: actions/checkout@v4 + with: + ref: main # Make sure to check out the latest commit on main, not the original commit that triggered the workflow + fetch-depth: 0 + - name: Download built packages + uses: actions/download-artifact@v4 + with: + name: Packages + path: dist + - name: Publish package distributions to GitHub Releases + uses: python-semantic-release/upload-to-gh-release@main + with: + root_options: -v --strict + github_token: ${{ secrets.GITHUB_TOKEN }} + # Verify the package can be installed on all necessary python versions and operating systems from both TestPyPI and PyPI + pypi-install: + name: Install package + needs: + - print-inputs + - bump-version + - pypi-build + - upload-testpypi + - upload-pypi + - upload-github + if: inputs.build-and-publish-python-package && github.repository == inputs.repo-name + && github.ref == 'refs/heads/main' + runs-on: ${{ matrix.os-name }}-latest + permissions: {} + strategy: + fail-fast: false + matrix: + os-name: ${{ fromJSON(inputs.operating-systems-array) }} + python-version: ${{ fromJSON(inputs.python-versions-array) }} + index_urls: + - '' + - ' --index-url=https://test.pypi.org/simple/ --extra-index-url=https://pypi.org/simple' + steps: + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: ${{ matrix.python-version }} + check-latest: true + - name: Test installing package + # A retry is used to allow for some downtime before the package is installable + uses: nick-fields/retry@v3 + with: + timeout_minutes: 10 + max_attempts: 5 + retry_wait_seconds: 30 + warning_on_retry: false + command: pip install${{ matrix.index_urls }} "${{ env.PACKAGE_NAME }}==${{ + needs.bump-version.outputs.built-version }}" diff --git a/.github/workflows/_reusable-package-testpypi.yml b/.github/workflows/_reusable-package-testpypi.yml new file mode 100644 index 00000000..774d17cf --- /dev/null +++ b/.github/workflows/_reusable-package-testpypi.yml @@ -0,0 +1,85 @@ +--- +name: Publish to TestPyPI +on: + workflow_call: + inputs: + package-name: + description: The name of the package to build, upload, and install. + required: true + type: string + repo-name: + description: The full name of the repository to use to gate uploads, in the + format `owner/repo`. + required: true + type: string +concurrency: + group: pypi (Reusable Workflows) +env: + PACKAGE_NAME: ${{ inputs.package-name }} +jobs: + test-pypi-build: + name: Build package with unique version for test.pypi.org + runs-on: ubuntu-latest + permissions: + id-token: write + attestations: write + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + - if: ${{ endsWith(github.repository, '/python-package-ci-cd') }} # Run the local action when this is run in the python-package-ci-cd repository + uses: ./actions/create-unique-testpypi-version + id: create-version + with: + package-name: ${{ inputs.package-name }} + - if: ${{ !endsWith(github.repository, '/python-package-ci-cd') }} # Run the public action when this is run outside the python-package-ci-cd repository + uses: tektronix/python-package-ci-cd/actions/create-unique-testpypi-version@main # TODO: update branch to tag + id: create-version + with: + package-name: ${{ inputs.package-name }} + - name: Build package + uses: hynek/build-and-inspect-python-package@v2.8.0 + with: + attest-build-provenance-github: 'true' + outputs: + built-version: ${{ steps.create-version.outputs.new-version }} + test-pypi-upload: + name: Upload package to test.pypi.org + needs: [test-pypi-build] + if: github.repository == inputs.repo-name + runs-on: ubuntu-latest + environment: package-testpypi + permissions: + id-token: write + steps: + - name: Download built packages + uses: actions/download-artifact@v4 + with: + name: Packages + path: dist + - name: Upload package to Test PyPI + uses: pypa/gh-action-pypi-publish@v1.9.0 + with: + repository-url: https://test.pypi.org/legacy/ + test-pypi-install: + name: Install package from test.pypi.org + needs: [test-pypi-build, test-pypi-upload] + if: github.repository == inputs.repo-name + runs-on: ubuntu-latest + permissions: + contents: read + steps: + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version-file: pyproject.toml + - name: Test installing from test.pypi.org + # A retry is used to allow for some downtime before the package is installable + uses: nick-fields/retry@v3 + with: + timeout_minutes: 10 + max_attempts: 5 + retry_wait_seconds: 30 + warning_on_retry: false + command: pip install --index-url=https://test.pypi.org/simple/ --extra-index-url=https://pypi.org/simple + "$PACKAGE_NAME==${{ needs.test-pypi-build.outputs.built-version }}" diff --git a/.github/workflows/_reusable-update-python-and-pre-commit-dependencies.yml b/.github/workflows/_reusable-update-python-and-pre-commit-dependencies.yml index b8131ef1..6dfa005f 100644 --- a/.github/workflows/_reusable-update-python-and-pre-commit-dependencies.yml +++ b/.github/workflows/_reusable-update-python-and-pre-commit-dependencies.yml @@ -11,39 +11,42 @@ on: description: The email of the user to use when committing changes to the repository. required: true type: string - dependency-dict: - description: 'Specify a valid dictionary of dependency groups to update, where - each key is a dependency group name, and each value is a tuple of dependencies - to update within that group, e.g. {"dev": ("pylint", "ruff"), "tests": ("ruff")}.' - required: false - type: string - default: '' - update-pre-commit: - description: A boolean indicating if the pre-commit hooks should be updated. - required: false - type: boolean - default: false - run-pre-commit: - description: A boolean indicating to run the pre-commit hooks to perform auto-fixing - after updating the dependencies. Setting this input to `true` will also set - the update-pre-commit input to `true`. - required: false - type: boolean - default: false - pre-commit-hook-skip-list: - description: A comma-separated list of pre-commit hooks to skip (only applicable - when `run-pre-commit=true`). - required: false - default: '' - export-dependency-groups: - description: A comma-separated list of dependency groups that should have their - requirements exported. An output folder can be specified by appending a ":" - followed by the custom output folder path to the provided group name, e.g. - "tests:custom/folder/path". The created file will always be named "requirements.txt", - and the folder will default to matching the group name if no custom folder - path is given. - required: false - default: '' + dependency-dict: + description: 'Specify a valid JSON dictionary of dependency groups to update, + where each key is a dependency group name, and each value is a list of dependencies + to update within that group, e.g. {"dev": ["pylint", "ruff"], "tests": ["ruff"]}. + Use an empty string, e.g. "", for dependencies located in the default group' + required: false + type: string + default: '' + update-pre-commit: + description: A boolean indicating if the pre-commit hooks should be updated. + required: false + type: boolean + default: false + run-pre-commit: + description: A boolean indicating to run the pre-commit hooks to perform auto-fixing + after updating the dependencies. Setting this input to `true` will also + set the update-pre-commit input to `true`. + required: false + type: boolean + default: false + pre-commit-hook-skip-list: + description: A comma-separated list of pre-commit hooks to skip (only applicable + when `run-pre-commit=true`). + required: false + type: string + default: '' + export-dependency-groups: + description: A comma-separated list of dependency groups that should have + their requirements exported. An output folder can be specified by appending + a ":" followed by the custom output folder path to the provided group name, + e.g. "tests:custom/folder/path". The created file will always be named "requirements.txt", + and the folder will default to matching the group name if no custom folder + path is given. + required: false + type: string + default: '' secrets: checkout-token: description: The token to use for checking out the repository, must have permissions @@ -74,7 +77,16 @@ jobs: passphrase: ${{ secrets.gpg-signing-key-passphrase }} git_user_signingkey: true git_commit_gpgsign: true - - uses: tektronix/python-package-ci-cd/actions/update-development-dependencies@main # TODO: pin to a version + - if: ${{ endsWith(github.repository, '/python-package-ci-cd') }} # Run the local action when this is run in the python-package-ci-cd repository + uses: ./actions/update-development-dependencies + with: + dependency-dict: ${{ inputs.dependency-dict }} + update-pre-commit: ${{ inputs.update-pre-commit }} + run-pre-commit: ${{ inputs.run-pre-commit }} + pre-commit-hook-skip-list: ${{ inputs.pre-commit-hook-skip-list }} + export-dependency-groups: ${{ inputs.export-dependency-groups }} + - if: ${{ !endsWith(github.repository, '/python-package-ci-cd') }} # Run the public action when this is run outside the python-package-ci-cd repository + uses: tektronix/python-package-ci-cd/actions/update-development-dependencies@main # TODO: update branch to tag with: dependency-dict: ${{ inputs.dependency-dict }} update-pre-commit: ${{ inputs.update-pre-commit }} diff --git a/.github/workflows/package-release.yml b/.github/workflows/package-release.yml new file mode 100644 index 00000000..e2dbf2b5 --- /dev/null +++ b/.github/workflows/package-release.yml @@ -0,0 +1,34 @@ +--- +name: Package Release +on: + workflow_dispatch: + inputs: + release-level: + type: choice + required: true + description: | + Select the release level: + patch for backward compatible minor changes and bug fixes, + minor for backward compatible larger changes, + major for non-backward compatible changes. + options: [patch, minor, major] +concurrency: + group: pypi +jobs: + package-release: + uses: tektronix/tm_devices/.github/workflows/package-release.yml@main + with: + package-name: python-package-ci-cd + repo-name: tektronix/python-package-ci-cd + build-and-publish-python-package: false + commit-user-name: ${{ vars.TEK_OPENSOURCE_NAME }} + commit-user-email: ${{ vars.TEK_OPENSOURCE_EMAIL }} + release-level: ${{ inputs.release-level }} + permissions: + contents: write + id-token: write + attestations: write + secrets: + checkout-token: ${{ secrets.TEK_OPENSOURCE_TOKEN }} + ssh-signing-key-private: ${{ secrets.TEK_OPENSOURCE_SSH_SIGNING_KEY_PRIVATE }} + ssh-signing-key-public: ${{ secrets.TEK_OPENSOURCE_SSH_SIGNING_KEY_PUBLIC }} diff --git a/.github/workflows/test-actions.yml b/.github/workflows/test-actions.yml new file mode 100644 index 00000000..bcf8bc05 --- /dev/null +++ b/.github/workflows/test-actions.yml @@ -0,0 +1,141 @@ +--- +name: Test repository Actions +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' }} +# IMPORTANT: Any new jobs need to be added to the check-tests-passed job to ensure they correctly gate code changes +jobs: + test-create-unique-testpypi-version: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: ./actions/create-unique-testpypi-version + id: create-version + with: + package-name: example-package # this is an example package that is never updated + - name: Verify the new version number + run: | + if [ "${{ steps.create-version.outputs.new-version }}" != "0.0.1.post1" ]; then + echo "The new version number doesn't match the expected version number." + echo "Expected: 0.0.1.post1" + echo "Actual: ${{ steps.create-version.outputs.new-version }}" + exit 1 + fi + test-find-unreleased-changelog-items: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - name: Overwrite CHANGELOG.md with dummy data + run: | + cat < temp_changelog.md + # Changelog + --- + ## Unreleased + Things to be included in the next release go here. + ### Added + - Something will be added + --- + ## v0.0.1 + ### Added + - Something was done here in the past + EOF + cp temp_changelog.md CHANGELOG.md + - uses: ./actions/find-unreleased-changelog-items + with: + release-level: patch + previous-changelog-filename: .testing_previous_changelog_for_template.md + previous-release-notes-filename: .testing_previous_release_notes_for_template.md + - name: Get Job Summary + uses: austenstone/job-summary@v2.0 + id: job-summary + with: + name: temp_job_summary + create-pdf: false + create-pdf-artifact: false + create-md: true + create-md-artifact: false + - name: Compare CHANGELOG.md with the created copy + run: | + FILE1="./temp_changelog.md" + FILE2="./python_semantic_release_templates/.testing_previous_changelog_for_template.md" + if diff "$FILE1" "$FILE2" > /dev/null; then + echo "The changelog files are identical." + else + echo "The changelog files are different. Here is the diff:" + diff "$FILE1" "$FILE2" + exit 1 + fi + - name: Verify created Release Notes + run: | + FILE_PATH="./python_semantic_release_templates/.testing_previous_release_notes_for_template.md" + + # Define the multiline string using a heredoc + MULTILINE_STRING=$(cat <<'EOF' + ## Unreleased + Things to be included in the next release go here. + ### Added + - Something will be added + EOF + ) + + # Compare the file contents to the multiline string + if diff <(echo "$MULTILINE_STRING") "$FILE_PATH" > /dev/null; then + echo "The Release Notes contents are correct." + else + echo "The Release Notes contents are not correct. Here is the diff:" + diff <(echo "$MULTILINE_STRING") "$FILE_PATH" + exit 1 + fi + - name: Verify the Job Summary + run: | + FILE_PATH="./temp_job_summary.md" + + # Define the multiline string using a heredoc + MULTILINE_STRING=$(cat <<'EOF' + ## Workflow Inputs + - release-level: patch + ## Incoming Changes + Things to be included in the next release go here. + ### Added + - Something will be added + EOF + ) + + # Compare the file contents to the multiline string + if diff <(echo "$MULTILINE_STRING") "$FILE_PATH" > /dev/null; then + echo "The Job Summary contents are correct." + else + echo "The Job Summary contents are not correct. Here is the diff:" + diff <(echo "$MULTILINE_STRING") "$FILE_PATH" + exit 1 + fi + test-update-development-dependencies: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: ./actions/update-development-dependencies + with: + update-pre-commit: true + run-pre-commit: true + dependency-dict: '{"dev": ["pyright"]}' + pre-commit-hook-skip-list: remove-tabs,forbid-tabs,check-readthedocs,check-dependabot,check-github-actions,check-github-workflows,commitizen,blacken-docs,yamlfix,hadolint,mdformat,markdown-link-check,check-poetry,toml-sort-fix,pyright,poetry-audit,ruff,ruff-format,docformatter + export-dependency-groups: udd:actions/update-development-dependencies,cutv:actions/create-unique-testpypi-version,fci:actions/find-unreleased-changelog-items + # Check that all jobs passed + check-action-tests-passed: + if: ${{ !cancelled() }} + needs: + - test-create-unique-testpypi-version + - test-find-unreleased-changelog-items + - test-update-development-dependencies + runs-on: ubuntu-latest + steps: + - name: Decide whether the needed jobs succeeded or failed + uses: re-actors/alls-green@release/v1 + with: + jobs: ${{ toJSON(needs) }} diff --git a/.github/workflows/update-python-and-pre-commit-dependencies.yml b/.github/workflows/update-python-and-pre-commit-dependencies.yml index 3ab958c0..c9abdbbd 100644 --- a/.github/workflows/update-python-and-pre-commit-dependencies.yml +++ b/.github/workflows/update-python-and-pre-commit-dependencies.yml @@ -5,33 +5,18 @@ on: branches: [main] jobs: update-python-and-pre-commit-deps: - # TODO: switch to using the Reusable Workflow - name: Update python linters and pre-commit dependencies - runs-on: ubuntu-latest - if: ${{ github.actor == 'dependabot[bot]' && contains(github.head_ref, '/pip/') }} + uses: ./.github/workflows/_reusable-update-python-and-pre-commit-dependencies.yml + with: + commit-user-name: ${{ vars.TEK_OPENSOURCE_NAME }} + commit-user-email: ${{ vars.TEK_OPENSOURCE_EMAIL }} + dependency-dict: '{"dev": ["pyright"]}' + update-pre-commit: true + run-pre-commit: true + pre-commit-hook-skip-list: pyright,poetry-audit + export-dependency-groups: udd:actions/update-development-dependencies,cutv:actions/create-unique-testpypi-version,fci:actions/find-unreleased-changelog-items permissions: contents: write - steps: - - uses: actions/checkout@v4 - with: - fetch-depth: 0 - ref: ${{ github.head_ref }} - token: ${{ secrets.TEK_OPENSOURCE_TOKEN }} - - uses: crazy-max/ghaction-import-gpg@v6 - with: - gpg_private_key: ${{ secrets.TEK_OPENSOURCE_GPG_SIGNING_KEY_PRIVATE }} - passphrase: ${{ secrets.TEK_OPENSOURCE_GPG_SIGNING_KEY_PASSPHRASE }} - git_user_signingkey: true - git_commit_gpgsign: true - - uses: ./actions/update-development-dependencies - with: - update-pre-commit: true - run-pre-commit: true - pre-commit-hook-skip-list: pyright,poetry-audit - export-dependency-groups: udd:actions/update-development-dependencies - - uses: stefanzweifel/git-auto-commit-action@v5 - with: - commit_message: 'chore: Update python linters and pre-commit dependencies.' - commit_user_name: ${{ vars.TEK_OPENSOURCE_NAME }} - commit_user_email: ${{ vars.TEK_OPENSOURCE_EMAIL }} - commit_author: ${{ vars.TEK_OPENSOURCE_NAME }} <${{ vars.TEK_OPENSOURCE_EMAIL }}> + secrets: + checkout-token: ${{ secrets.TEK_OPENSOURCE_TOKEN }} + gpg-signing-key-private: ${{ secrets.TEK_OPENSOURCE_GPG_SIGNING_KEY_PRIVATE }} + gpg-signing-key-passphrase: ${{ secrets.TEK_OPENSOURCE_GPG_SIGNING_KEY_PASSPHRASE }} diff --git a/.gitignore b/.gitignore index 62da3829..1fc6d1c3 100644 --- a/.gitignore +++ b/.gitignore @@ -1,7 +1,9 @@ # Temporary files temp*.* python_semantic_release_templates/.previous_changelog_for_template.md +python_semantic_release_templates/.testing_previous_changelog_for_template.md python_semantic_release_templates/.previous_release_notes_for_template.md +python_semantic_release_templates/.testing_previous_release_notes_for_template.md # Environments poetry.lock diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 18674042..3c0f003f 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -59,7 +59,7 @@ repos: rev: e70baeefd566058716df2f29eae8fe8ffc213a9f # frozen: v2.12.1b3 hooks: - id: hadolint - args: [--ignore=DL3008] + args: [--ignore=DL3008, --ignore=DL3018] - repo: https://github.com/executablebooks/mdformat rev: 08fba30538869a440b5059de90af03e3502e35fb # frozen: 0.7.17 hooks: diff --git a/CHANGELOG.md b/CHANGELOG.md index 1afa590e..b3aacde1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,19 +1,19 @@ # Changelog -The format is based on [Keep a Changelog](https://keepachangelog.com), and this -project adheres to [Semantic Versioning](https://semver.org). +--- + +## Unreleased + +Things to be included in the next release go here. -Valid subsections within a version are: +### Added -- Added -- Changed -- Deprecated -- Removed -- Fixed -- Security +- Something will be added --- -## Unreleased +## v0.0.1 -Things to be included in the next release go here. +### Added + +- Something was done here in the past diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 755d54d5..136970cc 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -55,7 +55,7 @@ Ready to contribute? Here's how to set up `python-package-ci-cd` for local devel - Using the helper script (recommended): ```console - python scripts/contributor_setup.py + python contributor_setup.py ``` 4. Check to see if there are any [open issues](https://github.com/tektronix/python-package-ci-cd/issues) or [pull requests](https://github.com/tektronix/python-package-ci-cd/pulls) that are related to the change you wish to make. diff --git a/README.md b/README.md index 052d7ba0..75a82533 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,6 @@ | | | | ---------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| **Testing** | [![Action testing status](https://github.com/tektronix/python-package-ci-cd/actions/workflows/test-actions.yml/badge.svg?branch=main)](https://github.com/tektronix/python-package-ci-cd/actions/workflows/test-actions.yml) | | **Code Quality** | [![CodeQL status](https://github.com/tektronix/python-package-ci-cd/actions/workflows/codeql-analysis.yml/badge.svg?branch=main)](https://github.com/tektronix/python-package-ci-cd/actions/workflows/codeql-analysis.yml) [![CodeFactor grade](https://www.codefactor.io/repository/github/tektronix/python-package-ci-cd/badge)](https://www.codefactor.io/repository/github/tektronix/python-package-ci-cd) [![pre-commit status](https://results.pre-commit.ci/badge/github/tektronix/python-package-ci-cd/main.svg)](https://results.pre-commit.ci/latest/github/tektronix/python-package-ci-cd/main) | --- @@ -11,7 +12,14 @@ Python Packaging CI/CD. ## Actions -- [`update-development-dependencies`](./actions/update-development-dependencies.md) +- [`create-unique-testpypi-version`](./actions/create-unique-testpypi-version/readme.md) + - This action creates a unique version number for the provided Python package to enable uploading + the package to [TestPyPI](https://test.pypi.org). +- [`find-unreleased-changelog-items`](./actions/find-unreleased-changelog-items/readme.md) + - This action will parse the repository's `CHANGELOG.md` file to determine if + there are any unreleased items. It will fail if it cannot find any unreleased + items, as this means that the package is not ready for a new release. +- [`update-development-dependencies`](./actions/update-development-dependencies/readme.md) - This action enables updating Python development dependencies using the [`Poetry`](https://python-poetry.org/) package manager in-sync with [`pre-commit`](https://pre-commit.com/) hooks. @@ -32,6 +40,17 @@ Python Packaging CI/CD. [`hynek/build-and-inspect-python-package`](https://github.com/hynek/build-and-inspect-python-package) action, and then verify that the package can be installed on each combination of Python version and operating system specified. +- [`package-release.yml`](./workflows/package-release.md) + - This workflow will create a new release of the package using the + [`python-semantic-release`](https://python-semantic-release.readthedocs.io/en/latest/) tool. + It will then build the package, upload the package to [TestPyPI](https://test.pypi.org) and [PyPI](https://pypi.org), + create a new GitHub Release for the project, + and then verify that the package can be installed from [TestPyPI](https://test.pypi.org) and [PyPI](https://pypi.org). +- [`package-testpypi.yml`](./workflows/package-testpypi.md) + - This workflow will build the package using the + [`hynek/build-and-inspect-python-package`](https://github.com/hynek/build-and-inspect-python-package) + action, upload the package to [TestPyPI](https://test.pypi.org), and then verify that the package + can be installed from [TestPyPI](https://test.pypi.org). - [`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 diff --git a/actions/create-unique-testpypi-version/Dockerfile b/actions/create-unique-testpypi-version/Dockerfile new file mode 100644 index 00000000..308cb7a6 --- /dev/null +++ b/actions/create-unique-testpypi-version/Dockerfile @@ -0,0 +1,11 @@ +FROM python:3.12-alpine + +# Copy over necessary files +COPY requirements.txt /requirements.txt +COPY main.py /main.py + +# Install dependencies +RUN python -m pip install --no-cache-dir --requirement /requirements.txt + +# Run the Python script as the entrypoint +CMD ["python", "-u", "/main.py"] diff --git a/actions/create-unique-testpypi-version/action.yml b/actions/create-unique-testpypi-version/action.yml new file mode 100644 index 00000000..83836a31 --- /dev/null +++ b/actions/create-unique-testpypi-version/action.yml @@ -0,0 +1,16 @@ +--- +name: Create unique .postN version number for test.pypi.org +description: Create a unique .postN version number to enable the package to be uploaded + to test.pypi.org. The unique version number is written back to the `pyproject.toml` + file in order to enable building the package with the custom version number. This + action only supports the Poetry package manager. +inputs: + package-name: + description: The name of the package on test.pypi.org + required: true +outputs: + new-version: + description: The new version number with a .postN suffix +runs: + using: docker + image: Dockerfile diff --git a/actions/create-unique-testpypi-version/main.py b/actions/create-unique-testpypi-version/main.py new file mode 100644 index 00000000..a2fd9b9d --- /dev/null +++ b/actions/create-unique-testpypi-version/main.py @@ -0,0 +1,72 @@ +"""Create a unique post-release version for test.pypi.org. + +This script will find the latest version of the package on test.pypi.org and create a new +post-release version, incrementing the `.postN` version if necessary. It will then write that +new version number back to the pyproject.toml file. + +This script will also set the value of a GitHub Actions output variable named `new-version` to the +newly created version number. + +This script needs to be run from a directory that contains a `pyproject.toml` file. +""" + +import os +import pathlib + +import tomli +import tomli_w + +from poetry.core.constraints.version import Version +from pypi_simple import PyPISimple + +_ENV_VAR_TRUE_VALUES = {"1", "true", "yes"} + +PYPROJECT_FILE = pathlib.Path("./pyproject.toml") + + +def main(package_name: str) -> None: + """Run the script to create the new version number. + + Args: + package_name: The name of the package create a unique version for. + """ + test_pypi_server = PyPISimple("https://test.pypi.org/simple/") + print(f"Checking for the latest version of `{package_name}` on test.pypi.org...") + + # Get the latest version of the package on test.pypi.org + latest_version = Version.parse( + test_pypi_server.get_project_page(package_name).packages[-1].version # pyright: ignore[reportArgumentType] + ) + print(f"Current version of `{package_name}` is: {latest_version}") + + # Create the .postN version suffix + new_post_release_num = 1 + if latest_version.post: + new_post_release_num += latest_version.post.number + + # Create the new version number + updated_version = Version.parse( + f"{'.'.join(str(x) for x in latest_version.parts)}.post{new_post_release_num}" + ) + print(f"New version of `{package_name}` will be: {updated_version}") + + # Update the pyproject.toml file with the new version number (only if running in GitHub Actions) + print("Updating the pyproject.toml file with the new version...") + # Read in the current data + with PYPROJECT_FILE.open("rb") as file_handle: + pyproject_data = tomli.load(file_handle) + # Modify the version value + pyproject_data["tool"]["poetry"]["version"] = updated_version.to_string() + # Write back the data to the file + with PYPROJECT_FILE.open("wb") as file_handle: + tomli_w.dump(pyproject_data, file_handle) + + # Set the output variable for GitHub Actions + with open(os.environ["GITHUB_OUTPUT"], "a") as github_output_file_handle: # noqa: PTH123 + github_output_file_handle.write(f"new-version={updated_version}\n") + + +if __name__ == "__main__": + # Run the main function + # See https://docs.github.com/en/actions/writing-workflows/choosing-what-your-workflow-does/store-information-in-variables#default-environment-variables + main(package_name=os.environ["INPUT_PACKAGE-NAME"]) diff --git a/actions/create-unique-testpypi-version/readme.md b/actions/create-unique-testpypi-version/readme.md new file mode 100644 index 00000000..6655f34f --- /dev/null +++ b/actions/create-unique-testpypi-version/readme.md @@ -0,0 +1,46 @@ +# create-unique-testpypi-version + +This action creates a unique version number for the provided Python package to enable uploading +the package to [TestPyPI](https://test.pypi.org/). + +It accomplishes this by creating a unique `.postN` version number. The unique version number is +written back to the `pyproject.toml` file in order to enable building the package with the +custom version number in a subsequent workflow step. This action currently only supports the +[Poetry package manager](https://python-poetry.org/). + +This action is used in the [package-testpypi.yml](../../workflows/package-testpypi.md) +reusable workflow. + +> [!IMPORTANT] +> This action requires that the `pyproject.toml` file exists in the current working directory. + +## Inputs + +| Input variable | Necessity | Description | Default | +| -------------- | --------- | ------------------------------------------------------- | ------- | +| `package-name` | required | The name of the package to create a unique version for. | | + +## Outputs + +| Output variable | Description | +| --------------- | ----------------------------------------------- | +| `new-version` | The new version number created for the package. | + +## Example + +```yaml +jobs: + test-pypi-build: + name: Build package with unique version for test.pypi.org + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + - uses: ./actions/create-unique-testpypi-version + id: create-version + with: + package-name: my-package # required + - name: Build package + uses: hynek/build-and-inspect-python-package@v2.8.0 +``` diff --git a/actions/create-unique-testpypi-version/requirements.txt b/actions/create-unique-testpypi-version/requirements.txt new file mode 100644 index 00000000..6dfbe066 --- /dev/null +++ b/actions/create-unique-testpypi-version/requirements.txt @@ -0,0 +1,18 @@ +annotated-types==0.7.0 ; python_version >= "3.12" and python_version < "3.13" +attrs==24.2.0 ; python_version >= "3.12" and python_version < "3.13" +beautifulsoup4==4.12.3 ; python_version >= "3.12" and python_version < "3.13" +certifi==2024.7.4 ; python_version >= "3.12" and python_version < "3.13" +charset-normalizer==3.3.2 ; python_version >= "3.12" and python_version < "3.13" +idna==3.7 ; python_version >= "3.12" and python_version < "3.13" +mailbits==0.2.1 ; python_version >= "3.12" and python_version < "3.13" +packaging==24.1 ; python_version >= "3.12" and python_version < "3.13" +poetry-core==1.9.0 ; python_version >= "3.12" and python_version < "3.13" +pydantic==2.8.2 ; python_version >= "3.12" and python_version < "3.13" +pydantic-core==2.20.1 ; python_version >= "3.12" and python_version < "3.13" +pypi-simple==1.6.0 ; python_version >= "3.12" and python_version < "3.13" +requests==2.32.3 ; python_version >= "3.12" and python_version < "3.13" +soupsieve==2.6 ; python_version >= "3.12" and python_version < "3.13" +tomli==2.0.1 ; python_version >= "3.12" and python_version < "3.13" +tomli-w==1.0.0 ; python_version >= "3.12" and python_version < "3.13" +typing-extensions==4.12.2 ; python_version >= "3.12" and python_version < "3.13" +urllib3==2.2.2 ; python_version >= "3.12" and python_version < "3.13" diff --git a/actions/find-unreleased-changelog-items/Dockerfile b/actions/find-unreleased-changelog-items/Dockerfile new file mode 100644 index 00000000..308cb7a6 --- /dev/null +++ b/actions/find-unreleased-changelog-items/Dockerfile @@ -0,0 +1,11 @@ +FROM python:3.12-alpine + +# Copy over necessary files +COPY requirements.txt /requirements.txt +COPY main.py /main.py + +# Install dependencies +RUN python -m pip install --no-cache-dir --requirement /requirements.txt + +# Run the Python script as the entrypoint +CMD ["python", "-u", "/main.py"] diff --git a/actions/find-unreleased-changelog-items/action.yml b/actions/find-unreleased-changelog-items/action.yml new file mode 100644 index 00000000..5e601811 --- /dev/null +++ b/actions/find-unreleased-changelog-items/action.yml @@ -0,0 +1,32 @@ +--- +name: Find Unreleased Changelog Items +description: Find all unreleased items in the changelog file and populate files with + the previous changelog contents and the upcoming release note contents for use by + [`python-semantic-release`](https://python-semantic-release.readthedocs.io/en/latest/). + This action will also output incoming changes and the upcoming version bump level + if the `release-level` input is provided. +inputs: + release-level: + description: | + The version bump level: + patch for backward compatible minor changes and bug fixes, + minor for backward compatible larger changes, + major for non-backward compatible changes. + required: false + previous-changelog-filename: + description: The name of the file to copy the contents of the changelog into for + use in the `python-semantic-release` templates. This file will be created inside + of the directory defined by the `[tool.semantic_release.changelog.template_dir]` + key in the `pyproject.toml` file. + required: false + default: .previous_changelog_for_template.md + previous-release-notes-filename: + description: The name of the file to copy the contents of the `## Unreleased` + section of the changelog into for use in the GitHub Release Notes. This file + will be created inside of the directory defined by the `[tool.semantic_release.changelog.template_dir]` + key in the `pyproject.toml` file. + required: false + default: .previous_release_notes_for_template.md +runs: + using: docker + image: Dockerfile diff --git a/actions/find-unreleased-changelog-items/main.py b/actions/find-unreleased-changelog-items/main.py new file mode 100644 index 00000000..d0140298 --- /dev/null +++ b/actions/find-unreleased-changelog-items/main.py @@ -0,0 +1,126 @@ +"""This script will check for unreleased entries in the CHANGELOG.md file. + +It will exit with a non-zero exit code if there are no unreleased entries. + +This script needs to be run from a directory that contains a `pyproject.toml` file and a +`CHANGELOG.md` file. + +This script will do a few things: + - It will copy the necessary files into the defined template directory to properly update the + CHANGELOG.md and render the GitHub Release Notes. + - It can be configured to output the Unreleased changes and incoming version bump level into + the GITHUB_STEP_SUMMARY for easy viewing on the Workflow build summary page. +""" + +from __future__ import annotations + +import os +import pathlib +import re +import shutil + +import tomli + +_ENV_VAR_TRUE_VALUES = {"1", "true", "yes"} +PYPROJECT_FILE = pathlib.Path("./pyproject.toml") +CHANGELOG_FILEPATH = pathlib.Path("./CHANGELOG.md") + + +def _find_template_folder() -> pathlib.Path: + """Find the template folder from the pyproject.toml file. + + Returns: + The path to the template folder. + """ + with PYPROJECT_FILE.open("rb") as file_handle: + pyproject_data = tomli.load(file_handle) + try: + template_folder = pathlib.Path( + pyproject_data["tool"]["semantic_release"]["changelog"]["template_dir"] + ) + except KeyError: + template_folder = pathlib.Path("./templates") + return template_folder + + +def main( + filename_for_previous_changelog: str, + filename_for_previous_release_notes: str, + release_level: str | None, +) -> None: + """Check for entries in the Unreleased section of the CHANGELOG.md file. + + Args: + filename_for_previous_changelog: The filename to use to create the previous changelog file. + filename_for_previous_release_notes: The filename to use to create the previous + release notes file. + release_level: The release level to output to the GitHub Workflow Summary. + + Raises: + SystemExit: Indicates no new entries were found. + """ + # Set the filepaths for the template files + template_folder = _find_template_folder() + template_changelog_filepath = template_folder / pathlib.Path(filename_for_previous_changelog) + template_release_notes_filepath = template_folder / pathlib.Path( + filename_for_previous_release_notes + ) + + release_notes_content = "" + found_entries = False + with CHANGELOG_FILEPATH.open(mode="r", encoding="utf-8") as changelog_file: + tracking_unreleased = False + tracking_entries = False + for line in changelog_file: + if line.startswith(("___", "---")): + tracking_unreleased = False + tracking_entries = False + if line.startswith("## Unreleased"): + tracking_unreleased = True + if tracking_unreleased: + release_notes_content += line + if tracking_unreleased and line.startswith( + ( + "### Added\n", + "### Changed\n", + "### Deprecated\n", + "### Removed\n", + "### Fixed\n", + "### Security\n", + ) + ): + tracking_entries = True + if tracking_entries and not found_entries: + found_entries = bool(re.match(r"^- \w+", line)) + + if not found_entries: + msg = f"No unreleased entries were found in {CHANGELOG_FILEPATH}." + raise SystemExit(msg) + + # Copy the files to the correct location + shutil.copy(CHANGELOG_FILEPATH, template_changelog_filepath) + with template_release_notes_filepath.open("w", encoding="utf-8") as template_release_notes: + template_release_notes.write(release_notes_content.strip() + "\n") + + # If running in GitHub Actions, and the release_level is set, send the release level and + # incoming changes to the GitHub Summary + if release_level: + summary_contents = ( + f"## Workflow Inputs\n- release-level: {release_level}\n" + f"## Incoming Changes\n{release_notes_content.replace('## Unreleased', '').strip()}\n" + ) + print( + f"Adding the following contents to the GitHub Workflow Summary:\n\n{summary_contents}" + ) + with open(os.environ["GITHUB_STEP_SUMMARY"], "a") as summary_file: # noqa: PTH123 + summary_file.write(summary_contents) + + +if __name__ == "__main__": + # Run the main function + # See https://docs.github.com/en/actions/writing-workflows/choosing-what-your-workflow-does/store-information-in-variables#default-environment-variables + main( + filename_for_previous_changelog=os.environ["INPUT_PREVIOUS-CHANGELOG-FILENAME"], + filename_for_previous_release_notes=os.environ["INPUT_PREVIOUS-RELEASE-NOTES-FILENAME"], + release_level=os.getenv("INPUT_RELEASE-LEVEL"), + ) diff --git a/actions/find-unreleased-changelog-items/readme.md b/actions/find-unreleased-changelog-items/readme.md new file mode 100644 index 00000000..29004a0a --- /dev/null +++ b/actions/find-unreleased-changelog-items/readme.md @@ -0,0 +1,56 @@ +# find-unreleased-changelog-items + +This action will parse the repository's `CHANGELOG.md` file to determine if +there are any unreleased items. It will fail if it cannot find any unreleased +items, as this means that the package is not ready for a new release. + +This action will populate two files in the +[`python-semantic-release` templates directory](https://python-semantic-release.readthedocs.io/en/latest/configuration.html#config-changelog-template-dir). +One of those files will contain the contents of the `CHANGELOG.md` file in the +repo prior to creating the new release. The other file will contain the +contents of the `## Unreleased` section of the `CHANGELOG.md` file that +will be used to fill in the GitHub Release Notes. + +> [!IMPORTANT] +> This action requires that the `pyproject.toml` and `CHANGELOG.md` files exist in the +> current working directory. + +> [!IMPORTANT] +> This action requires the `CHANGELOG.md` file to be in a format that is based on +> [Keep a Changelog](https://keepachangelog.com) +> (the primary difference is the `## [Unreleased]` section is replaced by an `## Unreleased` section), +> and this project adheres to [Semantic Versioning](https://semver.org). See this repo's +> [CHANGELOG.md](../../CHANGELOG.md) for an example of the format to use. +> +> Valid subsections within a version are: +> +> - Added +> - Changed +> - Deprecated +> - Removed +> - Fixed +> - Security + +## Inputs + +| Input variable | Necessity | Description | Default | +| --------------------------------- | --------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------- | +| `release-level` | optional | The level of the impending release. Must be one of `major`, `minor`, or `patch`. Setting this input will trigger the action to output the summary of the incoming release level and the unreleased changes to the Workflow Summary. | | +| `previous-changelog-filename` | optional | The name of the file to copy the contents of the changelog into for use in the `python-semantic-release` templates. This file will be created inside of the directory defined by the `[tool.semantic_release.changelog.template_dir]` key in the `pyproject.toml` file. | `'.previous_changelog_for_template.md'` | +| `previous-release-notes-filename` | optional | The name of the file to copy the contents of the `## Unreleased` section of the changelog into for use in the GitHub Release Notes. This file will be created inside of the directory defined by the `[tool.semantic_release.changelog.template_dir]` key in the `pyproject.toml` file. | `'.previous_release_notes_for_template.md'` | + +## Example + +```yaml +jobs: + # Print the inputs to the summary page for easy User Review + print-inputs: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: tektronix/python-package-ci-cd/actions/find-unreleased-changelog-items@main # it is recommended to use the latest release tag instead of `main` + with: + release-level: ${{ inputs.release-level }} # optional + previous-changelog-filename: .previous_changelog_for_template.md # optional + previous-release-notes-filename: .previous_release_notes_for_template.md # optional +``` diff --git a/actions/find-unreleased-changelog-items/requirements.txt b/actions/find-unreleased-changelog-items/requirements.txt new file mode 100644 index 00000000..f29c938b --- /dev/null +++ b/actions/find-unreleased-changelog-items/requirements.txt @@ -0,0 +1 @@ +tomli==2.0.1 ; python_version >= "3.12" and python_version < "3.13" diff --git a/actions/update-development-dependencies/Dockerfile b/actions/update-development-dependencies/Dockerfile index 9392e656..94efb3ac 100644 --- a/actions/update-development-dependencies/Dockerfile +++ b/actions/update-development-dependencies/Dockerfile @@ -1,16 +1,14 @@ -FROM python:3.12-slim-bullseye +FROM python:3.12-alpine # Copy over necessary files COPY requirements.txt /requirements.txt -COPY update_development_dependencies.py /update_development_dependencies.py +COPY main.py /main.py # Install dependencies -RUN echo 'APT::Install-Recommends "0";' > /etc/apt/apt.conf.d/00-docker && \ - apt-get update --quiet && \ - apt-get install --quiet --assume-yes --no-install-recommends git && \ - apt-get clean && \ - rm --force --recursive /var/lib/apt/lists/* +RUN apk update && \ + apk add --no-cache git && \ + rm -rf /var/cache/apk/* RUN python -m pip install --no-cache-dir --requirement /requirements.txt -# Run the updater script as the entrypoint -CMD ["python", "-u", "/update_development_dependencies.py"] +# Run the Python script as the entrypoint +CMD ["python", "-u", "/main.py"] diff --git a/actions/update-development-dependencies/action.yml b/actions/update-development-dependencies/action.yml index 5c47049e..91f020a3 100644 --- a/actions/update-development-dependencies/action.yml +++ b/actions/update-development-dependencies/action.yml @@ -13,9 +13,10 @@ inputs: required: false default: 'false' dependency-dict: - description: 'Specify a valid dictionary of dependency groups to update, where - each key is a dependency group name, and each value is a tuple of dependencies - to update within that group, e.g. {"dev": ("pylint", "ruff"), "tests": ("ruff")}.' + description: 'Specify a valid JSON dictionary of dependency groups to update, + where each key is a dependency group name, and each value is a list of dependencies + to update within that group, e.g. {"dev": ["pylint", "ruff"], "tests": ["ruff"]}. + Use an empty string, e.g. "", for dependencies located in the default group' required: false default: '' update-pre-commit: diff --git a/actions/update-development-dependencies/update_development_dependencies.py b/actions/update-development-dependencies/main.py similarity index 57% rename from actions/update-development-dependencies/update_development_dependencies.py rename to actions/update-development-dependencies/main.py index 29bb5af8..bc02e6f1 100644 --- a/actions/update-development-dependencies/update_development_dependencies.py +++ b/actions/update-development-dependencies/main.py @@ -10,7 +10,6 @@ from __future__ import annotations -import argparse import contextlib import json import os @@ -27,80 +26,7 @@ _ENV_VAR_TRUE_VALUES = {"1", "true", "yes"} -def _parse_arguments() -> argparse.Namespace: - """Parse the command line arguments. - - Returns: - The parsed Namespace. - """ - parser = argparse.ArgumentParser() - parser.add_argument( - "--repo-root", - action="store", - dest="repo_root", - type=Path, - required=True, - help="The root directory of the repository.", - ) - parser.add_argument( - "--install-dependencies", - action="store_true", - default=False, - dest="install_dependencies", - help="Indicate if packages should not be installed via poetry (Primarily used in CI).", - ) - parser.add_argument( - "--dependency-dict", - dest="dependency_dict", - type=_convert_dict_input, - help=( - "Specify a dictionary of dependency groups to update, where each key is a dependency " - "group name, and each value is a tuple of dependencies to update within that group " - '(e.g., \'{"dev": ("pylint", "ruff"), "tests": ("ruff")}\').' - ), - default={}, - ) - parser.add_argument( - "--update-pre-commit", - dest="update_pre_commit", - action="store_true", - help="Update the pre-commit hooks.", - ) - parser.add_argument( - "--run-pre-commit", - dest="run_pre_commit", - action="store_true", - help=( - "Run pre-commit hooks. Setting this flag will also set the " - "--update-pre-commit flag to True." - ), - ) - parser.add_argument( - "--pre-commit-hook-skip-list", - dest="pre_commit_hook_skip_list", - help=( - "Specify a comma-separated list of pre-commit hooks to skip " - "(only applicable when using the --run-pre-commit flag)." - ), - ) - parser.add_argument( - "--export-dependency-group", - dest="dependency_groups", - action="append", - help=( - "Specify a poetry dependency group to export the requirements for. An output " - "folder can be specified by adding a ':' and the custom output folder path to " - 'the provided group name, e.g. "tests:custom/folder/path". The created file will ' - 'always be named "requirements.txt", and the folder will default to matching the ' - "group name if no custom folder path is given. " - "Use the flag multiple times for multiple groups." - ), - ) - - return parser.parse_args() - - -def _convert_dict_input(input_str: str) -> dict[str, tuple[str, ...]]: +def _convert_dict_input(input_str: str) -> dict[str, list[str]]: """Parse the input string into a dictionary of the required type. Args: @@ -110,7 +36,7 @@ def _convert_dict_input(input_str: str) -> dict[str, tuple[str, ...]]: The parsed dictionary. Raises: - argparse.ArgumentTypeError: If the input string does not match the required format. + ValueError: If the input string does not match the required format. """ try: # Convert the string to a dictionary using ast.literal_eval for safety @@ -118,15 +44,14 @@ def _convert_dict_input(input_str: str) -> dict[str, tuple[str, ...]]: # Check if the result is a dictionary with the correct type if isinstance(result_dict, dict) and all( - isinstance(k, str) and isinstance(v, tuple) and all(isinstance(i, str) for i in v) # pyright: ignore[reportUnknownVariableType] + isinstance(k, str) and isinstance(v, list) and all(isinstance(i, str) for i in v) # pyright: ignore[reportUnknownVariableType] for k, v in result_dict.items() # pyright: ignore[reportUnknownVariableType] ): return result_dict # pyright: ignore[reportUnknownVariableType] - msg = "Input does not match the required type of `dict[str, tuple[str, ...]]`." - raise ValueError(msg) # noqa: TRY301 + raise ValueError # noqa: TRY301 except (SyntaxError, ValueError) as e: - msg = f"Error parsing input: {e}" - raise argparse.ArgumentTypeError(msg) # noqa: B904 + msg = f'Input "{input_str}" does not match the required ' f"type of `dict[str, list[str]]`." + raise ValueError(msg) from e def _run_cmd_in_subprocess(command: str) -> None: @@ -143,7 +68,7 @@ def _run_cmd_in_subprocess(command: str) -> None: def _update_poetry_dependencies( python_executable: str, repository_root_directory: Path, - dependencies_to_update: dict[str, tuple[str, ...]], + dependencies_to_update: dict[str, list[str]], *, lock_only: bool, ) -> None: @@ -163,8 +88,9 @@ def _update_poetry_dependencies( # Remove the dependencies from poetry to avoid issues if they are in multiple groups for group, dependencies_list in dependencies_to_update.items(): dependencies = " ".join(f'"{x.split("[", maxsplit=1)[0]}"' for x in dependencies_list) + group_arg = f" --group={group}" if group else "" _run_cmd_in_subprocess( - f'"{python_executable}" -m poetry remove --lock --group={group} {dependencies}', + f'"{python_executable}" -m poetry remove --lock{group_arg} {dependencies}', ) # Get the latest versions for each of the dependencies to update @@ -180,7 +106,8 @@ def _update_poetry_dependencies( # Update dependencies in pyproject.toml using poetry dependencies = " ".join(f'"{x}"' for x in latest_dependency_versions) - poetry_add_cmd = f'"{python_executable}" -m poetry add --group={group} {dependencies}' + group_arg = f" --group={group}" if group else "" + poetry_add_cmd = f'"{python_executable}" -m poetry add{group_arg} {dependencies}' if lock_only: poetry_add_cmd += " --lock" _run_cmd_in_subprocess(poetry_add_cmd) @@ -260,63 +187,65 @@ def _sort_requirements_file(file_path: Path) -> None: _sort_requirements_file(Path(f"{output_folder}/requirements.txt")) -def main() -> None: - """Run the script to update the development dependencies.""" - python_executable = sys.executable +def main( + repo_root: str, + dependency_dict: dict[str, list[str]], + export_dependency_groups: list[str], + pre_commit_hook_skip_list: str, + *, + install_dependencies: bool, + run_pre_commit: bool, + update_pre_commit: bool, +) -> None: + """Run the script to update the development dependencies. - args = _parse_arguments() + Args: + repo_root: The root directory of the repository. + dependency_dict: The dictionary of dependency groups to update, where each key is a group + and each value is a list of dependencies to update within that group. + export_dependency_groups: The list of dependency groups to export the requirements for, + along with optional folder paths. + pre_commit_hook_skip_list: The list of pre-commit hooks to skip. + install_dependencies: A boolean indicating if the dependencies should be installed. + run_pre_commit: A boolean indicating if the pre-commit hooks should be run. + update_pre_commit: A boolean indicating if the pre-commit hooks should be updated. + """ + python_executable = sys.executable - repo_root_path = Path(args.repo_root).resolve() + repo_root_path = Path(repo_root).resolve() os.chdir(repo_root_path) print(f"\nUpdating development dependencies in {Path.cwd()}") _update_poetry_dependencies( python_executable, - args.repo_root, - args.dependency_dict, - lock_only=not args.install_dependencies, + repo_root_path, + dependency_dict, + lock_only=not install_dependencies, ) - if args.update_pre_commit or args.run_pre_commit: + if update_pre_commit or run_pre_commit: _update_pre_commit_dependencies(python_executable, repo_root_path) - if args.dependency_groups: - _export_requirements_files(python_executable, args.dependency_groups) - if args.run_pre_commit: + if export_dependency_groups: + _export_requirements_files(python_executable, export_dependency_groups) + if run_pre_commit: # Run the pre-commit hooks, ignore any errors since they are # just being run to auto-fix files. with contextlib.suppress(subprocess.CalledProcessError): - os.environ["SKIP"] = args.pre_commit_hook_skip_list + os.environ["SKIP"] = pre_commit_hook_skip_list _run_cmd_in_subprocess(f'"{python_executable}" -m pre_commit run --all-files') if __name__ == "__main__": - # Handle GitHub Actions environment variables - # See https://docs.github.com/en/actions/writing-workflows/choosing-what-your-workflow-does/store-information-in-variables#default-environment-variables - if os.getenv("GITHUB_ACTION"): - repo_root = os.getenv("INPUT_REPO-ROOT", "") - install_dependencies = os.getenv("INPUT_INSTALL-DEPENDENCIES", "") - dependency_dict = os.getenv("INPUT_DEPENDENCY-DICT", "") - update_pre_commit = os.getenv("INPUT_UPDATE-PRE-COMMIT", "") - run_pre_commit = os.getenv("INPUT_RUN-PRE-COMMIT", "") - pre_commit_hook_skip_list = os.getenv("INPUT_PRE-COMMIT-HOOK-SKIP-LIST", "") - export_dependency_groups = os.getenv("INPUT_EXPORT-DEPENDENCY-GROUPS", "") - script_args = [ - "--repo-root", - repo_root, - ] - if install_dependencies.lower() in _ENV_VAR_TRUE_VALUES: - script_args.append("--install-dependencies") - if dependency_dict: - script_args.extend(["--dependency-dict", dependency_dict]) - if update_pre_commit.lower() in _ENV_VAR_TRUE_VALUES: - script_args.append("--update-pre-commit") - if run_pre_commit.lower() in _ENV_VAR_TRUE_VALUES: - script_args.append("--run-pre-commit") - if pre_commit_hook_skip_list: - script_args.extend(["--pre-commit-hook-skip-list", pre_commit_hook_skip_list]) - if export_dependency_groups: - for dep_group in export_dependency_groups.split(","): - script_args.extend(["--export-dependency-group", dep_group]) - sys.argv.extend(script_args) - # Run the main function - main() + # See https://docs.github.com/en/actions/writing-workflows/choosing-what-your-workflow-does/store-information-in-variables#default-environment-variables + main( + repo_root=os.environ["INPUT_REPO-ROOT"], + dependency_dict=_convert_dict_input(os.environ["INPUT_DEPENDENCY-DICT"]), + export_dependency_groups=[ + x for x in os.environ["INPUT_EXPORT-DEPENDENCY-GROUPS"].split(",") if x + ], + pre_commit_hook_skip_list=os.environ["INPUT_PRE-COMMIT-HOOK-SKIP-LIST"], + install_dependencies=os.environ["INPUT_INSTALL-DEPENDENCIES"].lower() + in _ENV_VAR_TRUE_VALUES, + run_pre_commit=os.environ["INPUT_RUN-PRE-COMMIT"].lower() in _ENV_VAR_TRUE_VALUES, + update_pre_commit=os.environ["INPUT_UPDATE-PRE-COMMIT"].lower() in _ENV_VAR_TRUE_VALUES, + ) diff --git a/actions/update-development-dependencies/readme.md b/actions/update-development-dependencies/readme.md index fac3aa3f..77fcbefb 100644 --- a/actions/update-development-dependencies/readme.md +++ b/actions/update-development-dependencies/readme.md @@ -5,7 +5,8 @@ This action enables updating Python development dependencies using the [`pre-commit`](https://pre-commit.com/) hooks. > [!IMPORTANT] -> Any job using this action must have at least the following permissions: +> If the job that is using this action needs to commit the changes that this action makes, the job +> must have at least the following permissions: > > ```yaml > permissions: @@ -19,15 +20,15 @@ This action enables updating Python development dependencies using the ## Inputs -| Input variable | Necessity | Description | Default | -| --------------------------- | --------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------- | -| `repo-root` | optional | The root directory of the repository. | `.` | -| `install-dependencies` | optional | A boolean indicating if packages should be installed via poetry (this is not usually needed). | `false` | -| `dependency-dict` | optional | Specify a valid dictionary of dependency groups to update, where each key is a dependency group name, and each value is a tuple of dependencies to update within that group, e.g. `{"dev": ("pylint", "ruff"), "tests": ("ruff")}`. | `{}` | -| `update-pre-commit` | optional | A boolean indicating if the pre-commit hooks should be updated. | `false` | -| `run-pre-commit` | optional | A boolean indicating to run the pre-commit hooks to perform auto-fixing after updating the dependencies. Setting this input to `true` will also set the update-pre-commit input to `true`. | `false` | -| `pre-commit-hook-skip-list` | optional | A comma-separated list of pre-commit hooks to skip (only applicable when `run-pre-commit=true`). | `""` | -| `export-dependency-groups` | optional | A comma-separated list of dependency groups to export to a `requirements.txt` file. The format is `group1,group2:custom-path/to/test/folder`. | `""` | +| Input variable | Necessity | Description | Default | +| --------------------------- | --------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ------- | +| `repo-root` | optional | The root directory of the repository. | `.` | +| `install-dependencies` | optional | A boolean indicating if packages should be installed via poetry (this is not usually needed). | `false` | +| `dependency-dict` | optional | Specify a valid JSON dictionary of dependency groups to update, where each key is a dependency group name, and each value is a list of dependencies to update within that group, e.g. `'{"dev": ["pylint", "ruff"], "tests": ["ruff"]}'`. Use an empty string, e.g. `""`, for dependencies located in the default group' | `{}` | +| `update-pre-commit` | optional | A boolean indicating if the pre-commit hooks should be updated. | `false` | +| `run-pre-commit` | optional | A boolean indicating to run the pre-commit hooks to perform auto-fixing after updating the dependencies. Setting this input to `true` will also set the update-pre-commit input to `true`. | `false` | +| `pre-commit-hook-skip-list` | optional | A comma-separated list of pre-commit hooks to skip (only applicable when `run-pre-commit=true`). | `""` | +| `export-dependency-groups` | optional | A comma-separated list of dependency groups to export to a `requirements.txt` file. The format is `group1,group2:custom-path/to/test/folder`. | `""` | ## Example @@ -49,7 +50,7 @@ jobs: with: repo-root: . # optional, defaults to the current working directory install-dependencies: false # optional, this will almost never need to be set to true - dependency-dict: '{"dev": ("pylint", "ruff"), "tests": ("ruff")}' # optional, but without it nothing will get updated by Poetry + dependency-dict: '{"dev": ["pylint", "ruff"], "tests": ["ruff"]}' # optional, but without it nothing will get updated by Poetry update-pre-commit: true # optional run-pre-commit: true # optional pre-commit-hook-skip-list: 'pylint' # optional, hooks that don't auto-fix things can (and probably should be) skipped diff --git a/actions/update-development-dependencies/requirements.txt b/actions/update-development-dependencies/requirements.txt index 4000a000..36d8f657 100644 --- a/actions/update-development-dependencies/requirements.txt +++ b/actions/update-development-dependencies/requirements.txt @@ -49,7 +49,7 @@ requests==2.32.3 ; python_version >= "3.12" and python_version < "3.13" requests-toolbelt==1.0.0 ; python_version >= "3.12" and python_version < "3.13" ruyaml==0.91.0 ; python_version >= "3.12" and python_version < "3.13" secretstorage==3.3.3 ; python_version >= "3.12" and python_version < "3.13" and sys_platform == "linux" -setuptools==73.0.0 ; python_version >= "3.12" and python_version < "3.13" +setuptools==73.0.1 ; python_version >= "3.12" and python_version < "3.13" shellingham==1.5.4 ; python_version >= "3.12" and python_version < "3.13" soupsieve==2.6 ; python_version >= "3.12" and python_version < "3.13" toml==0.10.2 ; python_version >= "3.12" and python_version < "3.13" diff --git a/contributor_setup.py b/contributor_setup.py new file mode 100644 index 00000000..6b52b7ce --- /dev/null +++ b/contributor_setup.py @@ -0,0 +1,98 @@ +"""Set up an environment to use to contribute to this package. + +This script will run through the commands listed in the CONTRIBUTING.md file. +""" + +from __future__ import annotations + +import glob +import os +import platform +import shlex +import subprocess +import sys + +from pathlib import Path + +RUNNING_ON_LINUX = platform.system().upper() != "WINDOWS" +RUNNING_IN_VIRTUALENV = sys.prefix != sys.base_prefix + + +def create_virtual_environment(virtual_env_dir: str | os.PathLike[str]) -> None: + """Create a virtual environment. + + Args: + virtual_env_dir: The directory where the virtual environment should be created + """ + print(f"\nCreating virtualenv located at '{virtual_env_dir}'") + _run_cmd_in_subprocess(f"{sys.executable} -m venv {virtual_env_dir} --clear") + + +def _run_cmd_in_subprocess(command: str) -> None: + """Run the given command in a subprocess. + + Args: + command: The command string to send. + """ + command = command.replace("\\", "/") + print(f"\nExecuting command: {command}") + subprocess.check_call(shlex.split(command)) # noqa: S603 + + +def main() -> None: + """Set up the environment to allow development. + + Raises: + SystemExit: Indicates that the setup failed for some reason. + """ + starting_dir = Path.cwd() + try: + if RUNNING_IN_VIRTUALENV: + raise IndexError # noqa: TRY301 + if sys.version_info[0:2] != (3, 12): + msg = "Unable to set up the environment. Please use Python 3.12." + raise SystemExit(msg) + # Windows systems require the 64 bit python + if platform.system().lower() == "windows" and sys.maxsize <= 2**32: + msg = "Unable to set up the environment. Please use a 64-bit Python version." + raise SystemExit(msg) + # Create the virtual environment + virtual_env_dir = starting_dir / ".venv" + create_virtual_environment(virtual_env_dir) + os.environ["VIRTUAL_ENV"] = virtual_env_dir.as_posix() + + # Delete the previous poetry lock file + lock_file = Path(starting_dir) / "poetry.lock" + if lock_file.exists(): + lock_file.unlink() + + # Find the python executable from the new virtual environment + files = list( + filter( + lambda x: "site-packages" not in x and "pythonw" not in x, + glob.iglob( # noqa: PTH207 + f"{virtual_env_dir}/{'bin' if RUNNING_ON_LINUX else 'Scripts'}/**/python*", + recursive=True, + ), + ) + ) + python_executable = files[0] + commands_to_send = ( + f"{python_executable} -m pip install -U pip wheel poetry", + f"{python_executable} -m poetry install", + f"{python_executable} -m pre_commit install --install-hooks", + ) + for command in commands_to_send: + _run_cmd_in_subprocess(command) + except IndexError: + msg = ( + "Unable to set up the environment. Please run this script from a " + "standard Python installation, not a virtual environment." + ) + raise SystemExit(msg) # noqa: B904 + finally: + os.chdir(starting_dir) + + +if __name__ == "__main__": + main() diff --git a/pyproject.toml b/pyproject.toml index 38166946..33fb088d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -12,20 +12,31 @@ package-mode = false version = "0.0.0" [tool.poetry.dependencies] +python = "~3.12" + +[tool.poetry.group.cutv.dependencies] # dependencies for actions/create-unique-testpypi-version +poetry-core = "^1.9.0" +pypi-simple = "^1.6.0" +tomli = "^2.0.1" +tomli-w = "^1.0.0" + +[tool.poetry.group.dev.dependencies] pip = "^24.0" poetry-audit-plugin = "^0.4.0" poetry-pre-commit-plugin = "^0.1.2" -pyright = "1.1.376" -python = "~3.12" +pyright = "1.1.377" +python-semantic-release = "^9.8.7" + +[tool.poetry.group.fci.dependencies] # dependencies for actions/find-unreleased-changelog-items +tomli = "^2.0.1" [tool.poetry.group.udd.dependencies] # dependencies for actions/update-development-dependencies -maison = "^1.4.3" # yamlfix is broken with v2.0+ poetry = "^1.8.0" poetry-plugin-export = "^1.7.1" pre-commit = "^3.7" pypi-simple = "^1.6.0" toml-sort = "^0.23.1" -yamlfix = "^1.16.0" +yamlfix = "^1.17.0" [tool.pyright] ignore = [ diff --git a/python_semantic_release_templates/.macros.j2 b/python_semantic_release_templates/.macros.j2 new file mode 100644 index 00000000..5d5fa1dd --- /dev/null +++ b/python_semantic_release_templates/.macros.j2 @@ -0,0 +1,26 @@ +{%- macro populate_variables() %} + {%- set latest_release_dict_entry = (context.history.released.items()|list)[0] %} + {%- set latest_version_number = latest_release_dict_entry[0].as_tag() %} + {%- set latest_version_date = latest_release_dict_entry[1].tagged_date.strftime("%Y-%m-%d") %} + {%- set recently_merged_prs = {} %} + {%- for type_, commits in latest_release_dict_entry[1]["elements"] | dictsort %} + {%- for commit in commits %} + {%- set pr_num = commit.commit.message.rstrip().rsplit("(#", 1)[-1].split(")", 1)[0]|int %} + {%- if pr_num %} + {{- recently_merged_prs.update({commit.commit.message.split("\n")[0].rsplit("(#", 1)[0]|safe: pr_num})|replace("None", "") }} + {%- endif %} + {%- endfor %} + {%- endfor %} + + {%- if recently_merged_prs %} + {%- set merged_prs_text_list = ["\n\n### Merged Pull Requests\n\n"] %} + {%- for message, number in recently_merged_prs.items() %} + {{- merged_prs_text_list.append("- " ~ message ~ "([#" + number|string ~ "](" ~ number|string|pull_request_url ~ "))\n")|replace("None", "") }} + {%- endfor %} + {%- set merged_prs_text = (merged_prs_text_list|join).rstrip() %} + {%- else %} + {%- set merged_prs_text = "" %} + {%- endif %} + {%- set output = (latest_version_number, latest_version_date, merged_prs_text) %} + {{- caller(output) }} +{%- endmacro %} diff --git a/python_semantic_release_templates/.release_notes.md.j2 b/python_semantic_release_templates/.release_notes.md.j2 new file mode 100644 index 00000000..8d0767fe --- /dev/null +++ b/python_semantic_release_templates/.release_notes.md.j2 @@ -0,0 +1,9 @@ +{%- import ".macros.j2" as macros %} + +{%- call(output) macros.populate_variables() %} + {%- filter replace("## Unreleased", "# " + output[0] + " (" + output[1] + ")" + output[2])|replace("##", "#") %} + {%- filter replace("Things to be included in the next release go here.\n\n", "") %} + {%- include ".previous_release_notes_for_template.md" %} + {%- endfilter %} + {%- endfilter %} +{% endcall %} diff --git a/python_semantic_release_templates/CHANGELOG.md.j2 b/python_semantic_release_templates/CHANGELOG.md.j2 new file mode 100644 index 00000000..73bf2c1e --- /dev/null +++ b/python_semantic_release_templates/CHANGELOG.md.j2 @@ -0,0 +1,9 @@ +{%- import ".macros.j2" as macros %} + +{%- call(output) macros.populate_variables() %} + {%- filter replace("## Unreleased", "## Unreleased\n\nThings to be included in the next release go here.\n\n---\n\n## " + output[0] + " (" + output[1] + ")" + output[2]) %} + {%- filter replace("Things to be included in the next release go here.\n\n", "") %} + {%- include ".previous_changelog_for_template.md" %} + {%- endfilter %} + {%- endfilter %} +{% endcall %} diff --git a/workflows/check-api-for-breaking-changes.md b/workflows/check-api-for-breaking-changes.md index 7fe3f964..64afdb21 100644 --- a/workflows/check-api-for-breaking-changes.md +++ b/workflows/check-api-for-breaking-changes.md @@ -10,8 +10,17 @@ compatible version of [`griffe`](https://pypi.org/project/griffe/) to check for 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. +> [!NOTE] +> This workflow uses the following GitHub Actions: +> +> - [actions/checkout](https://github.com/actions/checkout) +> - [actions/setup-python](https://github.com/actions/setup-python) +> - [actions/upload-artifact](https://github.com/actions/upload-artifact) +> +> See the [Workflow file][workflow-file] for the currently used versions of each GitHub Action. + > [!TIP] -> See the [Workflow file](../.github/workflows/_reusable-check-api-for-breaking-changes.yml) for implementation details. +> See the [Workflow file][workflow-file] for implementation details. ## Inputs @@ -34,3 +43,5 @@ jobs: with: package-name: my_package_name # required ``` + +[workflow-file]: ../.github/workflows/_reusable-check-api-for-breaking-changes.yml diff --git a/workflows/codeql-analysis.md b/workflows/codeql-analysis.md index 60063b14..a501bd00 100644 --- a/workflows/codeql-analysis.md +++ b/workflows/codeql-analysis.md @@ -15,8 +15,17 @@ for the complete list of supported languages. > security-events: write > ``` +> [!NOTE] +> This workflow uses the following GitHub Actions: +> +> - [actions/checkout](https://github.com/actions/checkout) +> - [github/codeql-action/init](https://github.com/github/codeql-action) +> - [github/codeql-action/analyze](https://github.com/github/codeql-action) +> +> See the [Workflow file][workflow-file] for the currently used versions of each GitHub Action. + > [!TIP] -> See the [Workflow file](../.github/workflows/_reusable-codeql-analysis.yml) for implementation details. +> See the [Workflow file][workflow-file] for implementation details. ## Inputs @@ -43,3 +52,5 @@ jobs: contents: read security-events: write ``` + +[workflow-file]: ../.github/workflows/_reusable-codeql-analysis.yml diff --git a/workflows/enforce-community-standards.md b/workflows/enforce-community-standards.md index 30882329..86954bd2 100644 --- a/workflows/enforce-community-standards.md +++ b/workflows/enforce-community-standards.md @@ -20,8 +20,17 @@ The full list of files that this workflow checks for is as follows: - `.github/dependabot.@(yml|yaml)` - `.github/workflows/codeql-analysis.@(yml|yaml)` +> [!NOTE] +> This workflow uses the following GitHub Actions: +> +> - [actions/checkout](https://github.com/actions/checkout) +> - [andstor/file-existence-action](https://github.com/andstor/file-existence-action) +> - [re-actors/alls-green](https://github.com/re-actors/alls-green) +> +> See the [Workflow file][workflow-file] for the currently used versions of each GitHub Action. + > [!TIP] -> See the [Workflow file](../.github/workflows/_reusable-enforce-community-standards.yml) for implementation details. +> See the [Workflow file][workflow-file] for implementation details. ## Example @@ -37,3 +46,5 @@ jobs: enforce-community-standards: uses: tektronix/python-package-ci-cd/.github/workflows/_reusable-enforce-community-standards.yml@main # it is recommended to use the latest release tag instead of `main` ``` + +[workflow-file]: ../.github/workflows/_reusable-enforce-community-standards.yml diff --git a/workflows/package-build.md b/workflows/package-build.md index b63a1de5..92550993 100644 --- a/workflows/package-build.md +++ b/workflows/package-build.md @@ -21,8 +21,19 @@ and operating system specified. > For builds on other branches (or builds triggered by Pull Requests), the workflow will cancel > any currently running builds for the same branch (or Pull Request). +> [!NOTE] +> This workflow uses the following GitHub Actions: +> +> - [actions/checkout](https://github.com/actions/checkout) +> - [hynek/build-and-inspect-python-package](https://github.com/hynek/build-and-inspect-python-package) +> - [actions/download-artifact](https://github.com/actions/download-artifact) +> - [actions/setup-python](https://github.com/actions/setup-python) +> - [re-actors/alls-green](https://github.com/re-actors/alls-green) +> +> See the [Workflow file][workflow-file] for the currently used versions of each GitHub Action. + > [!TIP] -> See the [Workflow file](../.github/workflows/_reusable-package-build.yml) for implementation details. +> See the [Workflow file][workflow-file] for implementation details. ## Inputs @@ -58,3 +69,5 @@ jobs: id-token: write attestations: write ``` + +[workflow-file]: ../.github/workflows/_reusable-package-build.yml diff --git a/workflows/package-release.md b/workflows/package-release.md new file mode 100644 index 00000000..b4e17185 --- /dev/null +++ b/workflows/package-release.md @@ -0,0 +1,132 @@ +# package-release.yml + +This workflow will create a new release of the package using the +[`python-semantic-release`](https://python-semantic-release.readthedocs.io/en/latest/) tool. +It will then build the package, upload the package to +[TestPyPI](https://test.pypi.org) and [PyPI](https://pypi.org), +create a new GitHub Release for the project, +and then verify that the package can be installed from +[TestPyPI](https://test.pypi.org) and [PyPI](https://pypi.org). + +This workflow runs an action that will populate two files in the +[`python-semantic-release` templates directory](https://python-semantic-release.readthedocs.io/en/latest/configuration.html#config-changelog-template-dir). +One of those files will contain the contents of the `CHANGELOG.md` file in the +repo prior to creating the new release. The other file will contain the +contents of the `## Unreleased` section of the `CHANGELOG.md` file that +will be used to fill in the GitHub Release Notes. + +> [!IMPORTANT] +> This workflow requires the `CHANGELOG.md` file to be in a format that is based on +> [Keep a Changelog](https://keepachangelog.com) +> (the primary difference is the `## [Unreleased]` section is replaced by an `## Unreleased` section), +> and this project adheres to [Semantic Versioning](https://semver.org). See this repo's +> [CHANGELOG.md](../CHANGELOG.md) for an example of the format to use. +> +> Valid subsections within a version are: +> +> - Added +> - Changed +> - Deprecated +> - Removed +> - Fixed +> - Security + +> [!IMPORTANT] +> When calling this reusable workflow, the permissions must be set as follows: +> +> ```yaml +> permissions: +> contents: write +> id-token: write +> attestations: write +> ``` + +> [!NOTE] +> This workflow uses concurrency to limit the number of builds that can run at the same time +> to a single build. This concurrency is shared across the `'pypi (Reusable Workflows)'` concurrency +> group within the repo that calls this workflow. + +> [!NOTE] +> This workflow uses the following GitHub Actions: +> +> - [actions/checkout](https://github.com/actions/checkout) +> - [tektronix/python-package-ci-cd/actions/find-unreleased-changelog-items](https://github.com/tektronix/python-package-ci-cd) +> - [python-semantic-release/python-semantic-release](https://github.com/python-semantic-release/python-semantic-release) +> - [hynek/build-and-inspect-python-package](https://github.com/hynek/build-and-inspect-python-package) +> - [actions/download-artifact](https://github.com/actions/download-artifact) +> - [pypa/gh-action-pypi-publish](https://github.com/pypa/gh-action-pypi-publish) +> - [python-semantic-release/upload-to-gh-release](https://github.com/python-semantic-release/upload-to-gh-release) +> - [actions/setup-python](https://github.com/actions/setup-python) +> - [nick-fields/retry](https://github.com/nick-fields/retry) +> +> See the [Workflow file][workflow-file] for the currently used versions of each GitHub Action. + +> [!TIP] +> See the [Workflow file][workflow-file] for implementation details. + +## Inputs + +| Input variable | Necessity | Description | Default | +| ---------------------------------- | --------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------- | +| `package-name` | required | The name of the package to build, upload, and install. | | +| `repo-name` | required | The full name of the repository to use to gate uploads, in the format `owner/repo`. | | +| `commit-user-name` | required | The name of the user to use when committing changes to the repository. | | +| `commit-user-email` | required | The email of the user to use when committing changes to the repository. | | +| `release-level` | required | The level of the release to create. Must be one of `major`, `minor`, or `patch`. | | +| `build-and-publish-python-package` | optional | A boolean value that determines whether to build and publish the Python package. If set to `false`, the package binaries will not be built or published to PyPI, TestPyPI, or GitHub Releases. | `true` | +| `python-versions-array` | optional | A valid JSON array of Python versions to test against. | | +| `operating-systems-array` | optional | A valid JSON array of operating system names to run tests on. | `'["ubuntu", "windows", "macos"]'` | +| `previous-changelog-filename` | optional | The name of the file to copy the contents of the changelog into for use in the `python-semantic-release` templates. This file will be created inside of the directory defined by the `[tool.semantic_release.changelog.template_dir]` key in the `pyproject.toml` file. | `'.previous_changelog_for_template.md'` | +| `previous-release-notes-filename` | optional | The name of the file to copy the contents of the `## Unreleased` section of the changelog into for use in the GitHub Release Notes. This file will be created inside of the directory defined by the `[tool.semantic_release.changelog.template_dir]` key in the `pyproject.toml` file. | `'.previous_release_notes_for_template.md'` | + +## Secrets + +| Secret variable | Necessity | Description | +| ------------------------- | --------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `checkout-token` | required | The token to use for checking out the repository, must have permissions to write back to the repository. | +| `ssh-signing-key-private` | required | A private SSH key associated with the account that owns the `checkout-token` that will be used to sign the commit and tag created by `python-semantic-release`. | +| `ssh-signing-key-public` | required | The public SSH key linked to the `secrets.ssh-signing-key-private` key that will be used to sign the commit and tag created by `python-semantic-release`. | + +## Example + +```yaml +name: Package Release +on: + workflow_dispatch: + inputs: + release-level: + type: choice + required: true + description: | + Select the release level: + patch for backward compatible minor changes and bug fixes, + minor for backward compatible larger changes, + major for non-backward compatible changes. + options: [patch, minor, major] +concurrency: # This concurrency is not required, but can be added if extra control of concurrent builds is required + group: pypi +jobs: + package-release: + uses: tektronix/python-package-ci-cd/.github/workflows/_reusable-package-release.yml@main # it is recommended to use the latest release tag instead of `main` + with: + package-name: my-package # required + repo-name: owner/my-package # required + commit-user-name: 'User Name' + commit-user-email: 'user-email' + release-level: ${{ inputs.release-level }} # required + build-and-publish-python-package: true # optional + python-versions-array: '["3.9", "3.10", "3.11", "3.12"]' # optional + operating-systems-array: '["ubuntu", "windows", "macos"]' # optional + previous-changelog-filename: '.previous_changelog_for_template.md' # optional + previous-release-notes-filename: '.previous_release_notes_for_template.md' # optional + permissions: + contents: write + id-token: write + attestations: write + secrets: + checkout-token: ${{ secrets.CHECKOUT_TOKEN }} + ssh-signing-key-private: ${{ secrets.SSH_SIGNING_KEY_PRIVATE }} + ssh-signing-key-public: ${{ secrets.SSH_SIGNING_KEY_PUBLIC }} +``` + +[workflow-file]: ../.github/workflows/_reusable-package-release.yml diff --git a/workflows/package-testpypi.md b/workflows/package-testpypi.md new file mode 100644 index 00000000..9d13e0da --- /dev/null +++ b/workflows/package-testpypi.md @@ -0,0 +1,71 @@ +# package-testpypi.yml + +This workflow will build the package using the +[`hynek/build-and-inspect-python-package`](https://github.com/hynek/build-and-inspect-python-package) +action, upload the package to [TestPyPI](https://test.pypi.org), and then verify that the package +can be installed from [TestPyPI](https://test.pypi.org). + +In order to ensure each version uploaded to [TestPyPI](https://test.pypi.org) is unique, the +workflow will first create a unique `.postN` version number for the package on top of the +officially released version of the package, incrementing `N` each time the workflow runs. + +> [!IMPORTANT] +> When calling this reusable workflow, the permissions must be set as follows: +> +> ```yaml +> permissions: +> contents: read +> id-token: write +> attestations: write +> ``` + +> [!NOTE] +> This workflow uses concurrency to limit the number of builds that can run at the same time +> to a single build. This concurrency is shared across the `'pypi (Reusable Workflows)'` concurrency +> group within the repo that calls this workflow. + +> [!NOTE] +> This workflow uses the following GitHub Actions: +> +> - [actions/checkout](https://github.com/actions/checkout) +> - [tektronix/python-package-ci-cd/actions/create-unique-testpypi-version](https://github.com/tektronix/python-package-ci-cd) +> - [hynek/build-and-inspect-python-package](https://github.com/hynek/build-and-inspect-python-package) +> - [actions/download-artifact](https://github.com/actions/download-artifact) +> - [pypa/gh-action-pypi-publish](https://github.com/pypa/gh-action-pypi-publish) +> - [actions/setup-python](https://github.com/actions/setup-python) +> - [nick-fields/retry](https://github.com/nick-fields/retry) +> +> See the [Workflow file][workflow-file] for the currently used versions of each GitHub Action. + +> [!TIP] +> See the [Workflow file][workflow-file] for implementation details. + +## Inputs + +| Input variable | Necessity | Description | Default | +| -------------- | --------- | ----------------------------------------------------------------------------------- | ------- | +| `package-name` | required | The name of the package to build, upload, and install. | | +| `repo-name` | required | The full name of the repository to use to gate uploads, in the format `owner/repo`. | | + +## Example + +```yaml +name: Publish to TestPyPI +on: + push: + branches: [main] +concurrency: # This concurrency is not required, but can be added if extra control of concurrent builds is required + group: pypi +jobs: + package-testpypi: + uses: tektronix/python-package-ci-cd/.github/workflows/_reusable-package-testpypi.yml@main # it is recommended to use the latest release tag instead of `main` + with: + package-name: my-package # required + repo-name: owner/my-package # required + permissions: + contents: read + id-token: write + attestations: write +``` + +[workflow-file]: ../.github/workflows/_reusable-package-testpypi.yml diff --git a/workflows/publish-api-comparison.md b/workflows/publish-api-comparison.md index 332effa5..25e86a7d 100644 --- a/workflows/publish-api-comparison.md +++ b/workflows/publish-api-comparison.md @@ -23,8 +23,17 @@ changes, usually a Workflow named `Check Public API for Breaking Changes`, see t > pull-requests: write > ``` +> [!NOTE] +> This workflow uses the following GitHub Actions: +> +> - [dawidd6/action-download-artifact](https://github.com/dawidd6/action-download-artifact) +> - [8BitJonny/gh-get-current-pr](https://github.com/8BitJonny/gh-get-current-pr) +> - [marocchino/sticky-pull-request-comment](https://github.com/marocchino/sticky-pull-request-comment) +> +> See the [Workflow file][workflow-file] for the currently used versions of each GitHub Action. + > [!TIP] -> See the [Workflow file](../.github/workflows/_reusable-publish-api-comparison.yml) for implementation details. +> See the [Workflow file][workflow-file] for implementation details. ## Example @@ -41,3 +50,5 @@ jobs: checks: write pull-requests: write ``` + +[workflow-file]: ../.github/workflows/_reusable-publish-api-comparison.yml diff --git a/workflows/publish-test-results.md b/workflows/publish-test-results.md index a808cbca..154c89b3 100644 --- a/workflows/publish-test-results.md +++ b/workflows/publish-test-results.md @@ -22,8 +22,17 @@ Workflow named `Test code`, see the [example](#example) below for the correct ya > pull-requests: write > ``` +> [!NOTE] +> This workflow uses the following GitHub Actions: +> +> - [dawidd6/action-download-artifact](https://github.com/dawidd6/action-download-artifact) +> - [8BitJonny/gh-get-current-pr](https://github.com/8BitJonny/gh-get-current-pr) +> - [marocchino/sticky-pull-request-comment](https://github.com/marocchino/sticky-pull-request-comment) +> +> See the [Workflow file][workflow-file] for the currently used versions of each GitHub Action. + > [!TIP] -> See the [Workflow file](../.github/workflows/_reusable-publish-test-results.yml) for implementation details. +> See the [Workflow file][workflow-file] for implementation details. ## Inputs @@ -53,3 +62,5 @@ jobs: checks: write pull-requests: write ``` + +[workflow-file]: ../.github/workflows/_reusable-publish-test-results.yml diff --git a/workflows/sbom-scan.md b/workflows/sbom-scan.md index afaf5e6d..26cff3fb 100644 --- a/workflows/sbom-scan.md +++ b/workflows/sbom-scan.md @@ -22,8 +22,21 @@ repository's Python package. > attestations: write > ``` +> [!NOTE] +> This workflow uses the following GitHub Actions: +> +> - [actions/checkout](https://github.com/actions/checkout) +> - [actions/setup-python](https://github.com/actions/setup-python) +> - [anchore/sbom-action](https://github.com/anchore/sbom-action) +> - [actions/attest-build-provenance](https://github.com/actions/attest-build-provenance) +> - [anchore/scan-action](https://github.com/anchore/scan-action) +> - [actions/upload-artifact](https://github.com/actions/upload-artifact) +> - [github/codeql-action/upload-sarif](https://github.com/github/codeql-action) +> +> See the [Workflow file][workflow-file] for the currently used versions of each GitHub Action. + > [!TIP] -> See the [Workflow file](../.github/workflows/_reusable-sbom-scan.yml) for implementation details. +> See the [Workflow file][workflow-file] for implementation details. ## Example @@ -45,3 +58,5 @@ jobs: id-token: write attestations: write ``` + +[workflow-file]: ../.github/workflows/_reusable-sbom-scan.yml diff --git a/workflows/test-code.md b/workflows/test-code.md index d54c40e9..a0d735e5 100644 --- a/workflows/test-code.md +++ b/workflows/test-code.md @@ -87,8 +87,22 @@ commands_pre = > For builds on other branches (or builds triggered by Pull Requests), the workflow will cancel > any currently running builds for the same branch (or Pull Request). +> [!NOTE] +> This workflow uses the following GitHub Actions: +> +> - [actions/checkout](https://github.com/actions/checkout) +> - [actions/setup-node](https://github.com/actions/setup-node) +> - [actions/setup-python](https://github.com/actions/setup-python) +> - [actions/upload-artifact](https://github.com/actions/upload-artifact) +> - [codecov/codecov-action](https://github.com/codecov/codecov-action) +> - [actions/download-artifact](https://github.com/actions/download-artifact) +> - [phoenix-actions/test-reporting](https://github.com/phoenix-actions/test-reporting) +> - [re-actors/alls-green](https://github.com/re-actors/alls-green) +> +> See the [Workflow file][workflow-file] for the currently used versions of each GitHub Action. + > [!TIP] -> See the [Workflow file](../.github/workflows/_reusable-test-code.yml) for implementation details. +> See the [Workflow file][workflow-file] for implementation details. ## Inputs @@ -123,9 +137,11 @@ jobs: uses: tektronix/python-package-ci-cd/.github/workflows/_reusable-test-code.yml@main # it is recommended to use the latest release tag instead of `main` with: repo-name: owner/repo # required - operating-systems-array: '["ubuntu", "windows", "macos"]' # required + operating-systems-array: '["ubuntu", "windows", "macos"]' # optional python-versions-array: '["3.9", "3.10", "3.11", "3.12"]' # required upload-to-codecov: true # optional secrets: codecov-token: ${{ secrets.CODECOV_TOKEN }} # optional ``` + +[workflow-file]: ../.github/workflows/_reusable-test-code.yml diff --git a/workflows/test-docs.md b/workflows/test-docs.md index 8210e270..001873eb 100644 --- a/workflows/test-docs.md +++ b/workflows/test-docs.md @@ -51,8 +51,20 @@ commands = > For builds on other branches (or builds triggered by Pull Requests), the workflow will cancel > any currently running builds for the same branch (or Pull Request). +> [!NOTE] +> This workflow uses the following GitHub Actions: +> +> - [actions/checkout](https://github.com/actions/checkout) +> - [actions/setup-node](https://github.com/actions/setup-node) +> - [actions/setup-python](https://github.com/actions/setup-python) +> - [thedoctor0/zip-release](https://github.com/thedoctor0/zip-release) +> - [actions/upload-artifact](https://github.com/actions/upload-artifact) +> - [re-actors/alls-green](https://github.com/re-actors/alls-green) +> +> See the [Workflow file][workflow-file] for the currently used versions of each GitHub Action. + > [!TIP] -> See the [Workflow file](../.github/workflows/_reusable-test-docs.yml) for implementation details. +> See the [Workflow file][workflow-file] for implementation details. ## Inputs @@ -83,3 +95,5 @@ jobs: python-version: '3.11' # required tox-env-array: '["docs", "doctests"]' # optional ``` + +[workflow-file]: ../.github/workflows/_reusable-test-docs.yml diff --git a/workflows/update-python-and-pre-commit-dependencies.md b/workflows/update-python-and-pre-commit-dependencies.md index a7b94b51..42364d1b 100644 --- a/workflows/update-python-and-pre-commit-dependencies.md +++ b/workflows/update-python-and-pre-commit-dependencies.md @@ -14,20 +14,30 @@ updates for the Python dependencies. > contents: write > ``` +> [!NOTE] +> This workflow uses the following GitHub Actions: +> +> - [actions/checkout](https://github.com/actions/checkout) +> - [crazy-max/ghaction-import-gpg](https://github.com/crazy-max/ghaction-import-gpg) +> - [tektronix/python-package-ci-cd/actions/update-development-dependencies](https://github.com/tektronix/python-package-ci-cd) +> - [stefanzweifel/git-auto-commit-action](https://github.com/stefanzweifel/git-auto-commit-action) +> +> See the [Workflow file][workflow-file] for the currently used versions of each GitHub Action. + > [!TIP] -> See the [Workflow file](../.github/workflows/_reusable-update-python-and-pre-commit-dependencies.yml) for implementation details. +> See the [Workflow file][workflow-file] for implementation details. ## Inputs -| Input variable | Necessity | Description | Default | -| --------------------------- | --------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------- | -| `commit-user-name` | required | The name of the user to use when committing changes to the repository. | | -| `commit-user-email` | required | The email of the user to use when committing changes to the repository. | | -| `dependency-dict` | optional | Specify a valid dictionary of dependency groups to update, where each key is a dependency group name, and each value is a tuple of dependencies to update within that group, e.g. `{"dev": ("pylint", "ruff"), "tests": ("ruff")}`. | `{}` | -| `update-pre-commit` | optional | A boolean indicating if the pre-commit hooks should be updated. | `false` | -| `run-pre-commit` | optional | A boolean indicating to run the pre-commit hooks to perform auto-fixing after updating the dependencies. Setting this input to `true` will also set the update-pre-commit input to `true`. | `false` | -| `pre-commit-hook-skip-list` | optional | A comma-separated list of pre-commit hooks to skip (only applicable when `run-pre-commit=true`). | `""` | -| `export-dependency-groups` | optional | A comma-separated list of dependency groups to export to a `requirements.txt` file. The format is `group1,group2:custom-path/to/test/folder`. | `""` | +| Input variable | Necessity | Description | Default | +| --------------------------- | --------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ------- | +| `commit-user-name` | required | The name of the user to use when committing changes to the repository. | | +| `commit-user-email` | required | The email of the user to use when committing changes to the repository. | | +| `dependency-dict` | optional | Specify a valid JSON dictionary of dependency groups to update, where each key is a dependency group name, and each value is a list of dependencies to update within that group, e.g. `'{"dev": ["pylint", "ruff"], "tests": ["ruff"]}'`. Use an empty string, e.g. `""`, for dependencies located in the default group' | `{}` | +| `update-pre-commit` | optional | A boolean indicating if the pre-commit hooks should be updated. | `false` | +| `run-pre-commit` | optional | A boolean indicating to run the pre-commit hooks to perform auto-fixing after updating the dependencies. Setting this input to `true` will also set the update-pre-commit input to `true`. | `false` | +| `pre-commit-hook-skip-list` | optional | A comma-separated list of pre-commit hooks to skip (only applicable when `run-pre-commit=true`). | `""` | +| `export-dependency-groups` | optional | A comma-separated list of dependency groups to export to a `requirements.txt` file. The format is `group1,group2:custom-path/to/test/folder`. | `""` | ## Secrets @@ -62,3 +72,5 @@ jobs: gpg-signing-key-private: ${{ secrets.GPG_SIGNING_KEY_PRIVATE }} gpg-signing-key-passphrase: ${{ secrets.GPG_SIGNING_KEY_PASSPHRASE }} ``` + +[workflow-file]: ../.github/workflows/_reusable-update-python-and-pre-commit-dependencies.yml