diff --git a/.github/workflows/image.yml b/.github/workflows/image.yml index 5983d882c..513cf34ec 100644 --- a/.github/workflows/image.yml +++ b/.github/workflows/image.yml @@ -4,13 +4,18 @@ on: branches: - develop - 'release/**' - tags: - - 'v[0-9]+.[0-9]+.[0-9]+**' + workflow_call: + inputs: + tag: + description: 'Tag to be released (e.g. v4.2.0)' + required: true + type: string env: CARGO_TERM_COLOR: always CARGO_INCREMENTAL: 0 CI: 1 + TAG: ${{ inputs.tag }} jobs: image-build-push: @@ -32,5 +37,16 @@ jobs: with: username: ${{ secrets.DOCKERHUB_USERNAME }} password: ${{ secrets.DOCKERHUB_TOKEN }} + - name: Login to GHCR + uses: docker/login-action@v3 + with: + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} - name: Build and push the release images - run: ./scripts/release.sh + run: | + if [[ "${{ github.event_name }}" == "workflow_dispatch" ]]; then + ./scripts/release.sh --tag ${{ inputs.tag }} --registry ghcr.io/openebs/mayastor/dev + elif [[ "${{ github.event_name }}" == "push" ]]; then + ./scripts/release.sh + fi diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 000000000..3ea8231cf --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,71 @@ +name: Release Tagging Workflow + +on: + release: + types: [ created ] + +jobs: + preflight-checks: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + with: + fetch-depth: 0 + submodules: recursive + + - uses: cachix/install-nix-action@v31.3.0 + - name: Pre-populate nix-shell + run: | + export NIX_PATH=nixpkgs=$(jq '.nixpkgs.url' nix/sources.json -r) + echo "NIX_PATH=$NIX_PATH" >> $GITHUB_ENV + nix-shell --pure --run "echo" ./scripts/staging/shell.nix + + - name: Login to Docker Hub + uses: docker/login-action@v3 + with: + username: ${{ secrets.DOCKERHUB_USERNAME }} + password: ${{ secrets.DOCKERHUB_TOKEN }} + + - name: Validate if the release should be even made + run: | + nix-shell --pure --run "./scripts/staging/validate.sh \ + --tag ${{ github.ref_name }} \ + --type release" ./scripts/staging/shell.nix + + release-images: + runs-on: ubuntu-latest + needs: preflight-checks + steps: + - name: Checkout + uses: actions/checkout@v4 + with: + fetch-depth: 0 + submodules: recursive + + - uses: cachix/install-nix-action@v31.3.0 + - name: Pre-populate nix-shell + run: | + export NIX_PATH=nixpkgs=$(jq '.nixpkgs.url' nix/sources.json -r) + echo "NIX_PATH=$NIX_PATH" >> $GITHUB_ENV + nix-shell --pure --run "echo" ./scripts/staging/shell.nix + + - name: Login to Docker Hub + uses: docker/login-action@v3 + with: + username: ${{ secrets.DOCKERHUB_USERNAME }} + password: ${{ secrets.DOCKERHUB_TOKEN }} + + - name: Login to GHCR + uses: docker/login-action@v3 + with: + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Mirror images from dev to Docker Hub + run: | + nix-shell --pure --run "./scripts/staging/mirror-images.sh \ + --source ghcr.io/${{ github.repository_owner }}/dev \ + --target docker.io/${{ github.repository_owner }} \ + --tag ${{ github.ref_name }}" ./scripts/staging/shell.nix diff --git a/.github/workflows/staging.yml b/.github/workflows/staging.yml new file mode 100644 index 000000000..14586c4f4 --- /dev/null +++ b/.github/workflows/staging.yml @@ -0,0 +1,49 @@ +name: Staging Release + +on: + workflow_dispatch: + inputs: + tag: + description: 'Tag to be released (e.g. v4.2.0)' + required: true + type: string + +env: + TAG: ${{ inputs.tag }} + CI: 1 + +jobs: + preflight-checks: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + with: + fetch-depth: 0 + submodules: recursive + + - uses: cachix/install-nix-action@v31.3.0 + - name: Pre-populate nix-shell + run: | + export NIX_PATH=nixpkgs=$(jq '.nixpkgs.url' nix/sources.json -r) + echo "NIX_PATH=$NIX_PATH" >> $GITHUB_ENV + nix-shell --pure --run "echo" ./scripts/staging/shell.nix + + - name: Login to + uses: docker/login-action@v3 + with: + username: ${{ secrets.REGISTRY_USERNAME }} + password: ${{ secrets.REGISTRY_PASSWORD }} + + - name: Validate if the staging should be even made + run: | + nix-shell --pure --keep CI --run "./scripts/staging/validate.sh \ + --tag ${TAG} \ + --type staging" ./scripts/staging/shell.nix + + build-images: + uses: ./.github/workflows/image.yml + needs: preflight-checks + with: + tag: ${{ inputs.tag }} + secrets: inherit diff --git a/scripts/staging/log.sh b/scripts/staging/log.sh new file mode 100755 index 000000000..6c12e0ddf --- /dev/null +++ b/scripts/staging/log.sh @@ -0,0 +1,25 @@ +#!/usr/bin/env bash + +# Write output to error output stream. +log_to_stderr() { + echo -e "${1}" >&2 +} + +log_error() { + log_to_stderr "ERROR: $1" +} + +log_warn() { + log_to_stderr "WARNING: $1" +} + +# Exit with error status and print error. +log_fatal() { + local -r _return="${2:-1}" + log_error "$1" + exit "${_return}" +} + +log() { + echo -e "${1}" +} \ No newline at end of file diff --git a/scripts/staging/mirror-images.sh b/scripts/staging/mirror-images.sh new file mode 100755 index 000000000..6db22fe11 --- /dev/null +++ b/scripts/staging/mirror-images.sh @@ -0,0 +1,61 @@ +#!/usr/bin/env bash + +# Mirror container images from source registry to target registry using crane. +# Preserves multi-platform support and image digests. + +set -euo pipefail + +SCRIPT_DIR="$(dirname "$(realpath "${BASH_SOURCE[0]:-"$0"}")")" +ROOT_DIR="${SCRIPT_DIR}/../.." + +# if PARENT_ROOT_DIR is not defined use the one below +: "${PARENT_ROOT_DIR:=$ROOT_DIR}" + +source "$PARENT_ROOT_DIR/scripts/utils/log.sh" +NO_RUN=true . "$PARENT_ROOT_DIR/scripts/release.sh" + +IMAGES=() +for name in $DEFAULT_IMAGES; do + image=$($NIX_EVAL -f "$PARENT_ROOT_DIR" "images.$BUILD_TYPE.$name.imageName" --raw --quiet --argstr product_prefix "$PRODUCT_PREFIX") + IMAGES+=("${image##*/}") +done + +SOURCE="" +TARGET="" +TAG="" + +while [[ $# -gt 0 ]]; do + case $1 in + --source) + SOURCE="$2" + shift 2 + ;; + --target) + TARGET="$2" + shift 2 + ;; + --tag) + TAG="$2" + shift 2 + ;; + *) + log_fatal "Unknown option: $1" + ;; + esac +done + +if [[ -z "$SOURCE" ]] || [[ -z "$TARGET" ]] || [[ -z "$TAG" ]]; then + log_fatal "Usage: $0 --source --target --tag " +fi + +echo "Mirroring images from ${SOURCE} to ${TARGET} with tag ${TAG}" + +for IMAGE in "${IMAGES[@]}"; do + echo "Mirroring ${IMAGE}:${TAG}..." + + SRC="${SOURCE}/${IMAGE}:${TAG}" + DEST="${TARGET}/${IMAGE}:${TAG}" + crane copy --platform all "${SRC}" "${DEST}" + + echo "✓ Successfully mirrored ${IMAGE}:${TAG}" +done diff --git a/scripts/staging/shell.nix b/scripts/staging/shell.nix new file mode 100755 index 000000000..2a4549e31 --- /dev/null +++ b/scripts/staging/shell.nix @@ -0,0 +1,20 @@ +{ pkgs ? import (import ../../nix/sources.nix).nixpkgs { + overlays = [ (_: _: { inherit (import ../../nix/sources.nix); }) (import ../../nix/overlay.nix { }) ]; + } +}: +pkgs.mkShellNoCC { + name = "staging-shell"; + buildInputs = with pkgs; [ + oras + crane + yq-go + jq + ] ++ pkgs.lib.optional (builtins.getEnv "IN_NIX_SHELL" == "pure" && pkgs.system != "aarch64-darwin") [ + docker + git + curl + nix + kubernetes-helm-wrapped + cacert + ]; +} diff --git a/scripts/staging/validate.sh b/scripts/staging/validate.sh new file mode 100755 index 000000000..aefdbbb23 --- /dev/null +++ b/scripts/staging/validate.sh @@ -0,0 +1,105 @@ +#!/usr/bin/env bash +set -euo pipefail + +SCRIPT_DIR="$(dirname "$(realpath "${BASH_SOURCE[0]:-"$0"}")")" +ROOT_DIR="${SCRIPT_DIR}/../.." +: "${PARENT_ROOT_DIR:=$ROOT_DIR}" + +source "$PARENT_ROOT_DIR/scripts/staging/log.sh" + +# --- Run release.sh in subshell so it resolves SOURCE_REL correctly --- +( + export NO_RUN=true + export CI=1 # avoid unbound variable errors and skip submodule update in CI + echo "📦 Running release.sh from: $PARENT_ROOT_DIR/scripts/release.sh" + bash "$PARENT_ROOT_DIR/scripts/release.sh" +) + + +DOCKERHUB_ORG="${DOCKERHUB_ORG:-openebs}" +IMAGE_REGISTRY="${IMAGE_REGISTRY:-docker.io}" + +TRIGGER="" +TAG="" + +while [[ $# -gt 0 ]]; do + case $1 in + --trigger|--type) TRIGGER="$2"; shift 2 ;; + --tag) TAG="$2"; shift 2 ;; + -h|--help) + cat < [--tag ] +Options: + --trigger release, staging, develop, prerelease + --type Alias for --trigger + --tag Release tag (e.g., v2.9.0) +EOF + exit 0 ;; + *) log_fatal "Unknown option $1" ;; + esac +done + +echo "Validating trigger: $TRIGGER" + +case "$TRIGGER" in + release|staging|develop|prerelease) + echo "✅ Valid trigger: $TRIGGER" + ;; + *) + log_fatal "❌ Error: Invalid trigger '$TRIGGER'." + ;; +esac + +echo "Validating tag: $TAG" + +case "$TRIGGER" in + release|staging) + [[ "$TAG" =~ ^v[0-9]+\.[0-9]+\.[0-9]+(-rc\.[0-9]+)?$ ]] \ + || log_fatal "❌ Tag must be in format vX.Y.Z or vX.Y.Z-rc.N" + ;; + develop) + [[ "$TAG" =~ ^v[0-9]+\.[0-9]+\.[0-9]+-develop$ ]] \ + || log_fatal "❌ Tag must be in format vX.Y.Z-develop" + ;; + prerelease) + [[ "$TAG" == "v0.0.0" ]] \ + || log_fatal "❌ For prerelease builds, tag must be exactly v0.0.0" + ;; +esac + +echo "✅ Tag validation passed" + +# --- Image Validation Section --- +VERSION="${TAG#v}" + +dockerhub_tag_exists() { + local repository="$1" tag="$2" + curl --silent -f -lSL "https://hub.docker.com/v2/repositories/${repository#docker.io/}/tags/${tag}" >/dev/null 2>&1 +} + +check_images() { + if [[ -n "${DEFAULT_IMAGES:-}" ]]; then + for name in $DEFAULT_IMAGES; do + image=$($NIX_EVAL -f "$PARENT_ROOT_DIR" "images.$BUILD_TYPE.$name.imageName" --raw --quiet --argstr product_prefix "$PRODUCT_PREFIX") + image_name="${image##*/}" + if dockerhub_tag_exists "${DOCKERHUB_ORG}/${image_name}" "${TAG}"; then + log_fatal "❌ Image ${DOCKERHUB_ORG}/${image_name}:${TAG} already exists" + else + echo "✅ Image ${DOCKERHUB_ORG}/${image_name}:${TAG} does not exist" + fi + done + else + echo "⚠️ No DEFAULT_IMAGES defined — skipping image existence checks." + fi +} + +case "$TRIGGER" in + staging|release) + check_images + ;; + develop|prerelease) + echo "Skipping image checks for $TRIGGER" + ;; +esac + +echo "✅ All validations completed successfully"