diff --git a/.github/workflows/oci-marketplace-publish.yml b/.github/workflows/oci-marketplace-publish.yml new file mode 100644 index 0000000..bd17ef1 --- /dev/null +++ b/.github/workflows/oci-marketplace-publish.yml @@ -0,0 +1,959 @@ +name: OCI Image to Marketplace release + +on: + workflow_dispatch: + inputs: + + image_url: + description: "URL to publicly accessible qcow2 file" + required: true + default: '' + + release_to_marketplace: + description: "Release the image to Marketplace listing" + required: true + type: boolean + default: true + + notify_mattermost: + description: "Send notification to Mattermost" + required: true + type: boolean + default: false + +env: + OCI_PUBLISHER_BASE_URL: https://cloud.oracle.com/publisher + OCI_COMPUTE_BASE_URL: https://cloud.oracle.com/compute + +jobs: + release-image-to-marketplace: + name: "Release image to OCI Marketplace" + runs-on: ubuntu-24.04 + env: + OCI_CLI_USER: ${{ secrets.OCI_CLI_USER }} + OCI_CLI_TENANCY: ${{ secrets.OCI_CLI_TENANCY }} + OCI_CLI_FINGERPRINT: ${{ secrets.OCI_CLI_FINGERPRINT }} + OCI_CLI_KEY_CONTENT: ${{ secrets.OCI_CLI_KEY_CONTENT }} + OCI_CLI_REGION: ${{ vars.OCI_CLI_REGION }} + + steps: + - uses: actions/checkout@v4 + + - name: Install OCI CLI and dependencies + run: | + # Install jq for JSON parsing + sudo apt-get update && sudo apt-get install -y jq + + # Install OCI CLI + curl -L -O https://raw.githubusercontent.com/oracle/oci-cli/master/scripts/install/install.sh + chmod +x install.sh + ./install.sh --accept-all-defaults + echo "$HOME/bin" >> $GITHUB_PATH + + - name: Configure OCI CLI + run: | + # Configure OCI CLI using environment variables + mkdir -p ~/.oci + echo "${{ secrets.OCI_CLI_KEY_CONTENT }}" > ~/.oci/key.pem + chmod 600 ~/.oci/key.pem + + # Verify OCI CLI is working + oci --version + + - name: Parse image URL and filename + id: parse-image + run: | + # Extract filename and path from URL + IMAGE_URL="${{ inputs.image_url }}" + IMAGE_FILENAME=$(basename "${IMAGE_URL}") + IMAGE_PATH=$(dirname "${IMAGE_URL}") + echo "IMAGE_FILENAME=${IMAGE_FILENAME}" >> $GITHUB_ENV + + # Parse filename: AlmaLinux-8-OCI-8.10-20260202.x86_64.qcow2 + # or: AlmaLinux-10-OCI-10.1-20260216.0.aarch64.qcow2 + # Pattern: AlmaLinux-{major}-OCI-{version}-{date}.{arch}.qcow2 + + if [[ ! "${IMAGE_FILENAME}" =~ AlmaLinux-([0-9]+)-OCI-([0-9.]+)-([0-9]+)((\.[0-9]+)?)\.(x86_64|aarch64)\.qcow2 ]]; then + echo "[Error] Invalid image filename format: '${IMAGE_FILENAME}'" + echo "Expected format: AlmaLinux-{major}-OCI-{version}-{date}.{arch}.qcow2" + echo "Example: AlmaLinux-8-OCI-8.10-20260202.x86_64.qcow2" + echo "Example: AlmaLinux-10-OCI-10.1-20260216.0.aarch64.qcow2" + exit 1 + fi + + ALMA_MAJOR="${BASH_REMATCH[1]}" + ALMA_VERSION="${BASH_REMATCH[2]}" + ALMA_DATE="${BASH_REMATCH[3]}${BASH_REMATCH[4]}" + ALMA_ARCH="${BASH_REMATCH[6]}" + + # Extract ALMA_RELEASE from URL path + # URL structure: .../images/{major}/{version}/oci/{release}/{filename} + # Example: .../images/8/8.10/oci/20260202092731/AlmaLinux-8-OCI-8.10-20260202.x86_64.qcow2 + + if [[ "${IMAGE_PATH}" =~ /oci/([0-9]+)$ ]]; then + ALMA_RELEASE="${BASH_REMATCH[1]}" + else + echo "[Error] Could not extract release timestamp from URL path: '${IMAGE_PATH}'" + echo "Expected URL format: .../images/{major}/{version}/oci/{release}/{filename}" + exit 1 + fi + + # AlmaLinux code name + case "${ALMA_VERSION}" in + 8.10) ALMA_CODE_NAME="Cerulean Leopard" ;; + 9.7) ALMA_CODE_NAME="Moss Jungle Cat" ;; + 10.1) ALMA_CODE_NAME="Heliotrope Lion" ;; + *) echo "[Error] Unsupported AlmaLinux version: '${ALMA_VERSION}'"; exit 1 ;; + esac + + echo "[Debug] Parsed metadata:" + echo " Major Version: ${ALMA_MAJOR}" + echo " Version: ${ALMA_VERSION}" + echo " Code Name: ${ALMA_CODE_NAME}" + echo " Release: ${ALMA_RELEASE}" + echo " Date: ${ALMA_DATE}" + echo " Architecture: ${ALMA_ARCH}" + + # Convert aarch64 to AArch64 for display + DISPLAY_ARCH="${ALMA_ARCH}" + [[ "${ALMA_ARCH}" == "aarch64" ]] && DISPLAY_ARCH="AArch64" + + echo "ALMA_MAJOR=${ALMA_MAJOR}" >> $GITHUB_ENV + echo "ALMA_VERSION=${ALMA_VERSION}" >> $GITHUB_ENV + echo "ALMA_CODE_NAME=${ALMA_CODE_NAME}" >> $GITHUB_ENV + echo "ALMA_RELEASE=${ALMA_RELEASE}" >> $GITHUB_ENV + echo "ALMA_DATE=${ALMA_DATE}" >> $GITHUB_ENV + echo "ALMA_ARCH=${ALMA_ARCH}" >> $GITHUB_ENV + echo "DISPLAY_ARCH=${DISPLAY_ARCH}" >> $GITHUB_ENV + echo "IMAGE_DISPLAY_NAME=AlmaLinux OS ${ALMA_VERSION} ${ALMA_ARCH} \`${ALMA_RELEASE}\`" >> $GITHUB_ENV + echo "CUSTOM_IMAGE_NAME=${IMAGE_FILENAME%.*}" >> $GITHUB_ENV + + - name: Download qcow2 image + run: | + # Download the qcow2 file from provided URL + echo "[Debug] Downloading image from: ${{ inputs.image_url }}" + curl --fail -s "${{ inputs.image_url }}" -o "${{ env.IMAGE_FILENAME }}" + + # Verify download + if [[ ! -f "${{ env.IMAGE_FILENAME }}" ]]; then + echo "[Error] Failed to download image" + exit 1 + fi + + # Check the image is QCOW format + if ! file "${{ env.IMAGE_FILENAME }}" | grep -q -i "qcow"; then + echo "[Error] Image is not in QCOW format" + exit 1 + fi + + - name: Upload to OCI Object Storage + run: | + # Upload qcow2 to OCI Object Storage + BUCKET_NAME="${{ vars.OCI_OBJECT_STORAGE_BUCKET }}" + NAMESPACE="${{ secrets.OCI_OBJECT_STORAGE_NAMESPACE }}" + OBJECT_NAME="images/${{ env.ALMA_MAJOR }}/${{ env.ALMA_VERSION }}/oci/${{ env.ALMA_RELEASE }}/${{ env.IMAGE_FILENAME }}" + + echo "[Debug] Uploading to Object Storage:" + echo " Namespace: ${NAMESPACE}" + echo " Bucket: ${BUCKET_NAME}" + echo " Object: ${OBJECT_NAME}" + + oci os object put \ + --bucket-name "${BUCKET_NAME}" \ + --namespace "${NAMESPACE}" \ + --file "${{ env.IMAGE_FILENAME }}" \ + --name "${OBJECT_NAME}" \ + --force + + echo "OBJECT_NAME=${OBJECT_NAME}" >> $GITHUB_ENV + echo "[Debug] Upload completed successfully" + + - name: Import as OCI Compute Image + id: import-image + run: | + # Import qcow2 from Object Storage as OCI Compute custom image + COMPARTMENT_ID="${{ secrets.OCI_COMPARTMENT_ID }}" + BUCKET_NAME="${{ vars.OCI_OBJECT_STORAGE_BUCKET }}" + NAMESPACE="${{ secrets.OCI_OBJECT_STORAGE_NAMESPACE }}" + + echo "[Debug] Importing compute image:" + echo " Custom Image Name: ${{ env.CUSTOM_IMAGE_NAME }}" + echo " Compartment: ${COMPARTMENT_ID:0:20}..." + echo " Bucket: ${BUCKET_NAME}" + echo " Namespace: ${NAMESPACE}" + + # Verify object exists in Object Storage before import + echo "[Debug] Verifying object in Object Storage..." + oci os object head \ + --bucket-name "${BUCKET_NAME}" \ + --namespace "${NAMESPACE}" \ + --name "${{ env.OBJECT_NAME }}" || { + echo "[Error] Object not found in Object Storage" + exit 1 + } + + # Start image import (launch options will be set after import) + echo "[Debug] Starting image import..." + set +e # Don't exit on error, capture it + IMPORT_OUTPUT=$(oci compute image import from-object \ + --compartment-id "${COMPARTMENT_ID}" \ + --display-name "${{ env.CUSTOM_IMAGE_NAME }}" \ + --namespace "${NAMESPACE}" \ + --bucket-name "${BUCKET_NAME}" \ + --name "${{ env.OBJECT_NAME }}" \ + --source-image-type QCOW2 \ + --launch-mode PARAVIRTUALIZED \ + --operating-system "AlmaLinux" \ + --operating-system-version "${{ env.ALMA_VERSION }}" \ + 2>&1) + IMPORT_EXIT_CODE=$? + set -e # Re-enable exit on error + + echo "[Debug] Import command exit code: ${IMPORT_EXIT_CODE}" + echo "[Debug] Import response:" + echo "${IMPORT_OUTPUT}" + + if [ ${IMPORT_EXIT_CODE} -ne 0 ]; then + echo "[Error] ❌ Image import failed with exit code ${IMPORT_EXIT_CODE}" + echo "[Error] Error details:" + echo "${IMPORT_OUTPUT}" + echo "" + echo "[Debug] Troubleshooting information:" + echo " - Compartment ID: ${COMPARTMENT_ID}" + echo " - Display Name: ${{ env.CUSTOM_IMAGE_NAME }}" + echo " - Namespace: ${NAMESPACE}" + echo " - Bucket: ${BUCKET_NAME}" + echo " - Object: ${{ env.OBJECT_NAME }}" + exit 1 + fi + + # Extract image OCID from JSON output using jq (more reliable than grep) + IMAGE_OCID=$(echo "${IMPORT_OUTPUT}" | jq -r '.data.id // empty') + + if [[ -z "${IMAGE_OCID}" ]]; then + echo "[Error] Failed to extract image OCID from import output" + echo "[Error] Full output was:" + echo "${IMPORT_OUTPUT}" + exit 1 + fi + + echo "IMAGE_OCID=${IMAGE_OCID}" >> $GITHUB_ENV + echo "[Debug] Image OCID: ${IMAGE_OCID}" + + # Wait for image to become AVAILABLE (manual polling) + echo "[Debug] Waiting for image to become AVAILABLE..." + MAX_WAIT_SECONDS=1800 # 30 minutes + POLL_INTERVAL=30 # Check every 30 seconds + ELAPSED=0 + + while [ $ELAPSED -lt $MAX_WAIT_SECONDS ]; do + IMAGE_STATE=$(oci compute image get \ + --image-id "${IMAGE_OCID}" \ + --query 'data."lifecycle-state"' \ + --raw-output 2>/dev/null || echo "ERROR") + + echo "[Debug] Current state: ${IMAGE_STATE} (elapsed: ${ELAPSED}s)" + + if [ "${IMAGE_STATE}" = "AVAILABLE" ]; then + echo "[Debug] ✅ Image is now AVAILABLE" + break + elif [ "${IMAGE_STATE}" = "ERROR" ] || [ "${IMAGE_STATE}" = "DELETED" ]; then + echo "[Error] ❌ Image import failed with state: ${IMAGE_STATE}" + exit 1 + fi + + sleep $POLL_INTERVAL + ELAPSED=$((ELAPSED + POLL_INTERVAL)) + done + + if [ $ELAPSED -ge $MAX_WAIT_SECONDS ]; then + echo "[Error] ⏱️ Timeout waiting for image to become AVAILABLE after ${MAX_WAIT_SECONDS}s" + exit 1 + fi + + - name: Configure Image Capabilities Schema + run: | + # Create/update image capability schema for the custom image + # Reference: https://docs.oracle.com/en-us/iaas/Content/Compute/Tasks/configuringimagecapabilities.htm + + echo "[Debug] Configuring image capability schema..." + + # Prepare image capabilities JSON based on global schema + # Must match the descriptor format including values arrays for enums + cat > /tmp/image_capabilities.json <<'EOF' + { + "Compute.Firmware": { + "descriptorType": "enumstring", + "source": "IMAGE", + "values": ["BIOS", "UEFI_64"], + "defaultValue": "UEFI_64" + }, + "Compute.SecureBoot": { + "descriptorType": "boolean", + "source": "IMAGE", + "defaultValue": true + }, + "Compute.LaunchMode": { + "descriptorType": "enumstring", + "source": "IMAGE", + "values": ["NATIVE", "EMULATED", "PARAVIRTUALIZED", "CUSTOM"], + "defaultValue": "PARAVIRTUALIZED" + }, + "Network.AttachmentType": { + "descriptorType": "enumstring", + "source": "IMAGE", + "values": ["E1000", "VFIO", "PARAVIRTUALIZED"], + "defaultValue": "PARAVIRTUALIZED" + }, + "Storage.BootVolumeType": { + "descriptorType": "enumstring", + "source": "IMAGE", + "values": ["ISCSI", "SCSI", "IDE", "PARAVIRTUALIZED"], + "defaultValue": "PARAVIRTUALIZED" + }, + "Storage.LocalDataVolumeType": { + "descriptorType": "enumstring", + "source": "IMAGE", + "values": ["ISCSI", "SCSI", "IDE", "PARAVIRTUALIZED"], + "defaultValue": "PARAVIRTUALIZED" + }, + "Storage.RemoteDataVolumeType": { + "descriptorType": "enumstring", + "source": "IMAGE", + "values": ["ISCSI", "SCSI", "IDE", "PARAVIRTUALIZED"], + "defaultValue": "PARAVIRTUALIZED" + }, + "Storage.ConsistentVolumeNaming": { + "descriptorType": "boolean", + "source": "IMAGE", + "defaultValue": true + }, + "Storage.ParaVirtualization.EncryptionInTransit": { + "descriptorType": "boolean", + "source": "IMAGE", + "defaultValue": true + }, + "Storage.ParaVirtualization.AttachmentVersion": { + "descriptorType": "enuminteger", + "source": "IMAGE", + "values": [1, 2], + "defaultValue": 2 + }, + "Storage.Iscsi.MultipathDeviceSupported": { + "descriptorType": "boolean", + "source": "IMAGE", + "defaultValue": true + }, + "Network.IPv6Only": { + "defaultValue": true, + "descriptorType": "boolean", + "source": "IMAGE" + } + } + EOF + + echo "[Debug] Image capabilities to configure:" + cat /tmp/image_capabilities.json | jq '.' + + # Create a unique schema name for this image + SCHEMA_NAME="almalinux-${ALMA_MAJOR}-${ALMA_ARCH}-schema" + COMPARTMENT_ID="${{ secrets.OCI_COMPARTMENT_ID }}" + + echo "[Debug] Schema name: ${SCHEMA_NAME}" + + # Get the available global image capability schema versions + echo "[Debug] Discovering global image capability schema versions..." + GLOBAL_CAP_ID=$( + oci compute global-image-capability-schema list --all | jq -r '.data[0].id') + GLOBAL_CAP_VERSION_NAME=$( + oci compute global-image-capability-schema-version list --all \ + --global-image-capability-schema-id $GLOBAL_CAP_ID \ + | jq -r '.data | sort_by(."time-created") | reverse | .[0].name') + + # Try to create the image capability schema + # Note: If schema exists, this will fail - we'll then update it + set +e + CREATE_OUTPUT=$(oci compute image-capability-schema create \ + --compartment-id "${COMPARTMENT_ID}" \ + --global-image-capability-schema-version-name $GLOBAL_CAP_VERSION_NAME \ + --image-id "${{ env.IMAGE_OCID }}" \ + --schema-data file:///tmp/image_capabilities.json \ + --display-name "${SCHEMA_NAME}" \ + 2>&1) + CREATE_EXIT=$? + set -e + + if [ ${CREATE_EXIT} -eq 0 ]; then + echo "[Debug] ✅ Image capability schema created successfully" + echo "${CREATE_OUTPUT}" | jq -r '.data.id' > /tmp/schema_id.txt + else + echo "[Debug] Schema creation failed (may already exist), attempting to update..." + + # List schemas for this image to find the existing one + SCHEMA_ID=$(oci compute image-capability-schema list \ + --compartment-id "${COMPARTMENT_ID}" \ + --image-id "${{ env.IMAGE_OCID }}" \ + --query 'data[0].id' \ + --raw-output 2>/dev/null || echo "") + + if [ -n "${SCHEMA_ID}" ]; then + echo "[Debug] Found existing schema: ${SCHEMA_ID}" + + # Update the existing schema + oci compute image-capability-schema update \ + --image-capability-schema-id "${SCHEMA_ID}" \ + --schema-data file:///tmp/image_capabilities.json \ + --force + + echo "[Debug] ✅ Image capability schema updated successfully" + else + echo "[Warning] Could not create or update image capability schema" + echo "[Warning] Error: ${CREATE_OUTPUT}" + fi + fi + + echo "" + echo "[Debug] Configured Image Capabilities:" + echo " Firmware: UEFI_64" + echo " Secure Boot: Enabled" + echo " Launch Mode: PARAVIRTUALIZED" + echo " Paravirtualization Version: 2" + echo " Consistent Volume Naming: Enabled" + echo " In-transit Encryption: Enabled" + echo " Multipath Device Support: Enabled" + echo " IPv6 Only: Enabled" + + # - name: Temporary step to set IMAGE_OCID, ARTIFACT_ID + # run: | + # echo "IMAGE_OCID=" >> $GITHUB_ENV + # echo "ARTIFACT_ID=" >> $GITHUB_ENV + + - name: Create the new Artifact + run: | + # Get the image shape compatibility entries + # Filter out the shapes that are not supported by the Marketplace: + # Standard1, DenseIO1, HighIO1, Generic, Standard2T, BigData, Flex + oci compute image-shape-compatibility-entry list \ + --image-id ${{ env.IMAGE_OCID }} \ + --all \ + --query 'data[*].{shape:shape}' \ + | jq --arg img "${{ env.IMAGE_OCID }}" '{ + sourceImageId: $img, + isSnapshotAllowed: true, + username: "opc", + imageShapeCompatibilityEntries: [ .[] | select(.shape | test("Standard1|DenseIO1|HighIO1|Generic|Standard2T|BigData|Flex") | not) ] + }' > /tmp/artifact_payload.json + + # Wrap OCI Custom Image into a Marketplace Artifact (request) + COMPARTMENT_ID="${{ secrets.OCI_COMPARTMENT_ID }}" + REQUEST_ID=$(oci marketplace-publisher artifact \ + create-artifact-create-machine-image-artifact-details \ + --compartment-id ${COMPARTMENT_ID} \ + --display-name '${{ env.CUSTOM_IMAGE_NAME }}' \ + --machine-image file:///tmp/artifact_payload.json \ + --query '"opc-work-request-id"' \ + --raw-output) + echo "[Debug] Request ID: ${REQUEST_ID}" + + ARTIFACT_ID=$(oci marketplace-publisher work-request get \ + --work-request-id ${REQUEST_ID} \ + | jq -r '.data.resources[0].identifier' 2>/dev/null || echo "ERROR") + + if [ -z "${ARTIFACT_ID}" ] || [ "${ARTIFACT_ID}" = "null" ] || [ "${ARTIFACT_ID}" = "ERROR" ]; then + echo "[Error] Failed to extract Artifact ID from work request" + exit 1 + fi + + echo "ARTIFACT_ID=${ARTIFACT_ID}" >> $GITHUB_ENV + echo "[Debug] Artifact ID: ${ARTIFACT_ID}" + + # Wait for artifact to become AVAILABLE (manual polling) + echo "[Debug] Waiting for artifact to become AVAILABLE..." + MAX_WAIT_SECONDS=2700 # 45 minutes + POLL_INTERVAL=30 # Check every 30 seconds + ELAPSED=0 + + while [ $ELAPSED -lt $MAX_WAIT_SECONDS ]; do + ARTIFACT_STATE=$(oci marketplace-publisher artifact get \ + --artifact-id ${ARTIFACT_ID} | jq -r '.data.status' 2>/dev/null || echo "ERROR") + + echo "[Debug] Current state: ${ARTIFACT_STATE} (elapsed: ${ELAPSED}s)" + + if [ "${ARTIFACT_STATE}" = "AVAILABLE" ]; then + echo "[Debug] ✅ Artifact is now AVAILABLE" + break + elif [ "${ARTIFACT_STATE}" = "ERROR" ] || [ "${ARTIFACT_STATE}" = "DELETED" ]; then + echo "[Error] ❌ Artifact import failed with state: ${ARTIFACT_STATE}" + exit 1 + fi + + sleep $POLL_INTERVAL + ELAPSED=$((ELAPSED + POLL_INTERVAL)) + done + + if [ $ELAPSED -ge $MAX_WAIT_SECONDS ]; then + echo "[Error] ⏱️ Timeout waiting for artifact to become AVAILABLE after ${MAX_WAIT_SECONDS}s" + exit 1 + fi + + - name: Get latest version of Terms Collection + run: | + # Get the most recent, ACTIVE Term Collection + TERMS_COLLECTION_DATA=$(oci marketplace-publisher term-collection list-terms \ + --all \ + --compartment-id ${{ secrets.OCI_COMPARTMENT_ID }}) + TERMS_COLLECTION_ID=$(echo "${TERMS_COLLECTION_DATA}" \ + | jq -r '.data.items | map(select(.["lifecycle-state"] == "ACTIVE")) | sort_by(.["time-created"]) | reverse | .[0].id' 2>/dev/null \ + || echo "ERROR") + TERMS_COLLECTION_NAME=$(echo "${TERMS_COLLECTION_DATA}" \ + | jq -r '.data.items | map(select(.["lifecycle-state"] == "ACTIVE")) | sort_by(.["time-created"]) | reverse | .[0].name' 2>/dev/null \ + || echo "ERROR") + + if [ -z "${TERMS_COLLECTION_ID}" ] || [ "${TERMS_COLLECTION_ID}" = "ERROR" ] || [ "${TERMS_COLLECTION_ID}" = "null" ]; then + echo "[Error] Failed to get latest ACTIVE version of Terms Collection" + exit 1 + fi + + echo "TERMS_COLLECTION_ID=${TERMS_COLLECTION_ID}" >> $GITHUB_ENV + echo "TERMS_COLLECTION_NAME=${TERMS_COLLECTION_NAME}" >> $GITHUB_ENV + echo "[Debug] Terms Collection ID: ${TERMS_COLLECTION_ID}" + echo "[Debug] Terms Collection Name: ${TERMS_COLLECTION_NAME}" + + # From the Terms Collection, get the most recent and ACTIVE version + TERM_VERSION_DATA=$(oci marketplace-publisher term-version-collection list-term-versions \ + --all \ + --term-id ${TERMS_COLLECTION_ID} \ + --compartment-id ${{ secrets.OCI_COMPARTMENT_ID }}) + TERM_VERSION_ID=$(echo "${TERM_VERSION_DATA}" \ + | jq -r '.data.items | map(select(.["lifecycle-state"] == "ACTIVE")) | sort_by(.["time-created"]) | reverse | .[0].id' 2>/dev/null \ + || echo "ERROR") + TERM_VERSION_NAME=$(echo "${TERM_VERSION_DATA}" \ + | jq -r '.data.items | map(select(.["lifecycle-state"] == "ACTIVE")) | sort_by(.["time-created"]) | reverse | .[0]."display-name"' 2>/dev/null \ + || echo "ERROR") + + if [ -z "${TERM_VERSION_ID}" ] || [ "${TERM_VERSION_ID}" = "ERROR" ] || [ "${TERM_VERSION_ID}" = "null" ]; then + echo "[Error] Failed to get latest ACTIVE version of Term Version" + exit 1 + fi + + echo "TERM_VERSION_ID=${TERM_VERSION_ID}" >> $GITHUB_ENV + echo "TERM_VERSION_NAME=${TERM_VERSION_NAME}" >> $GITHUB_ENV + echo "[Debug] Term Version ID: ${TERM_VERSION_ID}" + echo "[Debug] Term Version Name: ${TERM_VERSION_NAME}" + + - name: Get latest Revision of need Listing + run: | + # Get the most recent, ACTIVE, OCI_APPLICATION type Listing matching name pattern "AlmaLinux OS ${{ env.ALMA_VERSION }} (${{ env.ALMA_ARCH }})" + ARCH_STRING="${{ env.DISPLAY_ARCH }}" + [ "${{ env.ALMA_ARCH }}" = "aarch64" -a "${{ env.ALMA_MAJOR }}" = "10" ] \ + && ARCH_STRING="${{ env.DISPLAY_ARCH }}/ARM64" + LISTING_NAME="AlmaLinux OS ${{ env.ALMA_MAJOR }} (${ARCH_STRING})" + LISTING_OCID=$(oci marketplace-publisher listing-collection list-listings \ + --all \ + --compartment-id ${{ secrets.OCI_COMPARTMENT_ID }} \ + --query "data.items[? \"lifecycle-state\"=='ACTIVE' && \"listing-type\"=='OCI_APPLICATION' && contains(name, '${LISTING_NAME}')] | sort_by(@, &\"time-updated\") | [-1].id" \ + --raw-output) + if [ -z "${LISTING_OCID}" ] || [ "${LISTING_OCID}" = "ERROR" ] || [ "${LISTING_OCID}" = "null" ]; then + echo "[Error] Failed to get latest ACTIVE, OCI_APPLICATION type Listing matching name pattern '${LISTING_NAME}'" + exit 1 + fi + + echo "LISTING_OCID=${LISTING_OCID}" >> $GITHUB_ENV + echo "LISTING_NAME=${LISTING_NAME}" >> $GITHUB_ENV + echo "[Debug] Listing OCID: ${LISTING_OCID}" + echo "[Debug] Listing Name: ${LISTING_NAME}" + + # Get the most recent, ACTIVE, PUBLISHED Revision of the Listing + LISTING_REVISION_DATA=$(oci marketplace-publisher listing-revision-collection list-listing-revisions \ + --all \ + --listing-id ${LISTING_OCID} \ + --compartment-id ${{ secrets.OCI_COMPARTMENT_ID }}) + LISTING_REVISION_ID=$(echo "${LISTING_REVISION_DATA}" \ + | jq -r '.data.items | map(select(.["lifecycle-state"] == "ACTIVE" and .status == "PUBLISHED")) | sort_by(.["time-created"]) | reverse | .[0].id' 2>/dev/null \ + || echo "ERROR") + LISTING_REVISION_NAME=$(echo "${LISTING_REVISION_DATA}" \ + | jq -r '.data.items | map(select(.["lifecycle-state"] == "ACTIVE" and .status == "PUBLISHED")) | sort_by(.["time-created"]) | reverse | .[0]."display-name"' 2>/dev/null \ + || echo "ERROR") + + if [ -z "${LISTING_REVISION_ID}" ] || [ "${LISTING_REVISION_ID}" = "ERROR" ] || [ "${LISTING_REVISION_ID}" = "null" ]; then + echo "[Error] Failed to get latest ACTIVE, PUBLISHED Revision of the Listing" + exit 1 + fi + + echo "LISTING_REVISION_ID=${LISTING_REVISION_ID}" >> $GITHUB_ENV + echo "LISTING_REVISION_NAME=${LISTING_REVISION_NAME}" >> $GITHUB_ENV + echo "[Debug] Listing Revision ID: ${LISTING_REVISION_ID}" + echo "[Debug] Listing Revision Name: ${LISTING_REVISION_NAME}" + + - name: Clone Listing Revision (create a new Draft) + if: inputs.release_to_marketplace + run: | + # Clone the ACTIVE listing revision to create a new DRAFT revision + # that we can modify (add packages to) without affecting the live listing + echo "[Debug] Cloning listing revision: ${{ env.LISTING_REVISION_ID }}" + echo "[Debug] Listing: ${{ env.LISTING_NAME }}" + + set +e + CLONE_OUTPUT=$(oci marketplace-publisher listing-revision clone \ + --listing-revision-id ${{ env.LISTING_REVISION_ID }} \ + 2>&1) + CLONE_EXIT=$? + set -e + + echo "[Debug] Clone exit code: ${CLONE_EXIT}" + echo "[Debug] Clone output:" + echo "${CLONE_OUTPUT}" + + if [ ${CLONE_EXIT} -ne 0 ]; then + echo "[Error] Failed to clone listing revision" + exit 1 + fi + + # Clone returns a work request ID - extract it + WORK_REQUEST_ID=$(echo "${CLONE_OUTPUT}" | jq -r '."opc-work-request-id" // empty') + + if [ -z "${WORK_REQUEST_ID}" ]; then + echo "[Error] Failed to extract work request ID from clone output" + exit 1 + fi + + echo "[Debug] Work Request ID: ${WORK_REQUEST_ID}" + + # Poll the work request until it completes + echo "[Debug] Waiting for clone work request to complete..." + MAX_WAIT_SECONDS=300 # 5 minutes + POLL_INTERVAL=10 # Check every 10 seconds + ELAPSED=0 + + while [ $ELAPSED -lt $MAX_WAIT_SECONDS ]; do + set +e + WR_JSON=$(oci marketplace-publisher work-request get \ + --work-request-id "${WORK_REQUEST_ID}" \ + 2>&1) + WR_EXIT=$? + set -e + + if [ ${WR_EXIT} -ne 0 ]; then + echo "[Warning] Failed to get work request status (elapsed: ${ELAPSED}s): ${WR_JSON}" + sleep $POLL_INTERVAL + ELAPSED=$((ELAPSED + POLL_INTERVAL)) + continue + fi + + WR_STATUS=$(echo "${WR_JSON}" | jq -r '.data.status' 2>/dev/null || echo "UNKNOWN") + WR_PERCENT=$(echo "${WR_JSON}" | jq -r '.data."percent-complete" // "?"' 2>/dev/null) + echo "[Debug] Work Request status: ${WR_STATUS} (${WR_PERCENT}%) (elapsed: ${ELAPSED}s)" + + if [ "${WR_STATUS}" = "SUCCEEDED" ]; then + echo "[Debug] ✅ Clone work request succeeded" + break + elif [ "${WR_STATUS}" = "FAILED" ] || [ "${WR_STATUS}" = "CANCELED" ]; then + echo "[Error] ❌ Clone work request ${WR_STATUS}" + echo "${WR_JSON}" | jq '.data' 2>/dev/null || true + exit 1 + fi + + sleep $POLL_INTERVAL + ELAPSED=$((ELAPSED + POLL_INTERVAL)) + done + + if [ $ELAPSED -ge $MAX_WAIT_SECONDS ]; then + echo "[Error] ⏱️ Timeout waiting for clone work request after ${MAX_WAIT_SECONDS}s" + exit 1 + fi + + # Find the newly created draft revision (status=NEW, lifecycle-state=ACTIVE) + echo "[Debug] Looking for the new draft revision..." + REVISIONS_JSON=$(oci marketplace-publisher listing-revision-collection list-listing-revisions \ + --all \ + --listing-id ${{ env.LISTING_OCID }} \ + --compartment-id ${{ secrets.OCI_COMPARTMENT_ID }}) + + DRAFT_REVISION_ID=$(echo "${REVISIONS_JSON}" \ + | jq -r '.data.items | map(select(.["lifecycle-state"] == "ACTIVE" and .status == "NEW")) | sort_by(.["time-created"]) | reverse | .[0].id' 2>/dev/null \ + || echo "ERROR") + + if [ -z "${DRAFT_REVISION_ID}" ] || [ "${DRAFT_REVISION_ID}" = "null" ] || [ "${DRAFT_REVISION_ID}" = "ERROR" ]; then + echo "[Error] Failed to find the new draft revision" + echo "[Debug] Available revisions:" + echo "${REVISIONS_JSON}" | jq '.data.items[] | {id, status, "lifecycle-state", "time-created"}' 2>/dev/null || true + exit 1 + fi + + echo "DRAFT_REVISION_ID=${DRAFT_REVISION_ID}" >> $GITHUB_ENV + echo "[Debug] Draft Revision ID: ${DRAFT_REVISION_ID}" + + # - name: Temporary step to set DRAFT_REVISION_ID + # run: | + # echo "DRAFT_REVISION_ID=" >> $GITHUB_ENV + + - name: Update Draft Revision Details + if: inputs.release_to_marketplace + run: | + # Update the cloned revision's version details, headline, and tagline + # to reflect the new version being published + PACKAGE_VERSION="${{ env.ALMA_VERSION }}.${{ env.ALMA_DATE }}" + + # Extract release date from ALMA_DATE (first 8 digits: YYYYMMDD → "EEE MMM dd yyyy") + DATE_DIGITS=$(echo "${{ env.ALMA_DATE }}" | grep -oP '^\d{8}') + RELEASE_DATE=$(date -d "${DATE_DIGITS:0:4}-${DATE_DIGITS:4:2}-${DATE_DIGITS:6:2}" '+%a %b %d %Y') + + RELEASE_NOTES_URL="https://wiki.almalinux.org/release-notes/${{ env.ALMA_VERSION }}.html" + + echo "[Debug] Updating draft revision details:" + echo " Package Version: ${PACKAGE_VERSION}" + echo " Release Date: ${RELEASE_DATE}" + echo " Headline: AlmaLinux OS ${{ env.ALMA_VERSION }}" + echo " Tagline: AlmaLinux OS ${{ env.ALMA_VERSION }}" + echo " Code Name: ${{ env.ALMA_CODE_NAME }}" + echo " Release Notes: ${RELEASE_NOTES_URL}" + + # Build version-details JSON + VERSION_DETAILS=$(jq -n \ + --arg desc "