diff --git a/.ci/scripts/check_up_to_date.py b/.ci/scripts/check_up_to_date.py new file mode 100644 index 00000000..ad547d3c --- /dev/null +++ b/.ci/scripts/check_up_to_date.py @@ -0,0 +1,74 @@ +import argparse +import requests +from urllib.parse import urljoin +from packaging.version import parse +from packaging.requirements import Requirement + +PACKAGES = [ + "pulp-ansible", + "pulp-container", + "pulp-deb", + "pulp-gem", + "pulp-maven", + "pulp-python", + "pulp-rpm", + "pulp-ostree" +] + +INDEX = "https://pypi.org" + +def check_update(branch, current_versions): + """ + Go through each of the image's main Pulp components and see if there is a new version available. + """ + new_versions = {} + # Get the latest Z (or Y) pulpcore release for this branch + core_pypi_response = requests.get(urljoin(INDEX, "pypi/pulpcore/json")) + assert core_pypi_response.status_code == 200 + core_version = parse(current_versions["pulpcore"]) + for version, release in core_pypi_response.json()["releases"].items(): + cur_version = parse(version) + if cur_version > core_version: + if branch != "latest": + if cur_version.major != core_version.major or cur_version.minor != core_version.minor: + continue + core_version = cur_version + new_versions["pulpcore"] = core_version + + # Now check each plugin to see if they need updates + for plugin in PACKAGES: + if plugin not in current_versions: + continue + plugin_version = parse(current_versions[plugin]) + plugin_pypi_response = requests.get(urljoin(INDEX, f"pypi/{plugin}/json")) + assert plugin_pypi_response.status_code == 200 + plugin_versions = sorted((parse(v) for v in plugin_pypi_response.json()["releases"].keys()), reverse=True) + for version in plugin_versions: + if version <= plugin_version: + break + version_pypi_response = requests.get(urljoin(INDEX, f"pypi/{plugin}/{version}/json")) + assert version_pypi_response.status_code == 200 + deps = version_pypi_response.json()["info"]["requires_dist"] + core_dep = next(filter(lambda dep: dep.startswith("pulpcore"), deps)) + if core_version in Requirement(core_dep).specifier: + new_versions[plugin] = version + break + + if new_versions: + print("Updates needed for:") + for plugin, version in new_versions.items(): + print(f"{plugin}: {current_versions[plugin]} -> {version!s}") + exit(100) + + print("No updates needed :)") + +if __name__ == '__main__': + parser = argparse.ArgumentParser() + parser.add_argument("branch") + parser.add_argument("versions", type=argparse.FileType("r")) + opts = parser.parse_args() + versions = {} + for line in opts.versions: + plugin, _, version = line.rstrip("\n").partition("==") + versions[plugin] = version + check_update(opts.branch, versions) diff --git a/.github/actions/base_images/action.yml b/.github/actions/base_images/action.yml index fdc54c90..a12d67d4 100644 --- a/.github/actions/base_images/action.yml +++ b/.github/actions/base_images/action.yml @@ -11,6 +11,9 @@ outputs: base_cache_key: value: ${{ steps.hash_key.outputs.base_cache_key }} description: "The cache key the built images were uploaded to." + rebuilt_images: + value: ${{ env.BUILD_IMAGES }} + description: "The images that were rebuilt or empty" runs: using: "composite" diff --git a/.github/actions/build_image/action.yml b/.github/actions/build_image/action.yml index 8e351fb9..cfe4dc0c 100644 --- a/.github/actions/build_image/action.yml +++ b/.github/actions/build_image/action.yml @@ -16,6 +16,9 @@ inputs: description: 'Use the latest pulp-ui when building the image' default: 'false' required: false + built_base_images: + description: 'A JSON list of the base-images that were freshly rebuilt prior' + required: true outputs: app_version: value: ${{ steps.image_version_branch.outputs.app_version }} @@ -23,6 +26,9 @@ outputs: app_branch: value: ${{ steps.image_version_branch.outputs.app_branch }} description: 'The pulpcore version branch that the built image matches' + rebuilt_images: + value: ${{ env.BUILD }} + description: 'true/false if the app image was rebuilt' runs: using: "composite" @@ -39,7 +45,7 @@ runs: shell: bash - name: Restore podman images from cache - uses: actions/cache/restore@v3 + uses: actions/cache/restore@v4 with: key: base-images=${{ inputs.image_cache_key }} path: base-images.tar.gz @@ -62,7 +68,42 @@ runs: fi shell: bash + - name: Set hash key + run: echo "VERSIONKEY=versions-${{ github.ref_name }}=${{ hashFiles(format('images/{0}/stable/**', inputs.image_name)) }}" >> $GITHUB_ENV + shell: bash + + - name: Restore last builds versions from cache + id: cache + uses: actions/cache/restore@v4 + with: + key: ${{ env.VERSIONKEY }} + path: versions.freeze + + - name: Check if rebuild is needed + run: | + # Rebuilds are needed for + # 1. CI is being ran in a PR or is a nightly run + # 2. Base images were rebuilt + # 3. New pulp versions was released + if [[ "${{ github.event_name }}" == "pull_request" || "${{ inputs.image_variant }}" == "nightly" || -n "${{ inputs.built_base_images }}" ]]; then + echo "BUILD=true" >> $GITHUB_ENV + else + if [[ "${{ steps.cache.outputs.cache-hit }}" == "false" ]]; then + echo "BUILD=true" >> $GITHUB_ENV + else + # Script returns non-zero (100) when new versions are available + if python .ci/scripts/check_up_to_date.py ${{ github.ref_name }} versions.freeze; then + echo "BUILD=false" >> $GITHUB_ENV + else + echo "BUILD=true" >> $GITHUB_ENV + fi + fi + fi + echo "Going to rebuild: ${BUILD}" + shell: bash + - name: Build images + if: env.BUILD == 'true' run: | podman version buildah version @@ -84,7 +125,8 @@ runs: id: image_version_branch run: | base_image=$(echo ${{ inputs.image_name }} | cut -d '-' -f1) - app_version=$(podman run --pull=never pulp/${{ inputs.image_name }}:ci-amd64 bash -c "pip3 show pulpcore | sed -n -e 's/Version: //p'") + podman run --pull=never pulp/${{ inputs.image_name }}:ci-amd64 bash -c "pip3 freeze | grep pulp" >> versions.freeze + app_version=$(grep pulpcore versions.freeze | sed -n -e 's/pulpcore==//p') app_branch=$(echo ${app_version} | grep -oP '\d+\.\d+') echo "APP_VERSION: ${app_version}" @@ -92,3 +134,17 @@ runs: echo "app_version=${app_version}" >> "$GITHUB_OUTPUT" echo "app_branch=${app_branch}" >> "$GITHUB_OUTPUT" shell: bash + + - name: Clear cache for next upload + if: env.BUILD == 'true' && steps.cache.outputs.cache-hit == 'true' && github.event_name != 'pull_request' + run: | + echo "Deleting existing cache for ${{ env.VERSIONKEY }}" + gh cache delete ${{ env.VERSIONKEY }} -R ${{ github.repository }} + shell: bash + + - name: Cache versions + if: env.BUILD == 'true' && github.event_name != 'pull_request' + uses: actions/cache/save@v4 + with: + key: ${{ env.VERSIONKEY }} + path: versions.freeze diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 29c2a321..df817d7f 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -83,6 +83,7 @@ jobs: image_variant: "stable" image_cache_key: ${{ needs.base-images.outputs.base_cache_key }} latest_ui: ${{ github.base_ref == 'latest' }} + built_base_images: ${{ needs.base-images.outputs.rebuilt_images }} - name: Test App Image uses: "./.github/actions/test_image" diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index b9e91031..251c958b 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -175,15 +175,18 @@ jobs: image_variant: ${{ matrix.image_variant }} image_cache_key: ${{ needs.base-images.outputs.base_cache_key }} latest_ui: ${{ github.ref_name == 'latest' }} + built_base_images: ${{ needs.base-images.outputs.rebuilt_images }} + - name: Test App Image - if: matrix.image_variant != 'nightly' + if: ${{ matrix.image_variant != 'nightly' && steps.build_image.outputs.rebuilt_images == 'true' }} uses: "./.github/actions/test_image" with: image_name: ${{ matrix.image_name }} app_branch: ${{ steps.build_image.outputs.app_branch }} - name: Set tags + if: ${{ steps.build_image.outputs.rebuilt_images == 'true' }} run: | base_image=$(echo ${{ matrix.image_name }} | cut -d '-' -f1) if [[ "${{ matrix.image_name }}" == "pulp" ]]; then @@ -211,6 +214,7 @@ jobs: echo "TAGS=${tags}" >> $GITHUB_ENV - name: Publish App Image + if: ${{ steps.build_image.outputs.rebuilt_images == 'true' }} uses: "./.github/actions/publish_images" with: image_names: ${{ env.IMAGES }}