diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 16c78872..6f9c4df9 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -19,12 +19,15 @@ jobs: validate-release: runs-on: ubuntu-latest permissions: - contents: read + # Draft releases are visible only to tokens with push access. Keep that + # access isolated from the jobs that receive registry credentials. + contents: write outputs: version: ${{ steps.release.outputs.version }} tag: ${{ steps.release.outputs.tag }} source_commit: ${{ steps.bundle.outputs.source_commit }} manifest_sha256: ${{ steps.bundle.outputs.manifest_sha256 }} + bundle_name: ${{ steps.bundle.outputs.bundle_name }} is_draft: ${{ steps.release.outputs.is_draft }} steps: - name: Validate selected draft release @@ -121,8 +124,23 @@ jobs: --version "${RELEASE_VERSION}" \ --tag "${RELEASE_TAG}" \ --source-commit "${SOURCE_COMMIT}" + BUNDLE_NAME="release-promotion-${RELEASE_VERSION}-${SOURCE_COMMIT}-${GITHUB_RUN_ID}-attempt-${GITHUB_RUN_ATTEMPT}" echo "source_commit=${SOURCE_COMMIT}" >> "$GITHUB_OUTPUT" echo "manifest_sha256=${MANIFEST_SHA256}" >> "$GITHUB_OUTPUT" + echo "bundle_name=${BUNDLE_NAME}" >> "$GITHUB_OUTPUT" + + - name: Preserve verified release bundle + uses: actions/upload-artifact@v4 + with: + name: ${{ steps.bundle.outputs.bundle_name }} + path: | + ${{ runner.temp }}/release-bundle/cisco_sccfm_devkit-${{ steps.release.outputs.version }}-py3-none-any.whl + ${{ runner.temp }}/release-bundle/cisco_sccfm_devkit-${{ steps.release.outputs.version }}.tar.gz + ${{ runner.temp }}/release-bundle/cisco-sccfm-${{ steps.release.outputs.version }}.tar.gz + ${{ runner.temp }}/release-bundle/release-manifest.json + if-no-files-found: error + compression-level: 0 + retention-days: 30 publish-to-pypi: needs: validate-release @@ -148,17 +166,11 @@ jobs: python -m pip install --upgrade pip python -m pip install twine==6.2.0 - - name: Download exact draft release bundle - env: - GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} - RELEASE_TAG: ${{ needs.validate-release.outputs.tag }} - BUNDLE_DIR: ${{ runner.temp }}/release-bundle - run: | - set -euo pipefail - mkdir -p "${BUNDLE_DIR}" - gh release download "${RELEASE_TAG}" \ - --repo "${GITHUB_REPOSITORY}" \ - --dir "${BUNDLE_DIR}" + - name: Download verified release bundle + uses: actions/download-artifact@v4 + with: + name: ${{ needs.validate-release.outputs.bundle_name }} + path: ${{ runner.temp }}/release-bundle - name: Verify bundle and inspect PyPI state id: pypi @@ -300,17 +312,11 @@ jobs: python -m pip install --upgrade pip python -m pip install "ansible-core>=2.20,<2.22" - - name: Download exact draft release bundle - env: - GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} - RELEASE_TAG: ${{ needs.validate-release.outputs.tag }} - BUNDLE_DIR: ${{ runner.temp }}/release-bundle - run: | - set -euo pipefail - mkdir -p "${BUNDLE_DIR}" - gh release download "${RELEASE_TAG}" \ - --repo "${GITHUB_REPOSITORY}" \ - --dir "${BUNDLE_DIR}" + - name: Download verified release bundle + uses: actions/download-artifact@v4 + with: + name: ${{ needs.validate-release.outputs.bundle_name }} + path: ${{ runner.temp }}/release-bundle - name: Verify bundle and inspect Galaxy state id: galaxy diff --git a/sccfm-ansible/CHANGELOG.rst b/sccfm-ansible/CHANGELOG.rst index 1d844f1d..504add31 100644 --- a/sccfm-ansible/CHANGELOG.rst +++ b/sccfm-ansible/CHANGELOG.rst @@ -4,6 +4,14 @@ Cisco SCCFM Collection Release Notes .. contents:: Topics +v0.39.4 +======== + +Bugfixes +-------- + +- Fixed manual release promotion so verified draft assets are handed off to read-only PyPI and Galaxy publication jobs. + v0.39.3 ======== diff --git a/sccfm-ansible/changelogs/changelog.yaml b/sccfm-ansible/changelogs/changelog.yaml index 358e6957..052bf2b0 100644 --- a/sccfm-ansible/changelogs/changelog.yaml +++ b/sccfm-ansible/changelogs/changelog.yaml @@ -2,6 +2,13 @@ ancestor: null # sccfm-release-retarget-seed: 0.39.0 releases: + 0.39.4: + changes: + bugfixes: + - Fixed manual release promotion so verified draft assets are handed off to + read-only PyPI and Galaxy publication jobs. + fragments: [] + release_date: '2026-08-19' 0.39.3: changes: bugfixes: diff --git a/tests/test_release_artifacts.py b/tests/test_release_artifacts.py index 1baaacba..6c810855 100644 --- a/tests/test_release_artifacts.py +++ b/tests/test_release_artifacts.py @@ -251,6 +251,19 @@ def test_workflows_separate_automatic_preparation_from_manual_deployment() -> No assert 'git rev-parse "refs/tags/${RELEASE_TAG}^{commit}"' in validation assert "release-manifest-sha256:" in validation assert "release_artifacts verify" in validation + assert "contents: write" in validation + assert "actions/upload-artifact@v4" in validation + assert "bundle_name: ${{ steps.bundle.outputs.bundle_name }}" in validation + assert "name: ${{ steps.bundle.outputs.bundle_name }}" in validation + assert "GITHUB_RUN_ID" in validation + assert "GITHUB_RUN_ATTEMPT" in validation + assert "if-no-files-found: error" in validation + assert "overwrite:" not in validation + assert "secrets.PYPI_API_TOKEN" not in validation + assert "secrets.GALAXY_API_KEY" not in validation + assert validation.index("release_artifacts verify") < validation.index( + "actions/upload-artifact@v4" + ) prohibited_deploy_commands = ( "poetry build", @@ -260,17 +273,31 @@ def test_workflows_separate_automatic_preparation_from_manual_deployment() -> No "git commit ", "git tag ", "git push ", - "actions/upload-artifact", - "actions/download-artifact", "SCCFM_CI_DEPLOY_KEY", ) for command in prohibited_deploy_commands: assert command not in release - for job in (validation, pypi, galaxy, finalizer): + for job in (validation, finalizer): assert 'gh release download "${RELEASE_TAG}"' in job assert "release_artifacts verify" in job + for job in (pypi, galaxy): + assert "contents: read" in job + assert "contents: write" not in job + assert job.count("actions/download-artifact@v4") == 1 + assert "name: ${{ needs.validate-release.outputs.bundle_name }}" in job + assert 'gh release download "${RELEASE_TAG}"' not in job + assert "GH_TOKEN:" not in job + assert "secrets.GITHUB_TOKEN" not in job + assert "release_artifacts verify" in job + assert "EXPECTED_MANIFEST_SHA256" in job + assert job.index("actions/download-artifact@v4") < job.index("release_artifacts verify") + + assert "contents: write" in finalizer + assert release.count("actions/upload-artifact@v4") == 1 + assert release.count("actions/download-artifact@v4") == 2 + assert "pypa/gh-action-pypi-publish@dc37677b2e1c63e2034f94d8a5b11f265b73ba33" in pypi assert "pypa/gh-action-pypi-publish@release/v1" not in pypi assert "secrets.PYPI_API_TOKEN" in pypi