diff --git a/.github/workflows/bump-homebrew.yml b/.github/workflows/bump-homebrew.yml index 8446d94..ecae038 100644 --- a/.github/workflows/bump-homebrew.yml +++ b/.github/workflows/bump-homebrew.yml @@ -126,10 +126,48 @@ jobs: run: | set -euo pipefail - brew bump-formula-pr \ - --write-only --no-audit --no-browse \ - --url="${SDIST_URL}" \ - --sha256="${SDIST_SHA}" \ + FORMULA_FILE="tap/Formula/${FORMULA##*/}.rb" + FORMULA_FILE="${FORMULA_FILE}" \ + SDIST_URL="${SDIST_URL}" \ + SDIST_SHA="${SDIST_SHA}" \ + VERSION="${VERSION}" \ + python - <<'PY' + import os + import re + from pathlib import Path + from urllib.parse import unquote, urlparse + + formula_file = Path(os.environ["FORMULA_FILE"]) + url = os.environ["SDIST_URL"] + sha256 = os.environ["SDIST_SHA"] + version = os.environ["VERSION"] + expected_filename = f"cisco_sccfm_devkit-{version}.tar.gz" + parsed_url = urlparse(url) + if parsed_url.scheme != "https" or parsed_url.netloc != "files.pythonhosted.org": + raise SystemExit("PyPI sdist URL is not hosted by files.pythonhosted.org") + if Path(unquote(parsed_url.path)).name != expected_filename: + raise SystemExit("PyPI sdist URL does not match the release version") + if re.fullmatch(r"[0-9a-f]{64}", sha256) is None: + raise SystemExit("PyPI sdist SHA-256 is invalid") + + text = formula_file.read_text(encoding="utf-8") + url_match = re.search(r'(?m)^ url "([^"]+)"$', text) + sha_match = re.search(r'(?m)^ sha256 "([^"]+)"$', text) + if url_match is None or sha_match is None: + raise SystemExit("formula stable URL or SHA-256 is missing") + if not url_match.group(1).startswith("https://files.pythonhosted.org/"): + raise SystemExit("formula stable URL is not hosted by files.pythonhosted.org") + + updated = text[:url_match.start(1)] + url + text[url_match.end(1):] + sha_offset = sha_match.start(1) + (len(updated) - len(text)) + sha_end = sha_match.end(1) + (len(updated) - len(text)) + updated = updated[:sha_offset] + sha256 + updated[sha_end:] + formula_file.write_text(updated, encoding="utf-8") + PY + + brew update-python-resources \ + --ignore-main-package-cooldown \ + --version="${VERSION}" \ "${FORMULA}" diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 51fc864..7de9dfc 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -583,7 +583,7 @@ jobs: github.event_name == 'push' && steps.version.outputs.bumped == 'true' && steps.version.outputs.resume != 'true' - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v7 with: name: ${{ steps.source.outputs.bundle_name }} path: | @@ -686,7 +686,7 @@ jobs: python-version: "3.12" - name: Download exact release bundle - uses: actions/download-artifact@v4 + uses: actions/download-artifact@v7 with: name: ${{ needs.prepare-release.outputs.bundle_name }} path: ${{ runner.temp }}/release-bundle diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index c64945f..4ad5268 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -130,7 +130,7 @@ jobs: echo "bundle_name=${BUNDLE_NAME}" >> "$GITHUB_OUTPUT" - name: Preserve verified release bundle - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v7 with: name: ${{ steps.bundle.outputs.bundle_name }} path: | @@ -167,7 +167,7 @@ jobs: python -m pip install twine==6.2.0 - name: Download verified release bundle - uses: actions/download-artifact@v4 + uses: actions/download-artifact@v7 with: name: ${{ needs.validate-release.outputs.bundle_name }} path: ${{ runner.temp }}/release-bundle @@ -277,10 +277,21 @@ jobs: INSTALL_ROOT="$(mktemp -d "${RUNNER_TEMP}/sccfm-pypi-install.XXXXXX")" python -m venv "${INSTALL_ROOT}/venv" - "${INSTALL_ROOT}/venv/bin/python" -I -m pip install \ - --no-cache-dir \ - --index-url https://pypi.org/simple \ - "cisco-sccfm-devkit==${RELEASE_VERSION}" + for attempt in $(seq 1 60); do + if "${INSTALL_ROOT}/venv/bin/python" -I -m pip install \ + --no-cache-dir \ + --index-url https://pypi.org/simple \ + "cisco-sccfm-devkit==${RELEASE_VERSION}"; then + break + fi + if [[ "${attempt}" = "60" ]]; then + echo "::error::PyPI Simple API did not expose ${RELEASE_VERSION} in time" + exit 1 + fi + echo "PyPI Simple API has not exposed ${RELEASE_VERSION} yet" \ + "(attempt ${attempt}); waiting 5s..." + sleep 5 + done "${INSTALL_ROOT}/venv/bin/python" -I -m pip check "${INSTALL_ROOT}/venv/bin/sccfm-cli" --help >/dev/null "${INSTALL_ROOT}/venv/bin/sccfm-cli-interactive" --help >/dev/null @@ -313,7 +324,7 @@ jobs: python -m pip install "ansible-core>=2.20,<2.22" - name: Download verified release bundle - uses: actions/download-artifact@v4 + uses: actions/download-artifact@v7 with: name: ${{ needs.validate-release.outputs.bundle_name }} path: ${{ runner.temp }}/release-bundle @@ -433,10 +444,21 @@ jobs: cmp -s "${COLLECTION_PATH}" "${DOWNLOADED_COLLECTION}" INSTALL_ROOT="$(mktemp -d "${RUNNER_TEMP}/sccfm-galaxy-install.XXXXXX")" - python -m pip install \ - --no-cache-dir \ - --index-url https://pypi.org/simple \ - "cisco-sccfm-devkit==${RELEASE_VERSION}" + for attempt in $(seq 1 60); do + if python -m pip install \ + --no-cache-dir \ + --index-url https://pypi.org/simple \ + "cisco-sccfm-devkit==${RELEASE_VERSION}"; then + break + fi + if [[ "${attempt}" = "60" ]]; then + echo "::error::PyPI Simple API did not expose ${RELEASE_VERSION} in time" + exit 1 + fi + echo "PyPI Simple API has not exposed ${RELEASE_VERSION} yet" \ + "(attempt ${attempt}); waiting 5s..." + sleep 5 + done ansible-galaxy collection install \ "${DOWNLOADED_COLLECTION}" \ --collections-path "${INSTALL_ROOT}" \ diff --git a/tests/test_release_artifacts.py b/tests/test_release_artifacts.py index 6c81085..5bee45c 100644 --- a/tests/test_release_artifacts.py +++ b/tests/test_release_artifacts.py @@ -252,7 +252,7 @@ def test_workflows_separate_automatic_preparation_from_manual_deployment() -> No 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 "actions/upload-artifact@v7" 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 @@ -262,7 +262,7 @@ def test_workflows_separate_automatic_preparation_from_manual_deployment() -> No 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" + "actions/upload-artifact@v7" ) prohibited_deploy_commands = ( @@ -285,18 +285,18 @@ def test_workflows_separate_automatic_preparation_from_manual_deployment() -> No 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 job.count("actions/download-artifact@v7") == 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 job.index("actions/download-artifact@v7") < 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 release.count("actions/upload-artifact@v7") == 1 + assert release.count("actions/download-artifact@v7") == 2 assert "pypa/gh-action-pypi-publish@dc37677b2e1c63e2034f94d8a5b11f265b73ba33" in pypi assert "pypa/gh-action-pypi-publish@release/v1" not in pypi