diff --git a/stack/.github/workflows/push-image.yml b/stack/.github/workflows/push-image.yml index fd8b4ec6..145ee793 100644 --- a/stack/.github/workflows/push-image.yml +++ b/stack/.github/workflows/push-image.yml @@ -4,54 +4,85 @@ on: release: types: - published + + workflow_dispatch: + inputs: + version: + description: 'Version of the stack to push' + required: false + env: - REGISTRIES_FILENAME: "registries.json" + REGISTRIES_FILEPATH: "registries.json" + GCR_REGISTRY: "gcr.io" + GCR_USERNAME: "_json_key" jobs: preparation: name: Preparation - runs-on: ubuntu-latest + runs-on: ubuntu-22.04 outputs: matrix: ${{ steps.set-matrix.outputs.matrix }} - DOCKERHUB_ORG: ${{ steps.set-dockerhub-org-namespace.outputs.DOCKERHUB_ORG}} - push_to_gcr: ${{ steps.parse_configs.outputs.push_to_gcr}} - push_to_dockerhub: ${{ steps.parse_configs.outputs.push_to_dockerhub}} + DOCKERHUB_ORG: ${{ steps.set-dockerhub-org-namespace.outputs.DOCKERHUB_ORG }} + push_to_gcr: ${{ steps.parse_configs.outputs.push_to_gcr }} + push_to_dockerhub: ${{ steps.parse_configs.outputs.push_to_dockerhub }} + tag: ${{ steps.event.outputs.tag }} + repo_name: ${{ steps.registry-repo.outputs.repo_name }} steps: - name: Checkout uses: actions/checkout@v4 - - name: Set matrix - id: set-matrix + - name: Parse Event + id: event run: | - release_version="$(jq -r '.release.tag_name' "${GITHUB_EVENT_PATH}" | sed s/^v//)" + set -euo pipefail + shopt -s inherit_errexit + + # If the workflow has been triggered from dispatch event + if [[ "${{ github.event_name }}" == "workflow_dispatch" ]]; then + echo "tag=${{ github.event.inputs.version }}" >> "$GITHUB_OUTPUT" + else #The workflow has been triggered from publish event + echo "tag=$(jq -r '.release.tag_name' "${GITHUB_EVENT_PATH}" | sed s/^v//)" >> "$GITHUB_OUTPUT" + fi + - name: Get Registry Repo Name + id: registry-repo + run: | # Strip off the org and slash from repo name # paketo-buildpacks/repo-name --> repo-name - repo_name=$(echo "${{ github.repository }}" | sed 's/^.*\///') + echo "repo_name=$(echo "${{ github.repository }}" | sed 's/^.*\///')" >> "$GITHUB_OUTPUT" + - name: Set matrix + id: set-matrix + run: | + release_version="${{ steps.event.outputs.tag }}" + repo_name="${{ steps.registry-repo.outputs.repo_name }}" + release_info=$(curl -s "https://api.github.com/repos/${{ github.repository }}/releases/tags/v${release_version}") asset_prefix="${repo_name}-${release_version}-" - oci_images=$(jq -c --arg asset_prefix "$asset_prefix" '[.release.assets[].name | select(endswith(".oci")) | split(".oci") | .[0] | split($asset_prefix) | .[1]]' "${GITHUB_EVENT_PATH}") + oci_images=$(echo $release_info | jq -c --arg asset_prefix "$asset_prefix" '[ .assets[] | select(.name | endswith(".oci")) | {name: (.name | split(".oci") | .[0] | split($asset_prefix) | .[1]), url}]') + printf "matrix=%s\n" "${oci_images}" printf "matrix=%s\n" "${oci_images}" >> "$GITHUB_OUTPUT" - name: Set DOCKERHUB_ORG namespace id: set-dockerhub-org-namespace - run: echo "DOCKERHUB_ORG=${GITHUB_REPOSITORY_OWNER//-/}" >> "$GITHUB_OUTPUT" + run: | + echo "DOCKERHUB_ORG=${GITHUB_REPOSITORY_OWNER//-/}" >> "$GITHUB_OUTPUT" - name: Parse Configs id: parse_configs - run: | - registries_filename="${{ env.REGISTRIES_FILENAME }}" + registries_filepath="${{ env.REGISTRIES_FILEPATH }}" push_to_dockerhub=true push_to_gcr=true - if [[ -f $registries_filename ]]; then - if jq 'has("dockerhub")' $registries_filename > /dev/null; then - push_to_dockerhub=$(jq '.dockerhub' $registries_filename) + if [[ -f $registries_filepath ]]; then + + if jq 'has("dockerhub")' $registries_filepath > /dev/null; then + push_to_dockerhub=$(jq '.dockerhub' $registries_filepath) fi - if jq 'has("GCR")' $registries_filename > /dev/null; then - push_to_gcr=$(jq '.GCR' $registries_filename) + + if jq 'has("GCR")' $registries_filepath > /dev/null; then + push_to_gcr=$(jq '.GCR' $registries_filepath) fi fi @@ -68,50 +99,66 @@ jobs: oci_image: ${{ fromJSON(needs.preparation.outputs.matrix) }} steps: - - name: Parse Event - id: event - run: | - echo "tag=$(jq -r '.release.tag_name' "${GITHUB_EVENT_PATH}" | sed s/^v//)" >> "$GITHUB_OUTPUT" - echo "${{ matrix.oci_image }}_download_url=$(jq -r '.release.assets[] | select(.name | endswith("${{ matrix.oci_image }}.oci")) | .url' "${GITHUB_EVENT_PATH}")" >> "$GITHUB_OUTPUT" - - name: Checkout uses: actions/checkout@v4 - - name: Download ${{ matrix.oci_image }} Image + - name: Set up QEMU + uses: docker/setup-qemu-action@v3 + + - name: Set up buildx + uses: docker/setup-buildx-action@v3 + + - name: Download ${{ matrix.oci_image.name }} Image uses: paketo-buildpacks/github-config/actions/release/download-asset@main with: - url: ${{ steps.event.outputs[format('{0}_download_url', matrix.oci_image)] }} - output: "/github/workspace/${{ matrix.oci_image }}.oci" + url: "${{ matrix.oci_image.url }}" + output: "./${{ matrix.oci_image.name }}.oci" token: ${{ secrets.PAKETO_BOT_GITHUB_TOKEN }} - - name: Get Registry Repo Name - id: registry-repo - run: | - # Strip off the Github org prefix and 'stack' suffix from repo name - # paketo-buildpacks/some-name-stack --> some-name - echo "name=$(echo "${{ github.repository }}" | sed 's/^.*\///' | sed 's/\-stack$//')" >> "$GITHUB_OUTPUT" + - name: Docker login docker.io + uses: docker/login-action@v3 + if: ${{ needs.preparation.outputs.push_to_dockerhub == 'true' }} + with: + username: ${{ secrets.PAKETO_BUILDPACKS_DOCKERHUB_USERNAME }} + password: ${{ secrets.PAKETO_BUILDPACKS_DOCKERHUB_PASSWORD }} + registry: docker.io + + - name: Docker login gcr.io + uses: docker/login-action@v3 + if: ${{ needs.preparation.outputs.push_to_gcr == 'true' }} + with: + username: ${{ env.GCR_USERNAME }} + password: ${{ secrets.GCR_PUSH_BOT_JSON_KEY }} + registry: ${{ env.GCR_REGISTRY }} - - name: Push ${{ matrix.oci_image }} Image to DockerHub + - name: Push ${{ matrix.oci_image.name }} Image to registries id: push env: - DOCKERHUB_USERNAME: ${{ secrets.PAKETO_BUILDPACKS_DOCKERHUB_USERNAME }} - DOCKERHUB_PASSWORD: ${{ secrets.PAKETO_BUILDPACKS_DOCKERHUB_PASSWORD }} DOCKERHUB_ORG: "${{ needs.preparation.outputs.DOCKERHUB_ORG }}" - GCR_USERNAME: _json_key - GCR_PASSWORD: ${{ secrets.GCR_PUSH_BOT_JSON_KEY }} GCR_PROJECT: "${{ github.repository_owner }}" run: | - - if [ "${{ needs.preparation.outputs.push_to_dockerhub }}" == "true" ]; then - echo "${DOCKERHUB_PASSWORD}" | sudo skopeo login --username "${DOCKERHUB_USERNAME}" --password-stdin index.docker.io - sudo skopeo copy "oci-archive:${GITHUB_WORKSPACE}/${{ matrix.oci_image }}.oci" "docker://${DOCKERHUB_ORG}/${{ matrix.oci_image }}-${{ steps.registry-repo.outputs.name }}:${{ steps.event.outputs.tag }}" - sudo skopeo copy "oci-archive:${GITHUB_WORKSPACE}/${{ matrix.oci_image }}.oci" "docker://${DOCKERHUB_ORG}/${{ matrix.oci_image }}-${{ steps.registry-repo.outputs.name }}:latest" - fi - - if [ "${{ needs.preparation.outputs.push_to_gcr }}" == "true" ]; then - echo "${GCR_PASSWORD}" | sudo skopeo login --username "${GCR_USERNAME}" --password-stdin gcr.io - sudo skopeo copy "oci-archive:${GITHUB_WORKSPACE}/${{ matrix.oci_image }}.oci" "docker://gcr.io/${GCR_PROJECT}/${{ matrix.oci_image }}-${{ steps.registry-repo.outputs.name }}:${{ steps.event.outputs.tag }}" - sudo skopeo copy "oci-archive:${GITHUB_WORKSPACE}/${{ matrix.oci_image }}.oci" "docker://gcr.io/${GCR_PROJECT}/${{ matrix.oci_image }}-${{ steps.registry-repo.outputs.name }}:latest" + # Ensure other scripts can access the .bin directory to install their own + # tools after we install them as whatever user we are. + mkdir -p ./.bin/ + chmod 777 ./.bin/ + + ./scripts/publish.sh \ + --image-ref "docker.io/${DOCKERHUB_ORG}/${{ matrix.oci_image.name }}-${{ needs.preparation.outputs.repo_name }}:${{ needs.preparation.outputs.tag }}" \ + --image-ref "docker.io/${DOCKERHUB_ORG}/${{ matrix.oci_image.name }}-${{ needs.preparation.outputs.repo_name }}:latest" \ + --image-archive "./${{ matrix.oci_image.name }}.oci" + + if [ "${{ needs.preparation.outputs.push_to_gcr }}" = "true" ]; then + + platforms=$(docker manifest inspect "docker.io/${DOCKERHUB_ORG}/${{ matrix.oci_image.name }}-${{ needs.preparation.outputs.repo_name }}:${{ needs.preparation.outputs.tag }}" | + jq -r '[.manifests[].platform] | [.[] | .os + "/" + .architecture] | join(",")') + + echo "FROM docker.io/${DOCKERHUB_ORG}/${{ matrix.oci_image.name }}-${{ needs.preparation.outputs.repo_name }}:${{ needs.preparation.outputs.tag }}" | \ + docker buildx build -f - . \ + --tag "${{ env.GCR_REGISTRY }}/${GCR_PROJECT}/${{ matrix.oci_image.name }}-${{ needs.preparation.outputs.repo_name }}:${{ needs.preparation.outputs.tag }}" \ + --tag "${{ env.GCR_REGISTRY }}/${GCR_PROJECT}/${{ matrix.oci_image.name }}-${{ needs.preparation.outputs.repo_name }}:latest" \ + --platform "$platforms" \ + --provenance=false \ + --push fi # If the repository name contains 'bionic', let's push it to legacy image locations as well: # paketobuildpacks/{build/run}:{version}-{variant} @@ -125,12 +172,12 @@ jobs: # bionic-tiny --> tiny variant="${registry_repo#bionic-}" - sudo skopeo copy "oci-archive:${GITHUB_WORKSPACE}/${{ matrix.oci_image }}.oci" "docker://${DOCKERHUB_ORG}/${{ matrix.oci_image }}:${{ steps.event.outputs.tag }}-${variant}" - sudo skopeo copy "oci-archive:${GITHUB_WORKSPACE}/${{ matrix.oci_image }}.oci" "docker://${DOCKERHUB_ORG}/${{ matrix.oci_image }}:${{ steps.event.outputs.tag }}-${variant}-cnb" - sudo skopeo copy "oci-archive:${GITHUB_WORKSPACE}/${{ matrix.oci_image }}.oci" "docker://${DOCKERHUB_ORG}/${{ matrix.oci_image }}:${variant}-cnb" - sudo skopeo copy "oci-archive:${GITHUB_WORKSPACE}/${{ matrix.oci_image }}.oci" "docker://${DOCKERHUB_ORG}/${{ matrix.oci_image }}:${variant}" + sudo skopeo copy "oci-archive:./${{ matrix.oci_image.name }}.oci" "docker://${DOCKERHUB_ORG}/${{ matrix.oci_image.name }}:${{ steps.event.outputs.tag }}-${variant}" + sudo skopeo copy "oci-archive:./${{ matrix.oci_image.name }}.oci" "docker://${DOCKERHUB_ORG}/${{ matrix.oci_image.name }}:${{ steps.event.outputs.tag }}-${variant}-cnb" + sudo skopeo copy "oci-archive:./${{ matrix.oci_image.name }}.oci" "docker://${DOCKERHUB_ORG}/${{ matrix.oci_image.name }}:${variant}-cnb" + sudo skopeo copy "oci-archive:./${{ matrix.oci_image.name }}.oci" "docker://${DOCKERHUB_ORG}/${{ matrix.oci_image.name }}:${variant}" - sudo skopeo copy "docker://${DOCKERHUB_ORG}/${{ matrix.oci_image }}:${variant}-cnb" "docker://gcr.io/${GCR_PROJECT}/${{ matrix.oci_image }}:${variant}-cnb" + sudo skopeo copy "docker://${DOCKERHUB_ORG}/${{ matrix.oci_image.name }}:${variant}-cnb" "docker://gcr.io/${GCR_PROJECT}/${{ matrix.oci_image.name }}:${variant}-cnb" fi failure: diff --git a/stack/scripts/publish.sh b/stack/scripts/publish.sh new file mode 100755 index 00000000..44481a5a --- /dev/null +++ b/stack/scripts/publish.sh @@ -0,0 +1,194 @@ +#!/usr/bin/env bash + +set -eu +set -o pipefail + +readonly PROG_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +readonly ROOT_DIR="$(cd "${PROG_DIR}/.." && pwd)" +readonly BIN_DIR="${ROOT_DIR}/.bin" + +# shellcheck source=SCRIPTDIR/.util/tools.sh +source "${PROG_DIR}/.util/tools.sh" + +# shellcheck source=SCRIPTDIR/.util/print.sh +source "${PROG_DIR}/.util/print.sh" + +if [[ $BASH_VERSINFO -lt 4 ]]; then + util::print::error "Before running this script please update Bash to v4 or higher (e.g. on OSX: \$ brew install bash)" +fi + +function main() { + local build_ref=() + local run_ref=() + local image_ref=() + local build_archive="" + local run_archive="" + local image_archive="" + + while [[ "${#}" != 0 ]]; do + case "${1}" in + --help|-h) + shift 1 + usage + exit 0 + ;; + + --build-ref) + build_ref+=("${2}") + shift 2 + ;; + + --run-ref) + run_ref+=("${2}") + shift 2 + ;; + + --build-archive) + build_archive=${2} + shift 2 + ;; + + --run-archive) + run_archive=${2} + shift 2 + ;; + + --image-ref) + image_ref+=("${2}") + shift 2 + ;; + + --image-archive) + image_archive=${2} + shift 2 + ;; + + "") + # skip if the argument is empty + shift 1 + ;; + + *) + util::print::error "unknown argument \"${1}\"" + esac + done + + if [[ ${#image_ref[@]} != 0 || -n "$image_archive" ]]; then + if ((${#image_ref[@]} == 0)); then + util::print::error "--image-ref is required [Example: docker.io/paketobuildpacks/foo:latest]" + fi + + if [ -z "$image_archive" ]; then + util::print::error "--image-archive is required [Example: ./path/to/image.oci]" + fi + else + if ((${#build_ref[@]} == 0)); then + util::print::error "--build-ref is required [Example: docker.io/paketobuildpacks/foo:latest]" + fi + + if ((${#run_ref[@]} == 0)); then + util::print::error "--run-ref is required [Example: gcr.iopaketo-buildpacks/foo:1.0.0]" + fi + + if ((${#run_ref[@]} != ${#build_ref[@]})); then + util::print::error "must have the same number of --build-ref and --run-ref arguments" + fi + + if [ -z "$build_archive" ]; then + util::print::error "--build-archive is required [Example: ./path/to/build.oci]" + fi + + if [ -z "$run_archive" ]; then + util::print::error "--run-archive is required [Example: ./path/to/run.oci]" + fi + fi + + tools::install + + if [[ ${#image_ref[@]} != 0 || -n "$image_archive" ]]; then + stack::publish::image \ + "$image_archive" \ + "${#image_ref[@]}" \ + "${image_ref[@]}" + else + stack::publish \ + "$build_archive" \ + "$run_archive" \ + "${#build_ref[@]}" \ + "${build_ref[@]}" \ + "${#run_ref[@]}" \ + "${run_ref[@]}" + fi +} + +function usage() { + cat <<-USAGE +publish.sh [OPTIONS] + +Publishes the stack using the existing OCI image archives. + +OPTIONS + --build-ref list of build references to publish to [Required if --image-ref is not provided] + --run-ref list of run references to publish to [Required if --image-ref is not provided] + --build-archive path to the build OCI archive file [Required if --image-ref is not provided] + --run-archive path to the run OCI archive file [Required if --image-ref is not provided] + --image-ref list of image references to publish to [Required if --build-ref and --run-ref are not provided] + --image-archive path to the image OCI archive file [Required if --build-ref and --run-ref are not provided] + --help -h prints the command usage +USAGE +} + +function tools::install() { + util::tools::jam::install \ + --directory "${BIN_DIR}" +} + +function stack::publish() { + local build_archive="$1" + local run_archive="$2" + + # bash can't easily pass arrays, they all get merged into one list of arguments + # so we pass the lengths & extract the arrays from the single argument list + local build_ref_len="$3" # length of build ref array + local build_ref=("${@:4:$build_ref_len}") # pull out build_ref array + local run_len_slot=$(( 4 + build_ref_len)) # location of run_ref length + local run_ref_len="${*:$run_len_slot:1}" # length of run ref array + local run_ref_slot=$(( 1 + run_len_slot)) # location of run_ref array + local run_ref=("${@:$run_ref_slot:$run_ref_len}") # pull out run_ref array + + # iterate over build_ref & run_ref, they will be the same length + local len=${#build_ref[@]} + for (( i=0; i