diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 5e045630e..1d7930dfc 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -16,16 +16,18 @@ concurrency: jobs: - commitlint: + lint-commits: # condition: Execute IFF it is protected branch update, or a PR that is NOT in a draft state if: ${{ github.event_name != 'pull_request' || !github.event.pull_request.draft }} runs-on: ubuntu-latest steps: - - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 + - name: Setup | Checkout Repository + uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 with: fetch-depth: 0 - - uses: wagoid/commitlint-github-action@b948419dd99f3fd78a6548d48f94e3df7f6bf3ed # v6.2.1 + - name: Lint | Commit Messages + uses: wagoid/commitlint-github-action@b948419dd99f3fd78a6548d48f94e3df7f6bf3ed # v6.2.1 eval-changes: @@ -35,19 +37,20 @@ jobs: runs-on: ubuntu-latest steps: - - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 + - name: Setup | Checkout Repository + uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 with: - fetch-depth: 100 + fetch-depth: 0 - name: Evaluate | Check common file types for changes id: core-changed-files - uses: tj-actions/changed-files@24d32ffd492484c1d75e0c0b894501ddb9d30d62 #v47.0.0 + uses: tj-actions/changed-files@e0021407031f5be11a464abee9a0776171c79891 #v47.0.1 with: files_yaml_from_source_file: .github/changed-files-spec.yml - name: Evaluate | Check specific file types for changes id: ci-changed-files - uses: tj-actions/changed-files@24d32ffd492484c1d75e0c0b894501ddb9d30d62 #v47.0.0 + uses: tj-actions/changed-files@e0021407031f5be11a464abee9a0776171c79891 #v47.0.1 with: files_yaml: | ci: diff --git a/.github/workflows/cicd.yml b/.github/workflows/cicd.yml index c7d1033c5..b1696d9b6 100644 --- a/.github/workflows/cicd.yml +++ b/.github/workflows/cicd.yml @@ -19,21 +19,21 @@ jobs: runs-on: ubuntu-latest steps: - - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 + - name: Setup | Checkout Repository + uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 with: - fetch-depth: 100 # Must at least retrieve a set of commits to compare changes - # primarily because of any 'Rebase and Merge' PR action in GitHub + fetch-depth: 0 - name: Evaluate | Check common file types for changes id: core-changed-files - uses: tj-actions/changed-files@24d32ffd492484c1d75e0c0b894501ddb9d30d62 #v47.0.0 + uses: tj-actions/changed-files@e0021407031f5be11a464abee9a0776171c79891 #v47.0.1 with: base_sha: ${{ github.event.push.before }} files_yaml_from_source_file: .github/changed-files-spec.yml - name: Evaluate | Check specific file types for changes id: ci-changed-files - uses: tj-actions/changed-files@24d32ffd492484c1d75e0c0b894501ddb9d30d62 #v47.0.0 + uses: tj-actions/changed-files@e0021407031f5be11a464abee9a0776171c79891 #v47.0.1 with: base_sha: ${{ github.event.push.before }} files_yaml: | @@ -115,7 +115,7 @@ jobs: # possible that the branch was updated while the workflow was running. This # prevents accidentally releasing un-evaluated changes. - name: Setup | Checkout Repository on Release Branch - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 + uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 with: ref: ${{ github.ref_name }} fetch-depth: 0 @@ -125,7 +125,7 @@ jobs: git reset --hard ${{ github.sha }} - name: Setup | Download Build Artifacts - uses: actions/download-artifact@018cc2cf5baa6db3ef3c5f8a56943fffe632ef53 # v6.0.0 + uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7.0.0 id: artifact-download with: name: ${{ needs.validate.outputs.distribution-artifacts }} @@ -140,49 +140,21 @@ jobs: python -m scripts.bump_version_in_docs git add docs/* - - name: Evaluate | Verify upstream has NOT changed - # Last chance to abort before causing an error as another PR/push was applied to the upstream branch - # while this workflow was running. This is important because we are committing a version change - shell: bash - run: bash .github/workflows/verify_upstream.sh - - name: Release | Python Semantic Release id: release - uses: python-semantic-release/python-semantic-release@4d4cb0ab842247caea1963132c242c62aab1e4d5 # v10.4.1 + uses: python-semantic-release/python-semantic-release@02f2a5c74dbb6aa2989f10fc4af12cd8e6bf025f # v10.5.2 with: github_token: ${{ secrets.GITHUB_TOKEN }} verbosity: 1 build: false - name: Release | Add distribution artifacts to GitHub Release Assets - uses: python-semantic-release/publish-action@ae6462adc12bd3d1738070d784b65b5189b955a9 # v10.4.1 + uses: python-semantic-release/publish-action@948bb8fccc5e8072f2c52464b45c76a8bb3878e6 # v10.5.2 if: steps.release.outputs.released == 'true' with: github_token: ${{ secrets.GITHUB_TOKEN }} tag: ${{ steps.release.outputs.tag }} - - name: Release | Update Minor Release Tag Reference - if: steps.release.outputs.released == 'true' && steps.release.outputs.is_prerelease == 'false' - env: - FULL_VERSION_TAG: ${{ steps.release.outputs.tag }} - GIT_COMMITTER_NAME: ${{ env.GITHUB_ACTIONS_AUTHOR_NAME }} - GIT_COMMITTER_EMAIL: ${{ env.GITHUB_ACTIONS_AUTHOR_EMAIL }} - run: | - MINOR_VERSION_TAG="$(echo "$FULL_VERSION_TAG" | cut -d. -f1,2)" - git tag --force --annotate "$MINOR_VERSION_TAG" "${FULL_VERSION_TAG}^{}" -m "$MINOR_VERSION_TAG" - git push -u origin "$MINOR_VERSION_TAG" --force - - - name: Release | Update Major Release Tag Reference - if: steps.release.outputs.released == 'true' && steps.release.outputs.is_prerelease == 'false' - env: - FULL_VERSION_TAG: ${{ steps.release.outputs.tag }} - GIT_COMMITTER_NAME: ${{ env.GITHUB_ACTIONS_AUTHOR_NAME }} - GIT_COMMITTER_EMAIL: ${{ env.GITHUB_ACTIONS_AUTHOR_EMAIL }} - run: | - MAJOR_VERSION_TAG="$(echo "$FULL_VERSION_TAG" | cut -d. -f1)" - git tag --force --annotate "$MAJOR_VERSION_TAG" "${FULL_VERSION_TAG}^{}" -m "$MAJOR_VERSION_TAG" - git push -u origin "$MAJOR_VERSION_TAG" --force - outputs: released: ${{ steps.release.outputs.released || 'false' }} new-release-version: ${{ steps.release.outputs.version }} @@ -207,7 +179,7 @@ jobs: steps: - name: Setup | Download Build Artifacts - uses: actions/download-artifact@018cc2cf5baa6db3ef3c5f8a56943fffe632ef53 # v6.0.0 + uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7.0.0 id: artifact-download with: name: ${{ needs.validate.outputs.distribution-artifacts }} diff --git a/.github/workflows/manual.yml b/.github/workflows/manual.yml index 4f9e277c4..67d256fea 100644 --- a/.github/workflows/manual.yml +++ b/.github/workflows/manual.yml @@ -65,7 +65,7 @@ jobs: steps: - name: Setup | Install Python ${{ env.COMMON_PYTHON_VERSION }} - uses: actions/setup-python@e797f83bcb11b83ae66e0230d6156d7c80228e7c # v6.0.0 + uses: actions/setup-python@83679a892e2d95755f2dac6acb0bfd1e9ac5d548 # v6.1.0 with: python-version: ${{ env.COMMON_PYTHON_VERSION }} diff --git a/.github/workflows/stale.yml b/.github/workflows/stale.yml index 69f838a67..ba5c00d1c 100644 --- a/.github/workflows/stale.yml +++ b/.github/workflows/stale.yml @@ -28,7 +28,7 @@ jobs: steps: - name: Stale Issues/PRs - uses: actions/stale@5f858e3efba33a5ca4407a664cc011ad407f2008 # v10.1.0 + uses: actions/stale@997185467fa4f803885201cee163a9f38240193d # v10.1.1 with: # default: 30, GitHub Actions API Rate limit is 1000/hr operations-per-run: ${{ env.OPERATIONS_RATE_LIMIT }} @@ -67,7 +67,7 @@ jobs: # that point the submitter has 14 days before a reminder/warning is given. If # no response has been received within 3 weeks, the issue is closed. There are # no exemptions besides removing the awaiting-reply label. - uses: actions/stale@5f858e3efba33a5ca4407a664cc011ad407f2008 # v10.1.0 + uses: actions/stale@997185467fa4f803885201cee163a9f38240193d # v10.1.1 with: # GitHub Actions API Rate limit is 1000/hr operations-per-run: ${{ env.OPERATIONS_RATE_LIMIT }} @@ -97,7 +97,7 @@ jobs: # forgotten completely, this job will post a reminder message to the maintainers # No closures will occur and there are no exemptions besides removing the confirmed # label. - uses: actions/stale@5f858e3efba33a5ca4407a664cc011ad407f2008 # v10.1.0 + uses: actions/stale@997185467fa4f803885201cee163a9f38240193d # v10.1.1 with: # GitHub Actions API Rate limit is 1000/hr operations-per-run: ${{ env.OPERATIONS_RATE_LIMIT }} diff --git a/.github/workflows/validate.yml b/.github/workflows/validate.yml index 6f447d461..0e1e7654c 100644 --- a/.github/workflows/validate.yml +++ b/.github/workflows/validate.yml @@ -86,11 +86,11 @@ jobs: build: name: Build runs-on: ubuntu-latest - if: ${{ inputs.build-files-changed == 'true' || inputs.src-files-changed == 'true' || inputs.test-files-changed == 'true' || inputs.ci-files-changed == 'true' }} + if: ${{ inputs.build-files-changed == 'true' || inputs.src-files-changed == 'true' || inputs.test-files-changed == 'true' || inputs.ci-files-changed == 'true' || inputs.gha-src-files-changed == 'true' || inputs.gha-test-files-changed == 'true' }} steps: - name: Setup | Checkout Repository at workflow sha - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 + uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 with: ref: ${{ github.sha }} fetch-depth: 0 @@ -100,7 +100,7 @@ jobs: git checkout -B ${{ github.ref_name }} - name: Setup | Install Python ${{ env.COMMON_PYTHON_VERSION }} - uses: actions/setup-python@e797f83bcb11b83ae66e0230d6156d7c80228e7c # v6.0.0 + uses: actions/setup-python@83679a892e2d95755f2dac6acb0bfd1e9ac5d548 # v6.1.0 with: python-version: ${{ env.COMMON_PYTHON_VERSION }} cache: 'pip' @@ -112,7 +112,7 @@ jobs: - name: Build | Build next version artifacts id: version - uses: python-semantic-release/python-semantic-release@4d4cb0ab842247caea1963132c242c62aab1e4d5 # v10.4.1 + uses: python-semantic-release/python-semantic-release@02f2a5c74dbb6aa2989f10fc4af12cd8e6bf025f # v10.5.2 with: github_token: "" verbosity: 1 @@ -139,7 +139,7 @@ jobs: printf '%s\n' "artifacts_name=dist" >> $GITHUB_OUTPUT - name: Upload | Distribution Artifacts - uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0 + uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0 with: name: ${{ steps.build.outputs.artifacts_name }} path: ${{ steps.build.outputs.dist_dir }} @@ -161,13 +161,13 @@ jobs: steps: - name: Setup | Checkout Repository - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 + uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 with: ref: ${{ github.sha }} fetch-depth: 1 - name: Setup | Install Python ${{ env.LOWEST_PYTHON_VERSION }} - uses: actions/setup-python@e797f83bcb11b83ae66e0230d6156d7c80228e7c # v6.0.0 + uses: actions/setup-python@83679a892e2d95755f2dac6acb0bfd1e9ac5d548 # v6.1.0 with: python-version: ${{ env.LOWEST_PYTHON_VERSION }} cache: 'pip' @@ -217,19 +217,19 @@ jobs: steps: - name: Setup | Checkout Repository - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 + uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 with: ref: ${{ github.sha }} fetch-depth: 1 - name: Setup | Install Python ${{ matrix.python-version }} - uses: actions/setup-python@e797f83bcb11b83ae66e0230d6156d7c80228e7c # v6.0.0 + uses: actions/setup-python@83679a892e2d95755f2dac6acb0bfd1e9ac5d548 # v6.1.0 with: python-version: ${{ matrix.python-version }} cache: 'pip' - name: Setup | Download Distribution Artifacts - uses: actions/download-artifact@018cc2cf5baa6db3ef3c5f8a56943fffe632ef53 # v6.0.0 + uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7.0.0 with: name: ${{ needs.build.outputs.distribution-artifacts }} path: ./dist @@ -265,7 +265,7 @@ jobs: --junit-xml=tests/reports/pytest-results.xml - name: Report | Upload Cached Repos on Failure - uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0 + uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0 if: ${{ failure() && steps.tests.outcome == 'failure' }} with: name: ${{ format('cached-repos-{0}-{1}', matrix.os, matrix.python-version) }} @@ -275,7 +275,7 @@ jobs: retention-days: 1 - name: Report | Upload Tested Repos on Failure - uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0 + uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0 if: ${{ failure() && steps.tests.outcome == 'failure' }} with: name: ${{ format('tested-repos-{0}-{1}', matrix.os, matrix.python-version) }} @@ -306,19 +306,19 @@ jobs: steps: - name: Setup | Checkout Repository - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 + uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 with: ref: ${{ github.sha }} fetch-depth: 1 - name: Setup | Install Python ${{ matrix.python-version }} - uses: actions/setup-python@e797f83bcb11b83ae66e0230d6156d7c80228e7c # v6.0.0 + uses: actions/setup-python@83679a892e2d95755f2dac6acb0bfd1e9ac5d548 # v6.1.0 with: python-version: ${{ matrix.python-version }} cache: 'pip' - name: Setup | Download Distribution Artifacts - uses: actions/download-artifact@018cc2cf5baa6db3ef3c5f8a56943fffe632ef53 # v6.0.0 + uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7.0.0 with: name: ${{ needs.build.outputs.distribution-artifacts }} path: dist @@ -363,7 +363,7 @@ jobs: `--junit-xml=tests/reports/pytest-results.xml - name: Report | Upload Cached Repos on Failure - uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0 + uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0 if: ${{ failure() && steps.tests.outcome == 'failure' }} with: name: ${{ format('cached-repos-{0}-{1}', matrix.os, matrix.python-version) }} @@ -373,7 +373,7 @@ jobs: retention-days: 1 - name: Report | Upload Tested Repos on Failure - uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0 + uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0 if: ${{ failure() && steps.tests.outcome == 'failure' }} with: name: ${{ format('tested-repos-{0}-{1}', matrix.os, matrix.python-version) }} @@ -397,7 +397,6 @@ jobs: needs: - build - - unit-test env: TEST_CONTAINER_TAG: psr-action:latest @@ -405,13 +404,13 @@ jobs: steps: - name: Setup | Checkout Repository - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 + uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 with: fetch-depth: 1 ref: ${{ github.sha }} - name: Setup | Download Distribution Artifacts - uses: actions/download-artifact@018cc2cf5baa6db3ef3c5f8a56943fffe632ef53 # v6.0.0 + uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7.0.0 with: name: ${{ needs.build.outputs.distribution-artifacts }} path: ${{ env.ACTION_SRC_DIR }} @@ -447,13 +446,13 @@ jobs: steps: - name: Setup | Checkout Repository - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 + uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 with: ref: ${{ github.sha }} fetch-depth: 1 - name: Setup | Install Python ${{ env.COMMON_PYTHON_VERSION }} - uses: actions/setup-python@e797f83bcb11b83ae66e0230d6156d7c80228e7c # v6.0.0 + uses: actions/setup-python@83679a892e2d95755f2dac6acb0bfd1e9ac5d548 # v6.1.0 with: python-version: ${{ env.COMMON_PYTHON_VERSION }} cache: 'pip' diff --git a/.github/workflows/verify_upstream.sh b/.github/workflows/verify_upstream.sh deleted file mode 100644 index 8444eba57..000000000 --- a/.github/workflows/verify_upstream.sh +++ /dev/null @@ -1,33 +0,0 @@ -#!/bin/bash - -set -eu +o pipefail - -# Example output of `git status -sb`: -# ## master...origin/master [behind 1] -# M .github/workflows/verify_upstream.sh -UPSTREAM_BRANCH_NAME="$(git status -sb | head -n 1 | awk -F '\\.\\.\\.' '{print $2}' | cut -d ' ' -f1)" -printf '%s\n' "Upstream branch name: $UPSTREAM_BRANCH_NAME" - -set -o pipefail - -if [ -z "$UPSTREAM_BRANCH_NAME" ]; then - printf >&2 '%s\n' "::error::Unable to determine upstream branch name!" - exit 1 -fi - -git fetch "${UPSTREAM_BRANCH_NAME%%/*}" - -if ! UPSTREAM_SHA="$(git rev-parse "$UPSTREAM_BRANCH_NAME")"; then - printf >&2 '%s\n' "::error::Unable to determine upstream branch sha!" - exit 1 -fi - -HEAD_SHA="$(git rev-parse HEAD)" - -if [ "$HEAD_SHA" != "$UPSTREAM_SHA" ]; then - printf >&2 '%s\n' "[HEAD SHA] $HEAD_SHA != $UPSTREAM_SHA [UPSTREAM SHA]" - printf >&2 '%s\n' "::error::Upstream has changed, aborting release..." - exit 1 -fi - -printf '%s\n' "Verified upstream branch has not changed, continuing with release..." diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 98a3fcd5e..ff44a7ec7 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -4,6 +4,34 @@ CHANGELOG ========= +.. _changelog-v10.5.3: + +v10.5.3 (2025-12-14) +==================== + +🪲 Bug Fixes +------------ + +* **cmd-version**: Resolve unauthenticated git repo issues for upstream verification, closes + `#1373`_ (`PR#1388`_, `e164f68`_) + +* **github-action**: Fix failed signing issue when ssh was missing from action environment, closes + `#1376`_ (`PR#1389`_, `18b7eda`_) + +* **parser-conventional-monorepo**: Fix parser opts validator for outside dir path matches, closes + `#1380`_ (`PR#1382`_, `a51eadd`_) + +.. _#1373: https://github.com/python-semantic-release/python-semantic-release/issues/1373 +.. _#1376: https://github.com/python-semantic-release/python-semantic-release/issues/1376 +.. _#1380: https://github.com/python-semantic-release/python-semantic-release/issues/1380 +.. _18b7eda: https://github.com/python-semantic-release/python-semantic-release/commit/18b7edadd7e7dfe42ec43110acf5e1bd8bcd7eb3 +.. _a51eadd: https://github.com/python-semantic-release/python-semantic-release/commit/a51eadd8414a7e9cbfa66837ee5a840a6331dfa1 +.. _e164f68: https://github.com/python-semantic-release/python-semantic-release/commit/e164f682bfa4ca1e7cbe77aa068202fd8094eec7 +.. _PR#1382: https://github.com/python-semantic-release/python-semantic-release/pull/1382 +.. _PR#1388: https://github.com/python-semantic-release/python-semantic-release/pull/1388 +.. _PR#1389: https://github.com/python-semantic-release/python-semantic-release/pull/1389 + + .. _changelog-v10.5.2: v10.5.2 (2025-11-10) diff --git a/docs/configuration/automatic-releases/github-actions.rst b/docs/configuration/automatic-releases/github-actions.rst index d1469ffb3..683cf1bfc 100644 --- a/docs/configuration/automatic-releases/github-actions.rst +++ b/docs/configuration/automatic-releases/github-actions.rst @@ -893,14 +893,14 @@ to the GitHub Release Assets as well. - name: Action | Semantic Version Release id: release # Adjust tag with desired version if applicable. - uses: python-semantic-release/python-semantic-release@v10.5.2 + uses: python-semantic-release/python-semantic-release@v10.5.3 with: github_token: ${{ secrets.GITHUB_TOKEN }} git_committer_name: "github-actions" git_committer_email: "actions@users.noreply.github.com" - name: Publish | Upload to GitHub Release Assets - uses: python-semantic-release/publish-action@v10.5.2 + uses: python-semantic-release/publish-action@v10.5.3 if: steps.release.outputs.released == 'true' with: github_token: ${{ secrets.GITHUB_TOKEN }} @@ -1005,7 +1005,7 @@ The equivalent GitHub Action configuration would be: - name: Action | Semantic Version Release # Adjust tag with desired version if applicable. - uses: python-semantic-release/python-semantic-release@v10.5.2 + uses: python-semantic-release/python-semantic-release@v10.5.3 with: github_token: ${{ secrets.GITHUB_TOKEN }} force: patch @@ -1064,14 +1064,14 @@ Publish Action. - name: Release submodule 1 id: release-submod-1 - uses: python-semantic-release/python-semantic-release@v10.5.2 + uses: python-semantic-release/python-semantic-release@v10.5.3 with: directory: ${{ env.SUBMODULE_1_DIR }} github_token: ${{ secrets.GITHUB_TOKEN }} - name: Release submodule 2 id: release-submod-2 - uses: python-semantic-release/python-semantic-release@v10.5.2 + uses: python-semantic-release/python-semantic-release@v10.5.3 with: directory: ${{ env.SUBMODULE_2_DIR }} github_token: ${{ secrets.GITHUB_TOKEN }} @@ -1083,7 +1083,7 @@ Publish Action. # ------------------------------------------------------------------- # - name: Publish | Upload package 1 to GitHub Release Assets - uses: python-semantic-release/publish-action@v10.5.2 + uses: python-semantic-release/publish-action@v10.5.3 if: steps.release-submod-1.outputs.released == 'true' with: directory: ${{ env.SUBMODULE_1_DIR }} @@ -1091,7 +1091,7 @@ Publish Action. tag: ${{ steps.release-submod-1.outputs.tag }} - name: Publish | Upload package 2 to GitHub Release Assets - uses: python-semantic-release/publish-action@v10.5.2 + uses: python-semantic-release/publish-action@v10.5.3 if: steps.release-submod-2.outputs.released == 'true' with: directory: ${{ env.SUBMODULE_2_DIR }} diff --git a/pyproject.toml b/pyproject.toml index 71c8e9a6c..19b636cb7 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -6,7 +6,7 @@ build-backend = "setuptools.build_meta" [project] name = "python-semantic-release" -version = "10.5.2" +version = "10.5.3" description = "Automatic Semantic Versioning for Python projects" requires-python = "~= 3.8" license = { text = "MIT" } @@ -409,6 +409,7 @@ sections = { "tests" = ["tests"] } ignore_names = ["change_to_ex_proj_dir", "init_example_project"] [tool.semantic_release] +add_partial_tags = true logging_use_named_masks = true commit_parser = "conventional" commit_parser_options = { parse_squash_commits = true, ignore_merge_commits = true } diff --git a/src/gh_action/Dockerfile b/src/gh_action/Dockerfile index 7166042ab..7ccbd3d40 100644 --- a/src/gh_action/Dockerfile +++ b/src/gh_action/Dockerfile @@ -16,6 +16,8 @@ RUN \ apt update && apt install -y --no-install-recommends \ # install git with git-lfs support git git-lfs \ + # install ssh client for git signing + openssh-client \ # install python cmodule / binary module build utilities python3-dev gcc make cmake cargo \ # Configure global pip diff --git a/src/gh_action/requirements.txt b/src/gh_action/requirements.txt index 9e4eb9193..c25d6fc5a 100644 --- a/src/gh_action/requirements.txt +++ b/src/gh_action/requirements.txt @@ -1 +1 @@ -python-semantic-release == 10.5.2 +python-semantic-release == 10.5.3 diff --git a/src/semantic_release/cli/commands/version.py b/src/semantic_release/cli/commands/version.py index d96e0d8ab..7a6fa26ef 100644 --- a/src/semantic_release/cli/commands/version.py +++ b/src/semantic_release/cli/commands/version.py @@ -753,6 +753,7 @@ def version( # noqa: C901 project.verify_upstream_unchanged( local_ref="HEAD~1", upstream_ref=config.remote.name, + remote_url=remote_url, noop=opts.noop, ) except UpstreamBranchChangedError as exc: diff --git a/src/semantic_release/commit_parser/conventional/options_monorepo.py b/src/semantic_release/commit_parser/conventional/options_monorepo.py index 58bcaf47d..241f7aaba 100644 --- a/src/semantic_release/commit_parser/conventional/options_monorepo.py +++ b/src/semantic_release/commit_parser/conventional/options_monorepo.py @@ -43,37 +43,46 @@ class ConventionalCommitMonorepoParserOptions(ConventionalCommitParserOptions): to match them literally. """ - @classmethod @field_validator("path_filters", mode="before") - def convert_strs_to_paths(cls, value: Any) -> tuple[Path, ...]: - values = value if isinstance(value, Iterable) else [value] - results: list[Path] = [] + @classmethod + def convert_strs_to_paths(cls, value: Any) -> tuple[str, ...]: + if isinstance(value, str): + return (value,) - for val in values: - if isinstance(val, (str, Path)): - results.append(Path(val)) - continue + if isinstance(value, Path): + return (str(value),) - raise TypeError(f"Invalid type: {type(val)}, expected str or Path.") + if isinstance(value, Iterable): + results: list[str] = [] + for val in value: + if isinstance(val, (str, Path)): + results.append(str(Path(val))) + continue - return tuple(results) + msg = f"Invalid type: {type(val)}, expected str or Path." + raise TypeError(msg) + + return tuple(results) + + msg = f"Invalid type: {type(value)}, expected str, Path, or Iterable." + raise TypeError(msg) - @classmethod @field_validator("path_filters", mode="after") - def resolve_path(cls, dir_paths: tuple[Path, ...]) -> tuple[Path, ...]: + @classmethod + def resolve_path(cls, dir_path_strs: tuple[str, ...]) -> tuple[str, ...]: return tuple( ( - Path(f"!{Path(str_path[1:]).expanduser().absolute().resolve()}") + f"!{Path(str_path[1:]).expanduser().absolute().resolve()}" # maintains the negation prefix if it exists - if (str_path := str(path)).startswith("!") + if str_path.startswith("!") # otherwise, resolve the path normally - else path.expanduser().absolute().resolve() + else str(Path(str_path).expanduser().absolute().resolve()) ) - for path in dir_paths + for str_path in dir_path_strs ) - @classmethod @field_validator("scope_prefix", mode="after") + @classmethod def validate_scope_prefix(cls, scope_prefix: str) -> str: if not scope_prefix: return "" diff --git a/src/semantic_release/gitproject.py b/src/semantic_release/gitproject.py index a5e4e4e19..cc86d33b5 100644 --- a/src/semantic_release/gitproject.py +++ b/src/semantic_release/gitproject.py @@ -336,13 +336,18 @@ def git_push_tag( raise GitPushError(f"Failed to push tag ({tag}) to remote") from err def verify_upstream_unchanged( # noqa: C901 - self, local_ref: str = "HEAD", upstream_ref: str = "origin", noop: bool = False + self, + local_ref: str = "HEAD", + upstream_ref: str = "origin", + remote_url: str | None = None, + noop: bool = False, ) -> None: """ Verify that the upstream branch has not changed since the given local reference. :param local_ref: The local reference to compare against upstream (default: HEAD) :param upstream_ref: The name of the upstream remote or specific remote branch (default: origin) + :param remote_url: Optional authenticated remote URL to use for fetching (default: None, uses configured remote) :param noop: Whether to skip the actual verification (for dry-run mode) :raises UpstreamBranchChangedError: If the upstream branch has changed @@ -409,7 +414,46 @@ def verify_upstream_unchanged( # noqa: C901 # Fetch the latest changes from the remote self.logger.info("Fetching latest changes from remote '%s'", remote_name) try: - remote_ref_obj.fetch() + # Check if we should use authenticated URL for fetch + # Only use remote_url if: + # 1. It's provided and different from the configured remote URL + # 2. It contains authentication credentials (@ symbol) + # 3. The configured remote is NOT a local path, file:// URL, or test URL (example.com) + # This ensures we don't break tests or local development + configured_url = remote_ref_obj.url + is_local_or_test_remote = ( + configured_url.startswith(("file://", "/", "C:/", "H:/")) + or "example.com" in configured_url + or not configured_url.startswith( + ( + "https://", + "http://", + "git://", + "git@", + "ssh://", + "git+ssh://", + ) + ) + ) + + use_authenticated_fetch = ( + remote_url + and "@" in remote_url + and remote_url != configured_url + and not is_local_or_test_remote + ) + + if use_authenticated_fetch: + # Use authenticated remote URL for fetch + # Fetch the remote branch and update the local tracking ref + repo.git.fetch( + remote_url, + f"refs/heads/{remote_branch_name}:refs/remotes/{upstream_full_ref_name}", + ) + else: + # Use the default remote configuration for local paths, + # file:// URLs, test URLs, or when no authentication is needed + remote_ref_obj.fetch() except GitCommandError as err: self.logger.exception(str(err)) err_msg = f"Failed to fetch from remote '{remote_name}'" diff --git a/tests/fixtures/git_repo.py b/tests/fixtures/git_repo.py index 46ebed0d6..613955290 100644 --- a/tests/fixtures/git_repo.py +++ b/tests/fixtures/git_repo.py @@ -142,6 +142,7 @@ class CommitDef(TypedDict): sha: str datetime: NotRequired[DatetimeISOStr] include_in_changelog: bool + file_to_change: NotRequired[Path | str] class BaseRepoVersionDef(TypedDict): """A Common Repo definition for a get_commits_repo_*() fixture with all commit convention types""" @@ -290,6 +291,7 @@ class CommitSpec(TypedDict): scipy: str datetime: NotRequired[DatetimeISOStr] include_in_changelog: NotRequired[bool] + file_to_change: NotRequired[Path | str] class DetailsBase(TypedDict): pre_actions: NotRequired[Sequence[RepoActions]] @@ -1127,7 +1129,9 @@ def _simulate_change_commits_n_rtn_changelog_entry( changelog_entries: list[CommitDef] = [] for commit_msg in commit_msgs: if not git_repo.is_dirty(index=True, working_tree=False): - add_text_to_file(git_repo, file_in_repo) + add_text_to_file( + git_repo, str(commit_msg.get("file_to_change", file_in_repo)) + ) changelog_entries.append(commit_n_rtn_changelog_entry(git_repo, commit_msg)) @@ -1336,39 +1340,6 @@ def _configure_base_repo( # noqa: C901 @pytest.fixture(scope="session") def separate_squashed_commit_def() -> SeparateSquashedCommitDefFn: - # default_conventional_parser: ConventionalCommitParser, - # default_emoji_parser: EmojiCommitParser, - # default_scipy_parser: ScipyCommitParser, - # message_parsers: dict[ - # CommitConvention, - # ConventionalCommitParser | EmojiCommitParser | ScipyCommitParser, - # ] = { - # "conventional": ConventionalCommitParser( - # options=ConventionalCommitParserOptions( - # **{ - # **default_conventional_parser.options.__dict__, - # "parse_squash_commits": True, - # } - # ) - # ), - # "emoji": EmojiCommitParser( - # options=EmojiParserOptions( - # **{ - # **default_emoji_parser.options.__dict__, - # "parse_squash_commits": True, - # } - # ) - # ), - # "scipy": ScipyCommitParser( - # options=ScipyParserOptions( - # **{ - # **default_scipy_parser.options.__dict__, - # "parse_squash_commits": True, - # } - # ) - # ), - # } - def _separate_squashed_commit_def( squashed_commit_def: CommitDef, parser: SquashedCommitSupportedParser, @@ -1435,7 +1406,7 @@ def _convert( ) # Extract the correct commit message for the commit type - return { + commit_def: CommitDef = { **parse_msg_fn(commit_spec[commit_type], parser=parser), "cid": commit_spec["cid"], "datetime": ( @@ -1443,9 +1414,18 @@ def _convert( if "datetime" in commit_spec else stable_now_date.isoformat(timespec="seconds") ), - "include_in_changelog": (commit_spec.get("include_in_changelog", True)), + "include_in_changelog": commit_spec.get("include_in_changelog", True), } + if "file_to_change" in commit_spec: + commit_def.update( + { + "file_to_change": commit_spec["file_to_change"], + } + ) + + return commit_def + return _convert diff --git a/tests/fixtures/monorepos/example_monorepo.py b/tests/fixtures/monorepos/example_monorepo.py index 6150e0635..1b46bde2b 100644 --- a/tests/fixtures/monorepos/example_monorepo.py +++ b/tests/fixtures/monorepos/example_monorepo.py @@ -68,8 +68,12 @@ def build_spec_hash_4_example_monorepo( @pytest.fixture(scope="session") def cached_example_monorepo( build_repo_or_copy_cache: BuildRepoOrCopyCacheFn, - monorepo_pkg1_dir: Path, - monorepo_pkg2_dir: Path, + monorepo_pkg1_name: str, + monorepo_pkg2_name: str, + monorepo_pkg1_dir: str, + monorepo_pkg2_dir: str, + monorepo_pkg1_docs_dir: str, + monorepo_pkg2_docs_dir: str, monorepo_pkg1_version_py_file: Path, monorepo_pkg2_version_py_file: Path, monorepo_pkg1_pyproject_toml_file: Path, @@ -104,6 +108,13 @@ def hello_world() -> None: print("{pkg_name} Hello World") ''' ).lstrip() + doc_index_contents = dedent( + """ + ================== + {pkg_name} Documentation + ================== + """ + ).lstrip() with temporary_working_directory(cached_project_path): update_version_py_file( @@ -127,6 +138,14 @@ def hello_world() -> None: (".gitignore", gitignore_contents), (monorepo_pkg1_pyproject_toml_file, EXAMPLE_PYPROJECT_TOML_CONTENT), (monorepo_pkg2_pyproject_toml_file, EXAMPLE_PYPROJECT_TOML_CONTENT), + ( + Path(monorepo_pkg1_docs_dir, "index.rst"), + doc_index_contents.format(pkg_name=monorepo_pkg1_name), + ), + ( + Path(monorepo_pkg2_docs_dir, "index.rst"), + doc_index_contents.format(pkg_name=monorepo_pkg2_name), + ), ] for file, contents in file_2_contents: @@ -216,6 +235,27 @@ def monorepo_pkg2_name() -> str: return "pkg2" +@pytest.fixture(scope="session") +def monorepo_pkg_docs_dir_pattern() -> str: + return str(Path("docs", "source", "{package_name}")) + + +@pytest.fixture(scope="session") +def monorepo_pkg1_docs_dir( + monorepo_pkg1_name: str, + monorepo_pkg_docs_dir_pattern: str, +) -> str: + return monorepo_pkg_docs_dir_pattern.format(package_name=monorepo_pkg1_name) + + +@pytest.fixture(scope="session") +def monorepo_pkg2_docs_dir( + monorepo_pkg2_name: str, + monorepo_pkg_docs_dir_pattern: str, +) -> str: + return monorepo_pkg_docs_dir_pattern.format(package_name=monorepo_pkg2_name) + + @pytest.fixture(scope="session") def monorepo_pkg_dir_pattern() -> str: return str(Path("packages", "{package_name}")) diff --git a/tests/fixtures/monorepos/github_flow/monorepo_w_default_release.py b/tests/fixtures/monorepos/github_flow/monorepo_w_default_release.py index f42789953..ecbe491ed 100644 --- a/tests/fixtures/monorepos/github_flow/monorepo_w_default_release.py +++ b/tests/fixtures/monorepos/github_flow/monorepo_w_default_release.py @@ -96,8 +96,10 @@ def get_repo_definition_4_github_flow_monorepo_w_default_release_channel( monorepo_pkg2_changelog_rst_file: Path, monorepo_pkg1_name: str, monorepo_pkg2_name: str, - monorepo_pkg1_dir: Path, - monorepo_pkg2_dir: Path, + monorepo_pkg1_dir: str, + monorepo_pkg2_dir: str, + monorepo_pkg1_docs_dir: str, + monorepo_pkg2_docs_dir: str, monorepo_pkg1_version_py_file: Path, monorepo_pkg2_version_py_file: Path, monorepo_pkg1_pyproject_toml_file: Path, @@ -117,7 +119,8 @@ def get_repo_definition_4_github_flow_monorepo_w_default_release_channel( * chore(release): pkg1@1.1.0 [skip ci] (tag: pkg1-v1.1.0, branch: main, HEAD -> main) * feat(pkg1): file modified outside of pkg 1, identified by scope (#5) | - | * feat(pkg1): file modified outside of pkg 1, identified by scope (branch: pkg1/feat/pr-4) + | * docs: pkg1 docs modified outside of pkg 1, identified by path filter (branch: pkg1/feat/pr-4) + | * feat(pkg1): file modified outside of pkg 1, identified by scope |/ * chore(release): pkg2@1.1.1 [skip ci] (tag: pkg2-v1.1.1) * fix(pkg2-cli): file modified outside of pkg 2, identified by scope (#4) @@ -207,21 +210,23 @@ def _get_repo_from_definition( if commit_type != "conventional": raise ValueError(f"Unsupported commit type: {commit_type}") + pkg1_path_filters = (".", f"../../{monorepo_pkg1_docs_dir}") pkg1_commit_parser = ConventionalCommitMonorepoParser( options=ConventionalCommitMonorepoParserOptions( parse_squash_commits=True, ignore_merge_commits=ignore_merge_commits, scope_prefix=f"{monorepo_pkg1_name}-?", - path_filters=(".",), + path_filters=pkg1_path_filters, ) ) + pkg2_path_filters = (".", f"../../{monorepo_pkg2_docs_dir}") pkg2_commit_parser = ConventionalCommitMonorepoParser( options=ConventionalCommitMonorepoParserOptions( parse_squash_commits=pkg1_commit_parser.options.parse_squash_commits, ignore_merge_commits=pkg1_commit_parser.options.ignore_merge_commits, scope_prefix=f"{monorepo_pkg2_name}-?", - path_filters=(".",), + path_filters=pkg2_path_filters, ) ) @@ -277,7 +282,7 @@ def _get_repo_from_definition( ) ), "tool.semantic_release.commit_parser_options.scope_prefix": pkg1_commit_parser.options.scope_prefix, - "tool.semantic_release.commit_parser_options.path_filters": pkg1_commit_parser.options.path_filters, + "tool.semantic_release.commit_parser_options.path_filters": pkg1_path_filters, **(extra_configs or {}), }, }, @@ -301,7 +306,7 @@ def _get_repo_from_definition( ) ), "tool.semantic_release.commit_parser_options.scope_prefix": pkg2_commit_parser.options.scope_prefix, - "tool.semantic_release.commit_parser_options.path_filters": pkg2_commit_parser.options.path_filters, + "tool.semantic_release.commit_parser_options.path_filters": pkg2_path_filters, **(extra_configs or {}), }, }, @@ -774,7 +779,15 @@ def _get_repo_from_definition( "emoji": ":sparkles: (pkg1) file modified outside of pkg 1, identified by scope", "scipy": "ENH:pkg1: file modified outside of pkg 1, identified by scope", "datetime": next(commit_timestamp_gen), - } + }, + { + "cid": "pkg1-docs-2-squashed", + "conventional": "docs: pkg1 docs modified outside of pkg 1, identified by path filter", + "emoji": ":book: pkg1 docs modified outside of pkg 1, identified by path filter", + "scipy": "DOC: pkg1 docs modified outside of pkg 1, identified by path filter", + "datetime": next(commit_timestamp_gen), + "file_to_change": f"{monorepo_pkg1_docs_dir}/index.rst", + }, ] repo_construction_steps.extend( diff --git a/tests/fixtures/monorepos/github_flow/monorepo_w_release_channels.py b/tests/fixtures/monorepos/github_flow/monorepo_w_release_channels.py index 1e25cdb4f..9b4027195 100644 --- a/tests/fixtures/monorepos/github_flow/monorepo_w_release_channels.py +++ b/tests/fixtures/monorepos/github_flow/monorepo_w_release_channels.py @@ -97,8 +97,10 @@ def get_repo_definition_4_github_flow_monorepo_w_feature_release_channel( monorepo_pkg2_changelog_rst_file: Path, monorepo_pkg1_name: str, monorepo_pkg2_name: str, - monorepo_pkg1_dir: Path, - monorepo_pkg2_dir: Path, + monorepo_pkg1_dir: str, + monorepo_pkg2_dir: str, + monorepo_pkg1_docs_dir: str, + monorepo_pkg2_docs_dir: str, monorepo_pkg1_version_py_file: Path, monorepo_pkg2_version_py_file: Path, stable_now_date: GetStableDateNowFn, @@ -119,7 +121,7 @@ def get_repo_definition_4_github_flow_monorepo_w_feature_release_channel( | * chore(release): pkg2@1.1.0-alpha.2 [skip ci] (tag: pkg2-v1.1.0-alpha.2, branch: pkg2/feat/pr-2) | * fix(pkg2-cli): file modified outside of pkg 2, identified by scope | * chore(release): pkg2@1.1.0-alpha.1 [skip ci] (tag: pkg2-v1.1.0-alpha.1) - | * docs: add cli documentation + | * docs: pkg2 docs modified outside of pkg 2, identified by path filter | * test: add cli tests | * feat: no pkg scope but file in pkg 2 directory |/ @@ -203,21 +205,23 @@ def _get_repo_from_definition( if commit_type != "conventional": raise ValueError(f"Unsupported commit type: {commit_type}") + pkg1_path_filters = (".", f"../../{monorepo_pkg1_docs_dir}") pkg1_commit_parser = ConventionalCommitMonorepoParser( options=ConventionalCommitMonorepoParserOptions( parse_squash_commits=True, ignore_merge_commits=ignore_merge_commits, scope_prefix=f"{monorepo_pkg1_name}-?", - path_filters=(".",), + path_filters=pkg1_path_filters, ) ) + pkg2_path_filters = (".", f"../../{monorepo_pkg2_docs_dir}") pkg2_commit_parser = ConventionalCommitMonorepoParser( options=ConventionalCommitMonorepoParserOptions( parse_squash_commits=pkg1_commit_parser.options.parse_squash_commits, ignore_merge_commits=pkg1_commit_parser.options.ignore_merge_commits, scope_prefix=f"{monorepo_pkg2_name}-?", - path_filters=(".",), + path_filters=pkg2_path_filters, ) ) @@ -277,7 +281,7 @@ def _get_repo_from_definition( "prerelease_token": "alpha", }, "tool.semantic_release.commit_parser_options.scope_prefix": pkg1_commit_parser.options.scope_prefix, - "tool.semantic_release.commit_parser_options.path_filters": pkg1_commit_parser.options.path_filters, + "tool.semantic_release.commit_parser_options.path_filters": pkg1_path_filters, **(extra_configs or {}), }, }, @@ -307,7 +311,7 @@ def _get_repo_from_definition( "prerelease_token": "alpha", }, "tool.semantic_release.commit_parser_options.scope_prefix": pkg2_commit_parser.options.scope_prefix, - "tool.semantic_release.commit_parser_options.path_filters": pkg2_commit_parser.options.path_filters, + "tool.semantic_release.commit_parser_options.path_filters": pkg2_path_filters, **(extra_configs or {}), }, }, @@ -646,11 +650,12 @@ def _get_repo_from_definition( cid_pkg2_feb1_c3_docs := "pkg2_feat_branch_1_c3_docs" ), - "conventional": "docs: add cli documentation", - "emoji": ":memo: add cli documentation", - "scipy": "DOC: add cli documentation", + "conventional": "docs: pkg2 docs modified outside of pkg 2, identified by path filter", + "emoji": ":book: pkg2 docs modified outside of pkg 2, identified by path filter", + "scipy": "DOC: pkg2 docs modified outside of pkg 2, identified by path filter", "datetime": next(commit_timestamp_gen), "include_in_changelog": True, + "file_to_change": f"../../{monorepo_pkg2_docs_dir}/index.rst", }, ], commit_type, diff --git a/tests/fixtures/monorepos/trunk_based_dev/monorepo_w_tags.py b/tests/fixtures/monorepos/trunk_based_dev/monorepo_w_tags.py index 9687e088f..71bc6e9c7 100644 --- a/tests/fixtures/monorepos/trunk_based_dev/monorepo_w_tags.py +++ b/tests/fixtures/monorepos/trunk_based_dev/monorepo_w_tags.py @@ -88,8 +88,10 @@ def get_repo_definition_4_trunk_only_monorepo_w_tags( monorepo_pkg2_changelog_rst_file: Path, monorepo_pkg1_name: str, monorepo_pkg2_name: str, - monorepo_pkg1_dir: Path, - monorepo_pkg2_dir: Path, + monorepo_pkg1_dir: str, + monorepo_pkg2_dir: str, + monorepo_pkg1_docs_dir: str, + monorepo_pkg2_docs_dir: str, monorepo_pkg1_version_py_file: Path, monorepo_pkg2_version_py_file: Path, stable_now_date: GetStableDateNowFn, @@ -104,6 +106,7 @@ def get_repo_definition_4_trunk_only_monorepo_w_tags( ``` * chore(release): pkg1@0.1.0 [skip ci] (tag: pkg1-v0.1.0, branch: main) + * docs: pkg1 docs modified outside of pkg 1, identified by path filter * feat(pkg1): file modified outside of pkg 1, identified by scope * chore(release): pkg2@0.1.1 [skip ci] (tag: pkg2-v0.1.1) * fix(pkg2-cli): file modified outside of pkg 2, identified by scope @@ -182,21 +185,23 @@ def _get_repo_from_definition( if commit_type != "conventional": raise ValueError(f"Unsupported commit type: {commit_type}") + pkg1_path_filters = (".", f"../../{monorepo_pkg1_docs_dir}") pkg1_commit_parser = ConventionalCommitMonorepoParser( options=ConventionalCommitMonorepoParserOptions( parse_squash_commits=True, ignore_merge_commits=ignore_merge_commits, scope_prefix=f"{monorepo_pkg1_name}-?", - path_filters=(".",), + path_filters=pkg1_path_filters, ) ) + pkg2_path_filters = (".", f"../../{monorepo_pkg2_docs_dir}") pkg2_commit_parser = ConventionalCommitMonorepoParser( options=ConventionalCommitMonorepoParserOptions( parse_squash_commits=pkg1_commit_parser.options.parse_squash_commits, ignore_merge_commits=pkg1_commit_parser.options.ignore_merge_commits, scope_prefix=f"{monorepo_pkg2_name}-?", - path_filters=(".",), + path_filters=pkg2_path_filters, ) ) @@ -247,7 +252,7 @@ def _get_repo_from_definition( ) ), "tool.semantic_release.commit_parser_options.scope_prefix": pkg1_commit_parser.options.scope_prefix, - "tool.semantic_release.commit_parser_options.path_filters": pkg1_commit_parser.options.path_filters, + "tool.semantic_release.commit_parser_options.path_filters": pkg1_path_filters, **(extra_configs or {}), }, }, @@ -271,7 +276,7 @@ def _get_repo_from_definition( ) ), "tool.semantic_release.commit_parser_options.scope_prefix": pkg2_commit_parser.options.scope_prefix, - "tool.semantic_release.commit_parser_options.path_filters": pkg2_commit_parser.options.path_filters, + "tool.semantic_release.commit_parser_options.path_filters": pkg2_path_filters, **(extra_configs or {}), }, }, @@ -524,6 +529,14 @@ def _get_repo_from_definition( "scipy": "ENH:pkg1: file modified outside of pkg 1, identified by scope", "datetime": next(commit_timestamp_gen), }, + { + "cid": (cid_c11_pkg1_docs := "c11_pkg1_docs"), + "conventional": "docs: pkg1 docs modified outside of pkg 1, identified by path filter", + "emoji": ":book: pkg1 docs modified outside of pkg 1, identified by path filter", + "scipy": "DOC: pkg1 docs modified outside of pkg 1, identified by path filter", + "datetime": next(commit_timestamp_gen), + "file_to_change": f"{monorepo_pkg1_docs_dir}/index.rst", + }, ], commit_type, parser=cast( @@ -550,7 +563,10 @@ def _get_repo_from_definition( "details": { "new_version": pkg1_new_version, "dest_files": pkg1_changelog_file_definitions, - "commit_ids": [cid_c10_pkg1_feat], + "commit_ids": [ + cid_c10_pkg1_feat, + cid_c11_pkg1_docs, + ], }, }, change_to_pkg1_dir, diff --git a/tests/gh_action/suite/test_version_ssh_signing.sh b/tests/gh_action/suite/test_version_ssh_signing.sh new file mode 100644 index 000000000..0f0c82c1a --- /dev/null +++ b/tests/gh_action/suite/test_version_ssh_signing.sh @@ -0,0 +1,105 @@ +#!/bin/bash + +__file__="$(realpath "${BASH_SOURCE[0]}")" +__directory__="$(dirname "${__file__}")" + +if ! [ "${UTILS_LOADED}" = "true" ]; then + # shellcheck source=tests/utils.sh + source "$__directory__/../utils.sh" +fi + +test_version_ssh_signing() { + # Test that SSH signing keys are correctly configured in the action + # We will generate an SSH key pair and pass it to the action to ensure + # the ssh-agent and ssh-add commands work correctly + local index="${1:?Index not provided}" + local test_name="${FUNCNAME[0]}" + + # Generate a temporary SSH key pair for testing + local ssh_key_dir + ssh_key_dir="$(mktemp -d)" + local ssh_private_key_file="$ssh_key_dir/signing_key" + local ssh_public_key_file="$ssh_key_dir/signing_key.pub" + + # Generate SSH key pair (Ed25519 for faster generation and smaller keys) + # Note: Using empty passphrase (-N "") for test purposes only + if ! ssh-keygen -t ed25519 -N "" -f "$ssh_private_key_file" -C "test@example.com" >/dev/null 2>&1; then + error "Failed to generate SSH key pair!" + rm -rf "$ssh_key_dir" + return 1 + fi + + # Read the generated keys + local ssh_public_key + local ssh_private_key + ssh_public_key="$(cat "$ssh_public_key_file")" + ssh_private_key="$(cat "$ssh_private_key_file")" + + # Clean up the temporary key files + rm -rf "$ssh_key_dir" + + # Create expectations & set env variables that will be passed in for Docker command + local WITH_VAR_GITHUB_TOKEN="ghp_1x2x3x4x5x6x7x8x9x0x1x2x3x4x5x6x7x8x9x0" + local WITH_VAR_NO_OPERATION_MODE="true" + local WITH_VAR_VERBOSITY="2" + local WITH_VAR_GIT_COMMITTER_NAME="Test User" + local WITH_VAR_GIT_COMMITTER_EMAIL="test@example.com" + local WITH_VAR_SSH_PUBLIC_SIGNING_KEY="$ssh_public_key" + local WITH_VAR_SSH_PRIVATE_SIGNING_KEY="$ssh_private_key" + + # Expected messages in output + local expected_ssh_setup_msg="SSH Key pair found, configuring signing..." + local expected_psr_cmd=".*/bin/semantic-release -vv --noop version" + + # Execute the test & capture output + local output="" + if ! output="$(run_test "$index. $test_name" 2>&1)"; then + # Log the output for debugging purposes + log "$output" + error "fatal error occurred!" + error "::error:: $test_name failed!" + return 1 + fi + + # Evaluate the output to ensure SSH setup message is present + if ! printf '%s' "$output" | grep -q "$expected_ssh_setup_msg"; then + # Log the output for debugging purposes + log "$output" + error "Failed to find SSH setup message in the output!" + error "\tExpected Message: $expected_ssh_setup_msg" + error "::error:: $test_name failed!" + return 1 + fi + + # Evaluate the output to ensure ssh-agent was started successfully + if ! printf '%s' "$output" | grep -q "Agent pid"; then + # Log the output for debugging purposes + log "$output" + error "Failed to find ssh-agent start message in the output!" + error "\tExpected Message pattern: 'Agent pid'" + error "::error:: $test_name failed!" + return 1 + fi + + # Evaluate the output to ensure ssh-add was successful + if ! printf '%s' "$output" | grep -q "Identity added"; then + # Log the output for debugging purposes + log "$output" + error "Failed to find ssh-add success message in the output!" + error "\tExpected Message pattern: 'Identity added'" + error "::error:: $test_name failed!" + return 1 + fi + + # Evaluate the output to ensure the expected command is present + if ! printf '%s' "$output" | grep -q -E "$expected_psr_cmd"; then + # Log the output for debugging purposes + log "$output" + error "Failed to find the expected command in the output!" + error "\tExpected Command: $expected_psr_cmd" + error "::error:: $test_name failed!" + return 1 + fi + + log "\n$index. $test_name: PASSED!" +} diff --git a/tests/unit/semantic_release/test_gitproject.py b/tests/unit/semantic_release/test_gitproject.py index 09193d317..58bfedf81 100644 --- a/tests/unit/semantic_release/test_gitproject.py +++ b/tests/unit/semantic_release/test_gitproject.py @@ -62,6 +62,7 @@ def mock_repo(tmp_path: Path) -> RepoMock: # Mock remotes remote_obj = MagicMock() remote_obj.fetch = MagicMock() + remote_obj.url = "https://github.com/owner/repo.git" # Set a non-test URL # Mock refs for the remote ref_obj = MagicMock() @@ -249,6 +250,25 @@ def test_verify_upstream_unchanged_with_custom_ref( mock_repo.git.rev_parse.assert_called_once_with("HEAD~1") +def test_verify_upstream_unchanged_with_remote_url( + mock_gitproject: GitProject, mock_repo: RepoMock +): + """Test that verify_upstream_unchanged uses remote_url when provided.""" + remote_url = "https://token:x-oauth-basic@github.com/owner/repo.git" + + # Should not raise an exception + mock_gitproject.verify_upstream_unchanged( + local_ref="HEAD", remote_url=remote_url, noop=False + ) + + # Verify git.fetch was called with the remote_url and proper refspec instead of remote_ref_obj.fetch() + mock_repo.git.fetch.assert_called_once_with( + remote_url, "refs/heads/main:refs/remotes/origin/main" + ) + # Verify that remote_ref_obj.fetch() was NOT called + mock_repo.remotes["origin"].fetch.assert_not_called() + + def test_is_shallow_clone_true(mock_gitproject: GitProject, tmp_path: Path) -> None: """Test is_shallow_clone returns True when shallow file exists.""" # Create a shallow file