Skip to content

Azure: Test Image

Azure: Test Image #48

Workflow file for this run

name: "Azure: Test Image"
# Required repository configuration:
# secrets: AZURE_CLIENT_ID, AZURE_TENANT_ID, AZURE_SUBSCRIPTION_ID,
# MATTERMOST_WEBHOOK_URL
# vars: MATTERMOST_CHANNEL
#
# Required Azure RBAC actions for the service principal behind AZURE_CLIENT_ID
# (assign at the rg-alma-images resource-group scope):
# Microsoft.Compute/galleries/images/read
# Microsoft.Compute/virtualMachines/write
# Microsoft.Compute/virtualMachines/delete
# Microsoft.Compute/virtualMachines/deletePreservedOSDisk/action
# Microsoft.Compute/disks/delete
# Microsoft.Network/networkInterfaces/write
# Microsoft.Network/networkInterfaces/join/action
# Microsoft.Network/networkInterfaces/delete
# Microsoft.Network/networkSecurityGroups/read
# Microsoft.Network/networkSecurityGroups/write
# Microsoft.Network/networkSecurityGroups/join/action
# Microsoft.Network/networkSecurityGroups/delete
# Microsoft.Network/publicIPAddresses/read
# Microsoft.Network/publicIPAddresses/write
# Microsoft.Network/publicIPAddresses/join/action
# Microsoft.Network/publicIPAddresses/delete
# Microsoft.Network/virtualNetworks/write
# Microsoft.Network/virtualNetworks/subnets/join/action
# Microsoft.Resources/deployments/read
# Microsoft.Resources/deployments/write
# Microsoft.Resources/deployments/operationStatuses/read
on:
workflow_dispatch:
inputs:
compute_gallery_path:
description: "Compute Gallery Path: gallery_name/vm_image_definition/vm_image_version"
required: true
type: string
default: 'almalinux/almalinux-9-gen2/9.7.2026050101'
notify_mattermost:
description: "Send notification to Mattermost"
required: true
type: boolean
default: true
permissions:
id-token: write
contents: read
env:
RESOURCE_GROUP: rg-alma-images
AZURE_LOCATION: East US
AZURE_PORTAL_BASE_URL: https://portal.azure.com/#@/resource
jobs:
test-image:
name: "Test Azure Compute Gallery Image"
runs-on: ubuntu-24.04
env:
SSH_USER: almalinux
steps:
- uses: actions/checkout@v6
- name: Validate input
run: |
COMPUTE_GALLERY_PATH="${{ inputs.compute_gallery_path }}"
if [[ ! "${COMPUTE_GALLERY_PATH}" =~ ^[^/]+/[^/]+/[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
echo "[Error] Invalid Compute Gallery Path: '${COMPUTE_GALLERY_PATH}'"
echo "Expected format: gallery_name/vm_image_definition/vm_image_version"
echo "Example: almalinux/almalinux-9-gen2/9.7.2026050101"
exit 1
fi
echo "COMPUTE_GALLERY_PATH=${COMPUTE_GALLERY_PATH}" >> "$GITHUB_ENV"
- name: Parse Compute Gallery Path
run: |
IFS='/' read -r GALLERY_NAME VM_IMAGE_DEFINITION VM_IMAGE_VERSION <<< "${COMPUTE_GALLERY_PATH}"
IFS='.' read -r ALMA_MAJOR V_PART2 V_PART3 <<< "${VM_IMAGE_VERSION}"
# Kitten image-definitions use Major.Datestamp.Iteration with no minor
# (e.g. 10.20260501.0). Stable AlmaLinux uses Major.Minor.Patch where
# Patch encodes the datestamp+iteration (e.g. 9.7.2026050101).
case "${VM_IMAGE_DEFINITION}" in
*kitten*)
ALMA_VERSION="${ALMA_MAJOR}"
DATESTAMP_ITERATION="${V_PART2}.${V_PART3}"
RELEASE_STRING="AlmaLinux Kitten release ${ALMA_MAJOR}"
;;
*)
ALMA_VERSION="${ALMA_MAJOR}.${V_PART2}"
DATESTAMP_ITERATION="${V_PART3}"
RELEASE_STRING="AlmaLinux release ${ALMA_VERSION}"
;;
esac
echo "GALLERY_NAME=${GALLERY_NAME}"
echo "VM_IMAGE_DEFINITION=${VM_IMAGE_DEFINITION}"
echo "VM_IMAGE_VERSION=${VM_IMAGE_VERSION}"
echo "ALMA_VERSION=${ALMA_VERSION}"
echo "DATESTAMP_ITERATION=${DATESTAMP_ITERATION}"
echo "RELEASE_STRING=${RELEASE_STRING}"
{
echo "GALLERY_NAME=${GALLERY_NAME}"
echo "VM_IMAGE_DEFINITION=${VM_IMAGE_DEFINITION}"
echo "VM_IMAGE_VERSION=${VM_IMAGE_VERSION}"
echo "ALMA_VERSION=${ALMA_VERSION}"
echo "DATESTAMP_ITERATION=${DATESTAMP_ITERATION}"
echo "RELEASE_STRING=${RELEASE_STRING}"
} >> "$GITHUB_ENV"
- name: Install dependencies
run: |
sudo apt-get update
sudo apt-get install -y netcat-openbsd
- name: Azure login
uses: azure/login@v3
with:
client-id: ${{ secrets.AZURE_CLIENT_ID }}
tenant-id: ${{ secrets.AZURE_TENANT_ID }}
subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }}
- name: Resolve gallery image version and architecture
run: |
JSON=$(az sig image-version show \
--resource-group "${RESOURCE_GROUP}" \
--gallery-name "${GALLERY_NAME}" \
--gallery-image-definition "${VM_IMAGE_DEFINITION}" \
--gallery-image-version "${VM_IMAGE_VERSION}" \
--output json) || {
echo "[Error] Gallery image version not found: ${COMPUTE_GALLERY_PATH}"
exit 1
}
IMAGE_ID=$(jq -r '.id // empty' <<< "${JSON}")
# az sig image-version show keeps a 'properties' wrapper on some CLI /
# API combinations and flattens it on others; query both shapes.
SOURCE_URI=$(jq -r '
.storageProfile.osDiskImage.source.uri //
.properties.storageProfile.osDiskImage.source.uri //
empty
' <<< "${JSON}")
if [ -z "${IMAGE_ID}" ] || [ -z "${SOURCE_URI}" ]; then
echo "[Error] Could not extract image-version metadata for ${COMPUTE_GALLERY_PATH}"
echo "Raw JSON:"
echo "${JSON}"
exit 1
fi
VHD_FILE="${SOURCE_URI##*/}"
echo "Image ID: ${IMAGE_ID}"
echo "Source VHD: ${VHD_FILE}"
# Reverse-engineer architecture from the VHD source filename, using the
# same regex pair as the VHD path in .github/workflows/azure-to-gallery.yml.
regex_azure='-([0-9]+\.?[0-9]*)-([0-9]{8,9}(\.[0-9])?).*\.(x86_64|aarch64|arm64)'
regex_simple='almalinux-([0-9]+\.[0-9]+)-(x86_64|aarch64|arm64)\.([0-9]{8})'
if [[ $VHD_FILE =~ $regex_azure ]]; then
ALMA_ARCH="${BASH_REMATCH[4]}"
elif [[ $VHD_FILE =~ $regex_simple ]]; then
ALMA_ARCH="${BASH_REMATCH[2]}"
else
echo "[Error] Could not parse architecture from VHD source: '${VHD_FILE}'"
exit 1
fi
# Normalize arm64 -> aarch64 (the in-VM tests grep rpm output that uses
# the uname-style arch).
[ "${ALMA_ARCH}" = "arm64" ] && ALMA_ARCH="aarch64"
# Map arch -> Azure VM size
case "${ALMA_ARCH}" in
x86_64) VM_SIZE="Standard_D2as_v5" ;;
aarch64) VM_SIZE="Standard_D2ps_v5" ;;
esac
# CUSTOM_IMAGE_NAME (used as artifact name and notification label) is
# the source VHD filename without the .vhd extension.
CUSTOM_IMAGE_NAME="${VHD_FILE%.vhd}"
echo "ALMA_ARCH=${ALMA_ARCH}"
echo "VM_SIZE=${VM_SIZE}"
echo "CUSTOM_IMAGE_NAME=${CUSTOM_IMAGE_NAME}"
{
echo "IMAGE_ID=${IMAGE_ID}"
echo "VHD_FILE=${VHD_FILE}"
echo "ALMA_ARCH=${ALMA_ARCH}"
echo "VM_SIZE=${VM_SIZE}"
echo "CUSTOM_IMAGE_NAME=${CUSTOM_IMAGE_NAME}"
} >> "$GITHUB_ENV"
- name: Generate ephemeral SSH keypair
run: |
mkdir -p ~/.ssh
chmod 700 ~/.ssh
ssh-keygen -t ed25519 -N '' -C "azure-test-${GITHUB_RUN_ID}" -f ~/.ssh/azure_test
- name: Launch test VM
run: |
VM_NAME="azure-test-${ALMA_VERSION}-${DATESTAMP_ITERATION}-${ALMA_ARCH}-${GITHUB_RUN_ID}"
echo "VM Name: ${VM_NAME}"
VM_ID=$(az vm create \
--resource-group "${RESOURCE_GROUP}" \
--location "${AZURE_LOCATION}" \
--name "${VM_NAME}" \
--image "${IMAGE_ID}" \
--size "${VM_SIZE}" \
--admin-username "${SSH_USER}" \
--ssh-key-values ~/.ssh/azure_test.pub \
--public-ip-sku Standard \
--os-disk-size-gb 100 \
--nsg-rule SSH \
--query id --output tsv)
echo "VM ID: ${VM_ID}"
{
echo "VM_NAME=${VM_NAME}"
echo "VM_ID=${VM_ID}"
} >> "$GITHUB_ENV"
- name: Resolve VM public IP
run: |
PUBLIC_IP=$(az vm show \
--resource-group "${RESOURCE_GROUP}" \
--name "${VM_NAME}" \
--show-details \
--query publicIps --output tsv)
if [ -z "${PUBLIC_IP}" ] || [ "${PUBLIC_IP}" = "null" ]; then
echo "[Error] VM has no public IP"
exit 1
fi
echo "Public IP: ${PUBLIC_IP}"
echo "PUBLIC_IP=${PUBLIC_IP}" >> "$GITHUB_ENV"
- name: Wait for SSH
run: |
for i in $(seq 1 60); do
if nc -z -w2 "${PUBLIC_IP}" 22; then
echo "[Info] SSH port reachable after ${i} attempt(s)"
break
fi
if [ "${i}" -eq 60 ]; then
echo "[Error] SSH did not become reachable within 10 minutes"
exit 1
fi
sleep 10
done
ssh-keyscan -T 5 "${PUBLIC_IP}" >> ~/.ssh/known_hosts 2>/dev/null
- name: Run image tests
run: |
SSH=(ssh -i ~/.ssh/azure_test -o StrictHostKeyChecking=accept-new -o ConnectTimeout=15 "${SSH_USER}@${PUBLIC_IP}")
echo "[Debug] AlmaLinux release:"
ALMA_RELEASE=$("${SSH[@]}" "grep '${RELEASE_STRING}' /etc/almalinux-release")
echo "${ALMA_RELEASE}"
echo "[Debug] AlmaLinux release package:"
RELEASE_PACKAGE=$("${SSH[@]}" "rpm -qf /etc/almalinux-release")
echo "${RELEASE_PACKAGE}"
echo "[Debug] System architecture:"
SYSTEM_ARCH=$("${SSH[@]}" "rpm -q --qf='%{ARCH}\n' ${RELEASE_PACKAGE} | grep '${ALMA_ARCH}'")
echo "${SYSTEM_ARCH}"
echo "[Debug] Azure-specific RPM packages:"
# Installed by ansible/roles/azure_guest/tasks/main.yml. rpm -q exits
# non-zero (and prints "package X is not installed") if any are missing,
# which fails the step under set -e.
"${SSH[@]}" "rpm -q WALinuxAgent hyperv-daemons NetworkManager-cloud-setup cifs-utils"
echo "[Debug] Azure Linux Agent (waagent) service:"
"${SSH[@]}" "systemctl is-enabled waagent.service"
echo "[Debug] Disk and filesystems:"
"${SSH[@]}" "sudo lsblk"
"${SSH[@]}" 'ROOT_SIZE_BYTES=$(df -B1 --output=size / | tail -n 1 | tr -d " "); MIN_SIZE_BYTES=$((98*1024*1024*1024)); [ "${ROOT_SIZE_BYTES}" -gt "${MIN_SIZE_BYTES}" ] || { echo "[Error] Root filesystem resize check failed: ${ROOT_SIZE_BYTES} bytes (expected > ${MIN_SIZE_BYTES} bytes)"; exit 1; }'
echo "[Debug] Check for updates:"
# dnf check-update returns 100 when updates are available — treat as success
rc=0
"${SSH[@]}" "sudo dnf check-update" || rc=$?
if [ "${rc}" -ne 0 ] && [ "${rc}" -ne 100 ]; then
echo "[Error] dnf check-update failed with exit code ${rc}"
exit "${rc}"
fi
PKG_FILE="${VHD_FILE}.txt"
"${SSH[@]}" "rpm -qa --queryformat '%{NAME}\n' | sort > /tmp/${PKG_FILE}"
scp -i ~/.ssh/azure_test -o StrictHostKeyChecking=accept-new \
"${SSH_USER}@${PUBLIC_IP}:/tmp/${PKG_FILE}" "./${PKG_FILE}"
{
echo "PKG_FILE=${PKG_FILE}"
echo "ALMA_RELEASE=${ALMA_RELEASE}"
echo "SYSTEM_ARCH=${SYSTEM_ARCH}"
} >> "$GITHUB_ENV"
- name: Upload packages list artifact
if: env.PKG_FILE != ''
uses: actions/upload-artifact@v7
with:
name: ${{ env.PKG_FILE }}
path: ./${{ env.PKG_FILE }}
- name: Job summary
if: always() && env.CUSTOM_IMAGE_NAME != ''
run: |
{
echo "## Azure Image Test"
echo ""
echo "- **Compute Gallery Image**: [${COMPUTE_GALLERY_PATH}](${AZURE_PORTAL_BASE_URL}${IMAGE_ID})"
echo "- **Size**: \`${VM_SIZE}\`"
if [ -n "${VM_ID:-}" ]; then
echo "- **Test VM**: [${VM_NAME}](${AZURE_PORTAL_BASE_URL}${VM_ID})"
echo "- **Public IP**: \`${PUBLIC_IP:-n/a}\`"
fi
if [ -n "${ALMA_RELEASE:-}" ]; then
echo "- **AlmaLinux release**: \`${ALMA_RELEASE}\`"
fi
if [ -n "${SYSTEM_ARCH:-}" ]; then
echo "- **System architecture**: \`${SYSTEM_ARCH}\`"
fi
echo "- **Test**: ${{ job.status == 'success' && 'passed ✅' || 'failed ❌' }}"
} >> "$GITHUB_STEP_SUMMARY"
- name: Terminate test VM
if: always() && env.VM_NAME != ''
run: |
OS_DISK=$(az vm show \
--resource-group "${RESOURCE_GROUP}" \
--name "${VM_NAME}" \
--query 'storageProfile.osDisk.name' --output tsv 2>/dev/null || true)
az vm delete \
--resource-group "${RESOURCE_GROUP}" \
--name "${VM_NAME}" \
--yes --force-deletion true || true
if [ -n "${OS_DISK}" ]; then
az disk delete \
--resource-group "${RESOURCE_GROUP}" \
--name "${OS_DISK}" \
--yes --no-wait || true
fi
az network nic delete \
--resource-group "${RESOURCE_GROUP}" \
--name "${VM_NAME}VMNic" \
--no-wait || true
az network public-ip delete \
--resource-group "${RESOURCE_GROUP}" \
--name "${VM_NAME}PublicIP" \
--no-wait || true
az network nsg delete \
--resource-group "${RESOURCE_GROUP}" \
--name "${VM_NAME}NSG" \
--no-wait || true
- name: Send notification to Mattermost
uses: mattermost/action-mattermost-notify@master
if: always() && inputs.notify_mattermost && env.CUSTOM_IMAGE_NAME != ''
with:
MATTERMOST_WEBHOOK_URL: ${{ secrets.MATTERMOST_WEBHOOK_URL }}
MATTERMOST_CHANNEL: ${{ vars.MATTERMOST_CHANNEL }}
MATTERMOST_USERNAME: ${{ github.triggering_actor }}
TEXT: |
:almalinux: **${{ env.CUSTOM_IMAGE_NAME }}**, Azure image test, by the GitHub [Action](${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }})
**Compute Gallery Image**: [${{ env.COMPUTE_GALLERY_PATH }}](${{ env.AZURE_PORTAL_BASE_URL }}${{ env.IMAGE_ID }})
${{ env.VM_ID && format('**Test VM**: [{0}]({1}{2})', env.VM_NAME, env.AZURE_PORTAL_BASE_URL, env.VM_ID) || '' }}
**Size**: `${{ env.VM_SIZE }}`
${{ env.ALMA_RELEASE && format('**AlmaLinux release**: `{0}`', env.ALMA_RELEASE) || '' }}
${{ env.SYSTEM_ARCH && format('**System architecture**: `{0}`', env.SYSTEM_ARCH) || '' }}
**Test**: ${{ job.status == 'success' && 'passed ✅' || 'failed ❌' }}