Skip to content

Commit aa8dcec

Browse files
committed
feat(ci): add unified "Azure: Build, Release, Test, Publish" workflow
A single workflow_dispatch pipeline that takes Azure images from Packer build to Marketplace draft in one run: init-data |- build-gh-hosted (x86_64) \ shared-steps build, then |- build-self-hosted (aarch64xN) / azure-gallery-steps (in-job) v collect-images - merges per-image manifests into the test matrix v test-image - matrix, one gen2 gallery path per image v collect-passed - publish matrix from per-image passed-test records v publish-image - matrix, max-parallel: 1 (shared-offer contention) v pipeline-summary Key properties: - The gallery release runs INSIDE the build job on the .raw the build just produced (new azure-gallery-steps composite wrapping tools/azure_uploader.sh). The standalone flow's S3 upload -> download round-trip (~2x 30 GB per image) is gone from the critical path. - Stage logic is reused via composite actions, not workflow calls: - azure-gallery-steps: derives -d/-t from build inputs (no filename re-parsing), applies the community_gallery rule (AL10/Kitten -> private), installs azure-cli via pip on the aarch64 AlmaLinux 9 runner, and writes a per-image manifest artifact with blob URL, created gallery paths, the gen2 test path, and marketplace eligibility (Kitten aarch64-64k has no plan -> excluded up front). - azure-test-steps: extracted from azure-test.yml; job.status replaced with steps.run_tests.outcome (composite-safe), secrets passed as inputs. Behaviour unchanged. - azure-marketplace-steps: extracted from azure-to-marketplace.yml; same conversion, behaviour unchanged. - Matrix-job outputs collapse, so per-image data flows through artifacts: azure-manifest-<key>.json (gallery results) and azure-test-passed-<key>.json (written only by passing test legs). Publish therefore covers exactly the images that passed their test, even when a sibling image's test failed. - Marketplace publishes are serialised (max-parallel: 1): aarch64 and aarch64-64k share the almalinux-arm offer and parallel Product Ingestion configure calls collide on the offer's draft revision. - Inputs (the workflow_dispatch maximum of 10): the six azure-build inputs + community_gallery + new release_to_gallery gate + release_to_marketplace + submit_to_preview. release_to_gallery=false gives a build-only run; test and publish require the gallery stage. - Runner volumes bumped to 60g (RunsOn labels) / 60 GB (fork EC2 path): the fixed-VHD conversion roughly doubles the image footprint on disk. The standalone workflows (azure-build / azure-to-gallery / azure-test / azure-to-marketplace) are untouched and keep working independently.
1 parent 90f9527 commit aa8dcec

6 files changed

Lines changed: 2216 additions & 0 deletions

File tree

Lines changed: 277 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,277 @@
1+
name: "Azure: release built image to Compute Gallery"
2+
description: >
3+
Converts the locally-built Azure .raw image to a fixed VHD, uploads it to
4+
the Azure storage container, and creates Compute Gallery image-definition
5+
version(s) via tools/azure_uploader.sh. Designed to run right after
6+
./.github/actions/shared-steps inside an Azure build job, consuming the
7+
.raw file that build produced on this very runner - no S3 round-trip.
8+
Writes a per-image manifest artifact consumed by the downstream test and
9+
marketplace stages of the unified Azure workflow.
10+
11+
inputs:
12+
image_file:
13+
description: "Path to the built .raw image on this runner (shared-steps env.IMAGE_FILE)"
14+
required: true
15+
variant:
16+
description: "Build variant, e.g. 8, 9, 9-64k, 10, 10-64k, 10-kitten, 10-kitten-64k"
17+
required: true
18+
arch:
19+
description: "Image architecture: x86_64 or aarch64"
20+
required: true
21+
community_gallery:
22+
description: "Release to the Community (public) gallery when eligible (true/false)"
23+
required: true
24+
default: 'true'
25+
notify_mattermost:
26+
description: "Send notification to Mattermost (true/false)"
27+
required: true
28+
default: 'true'
29+
AZURE_CLIENT_ID:
30+
description: "Azure OIDC client id"
31+
required: true
32+
AZURE_TENANT_ID:
33+
description: "Azure tenant id"
34+
required: true
35+
AZURE_SUBSCRIPTION_ID:
36+
description: "Azure subscription id"
37+
required: true
38+
MATTERMOST_WEBHOOK_URL:
39+
description: "Mattermost webhook URL (required when notify_mattermost is true)"
40+
required: false
41+
default: ''
42+
MATTERMOST_CHANNEL:
43+
description: "Mattermost channel (required when notify_mattermost is true)"
44+
required: false
45+
default: ''
46+
47+
runs:
48+
using: "composite"
49+
steps:
50+
- name: Install azure-cli if missing
51+
shell: bash
52+
run: |
53+
# Install azure-cli if missing
54+
#
55+
# The x86_64 ubuntu24-full-x64 RunsOn image ships az. EL9 runners
56+
# don't:
57+
# - x86_64: install from Microsoft's official RPM repo.
58+
# - aarch64: Microsoft publishes no aarch64 RPMs - install via
59+
# pip into an ISOLATED venv. A bare `pip install azure-cli`
60+
# into the system prefix can resolve against pre-existing
61+
# system site-packages and produce a CLI whose `az storage`
62+
# module crashes with "'NoneType' object is not iterable".
63+
if command -v az >/dev/null 2>&1; then
64+
echo "[Debug] azure-cli present: $(az version --query '\"azure-cli\"' --output tsv 2>/dev/null || az --version | head -1)"
65+
exit 0
66+
fi
67+
68+
if [ -f /etc/redhat-release ] && [ "$(uname -m)" = "x86_64" ]; then
69+
echo "[Debug] azure-cli not found - installing from the Microsoft RPM repo"
70+
sudo rpm --import https://packages.microsoft.com/keys/microsoft.asc
71+
sudo dnf -y -q install https://packages.microsoft.com/config/rhel/9.0/packages-microsoft-prod.rpm || true
72+
sudo dnf -y -q install azure-cli
73+
elif [ -f /etc/redhat-release ]; then
74+
echo "[Debug] azure-cli not found - installing via pip into an isolated venv"
75+
sudo dnf -y -q install python3-pip gcc python3-devel
76+
sudo python3 -m venv /opt/azure-cli-venv
77+
sudo /opt/azure-cli-venv/bin/pip install --quiet --upgrade pip
78+
sudo /opt/azure-cli-venv/bin/pip install --quiet azure-cli
79+
sudo ln -sf /opt/azure-cli-venv/bin/az /usr/local/bin/az
80+
else
81+
echo "[Debug] azure-cli not found - installing via Microsoft's apt script"
82+
curl -sL https://aka.ms/InstallAzureCLIDeb | sudo bash
83+
fi
84+
85+
az --version | head -1
86+
# Fail fast if the storage module is broken (the symptom of a
87+
# poisoned dependency set) instead of dying mid-upload later.
88+
az storage blob list --help >/dev/null
89+
echo "[Debug] az storage module OK"
90+
91+
- name: Azure login
92+
uses: azure/login@v3
93+
with:
94+
client-id: ${{ inputs.AZURE_CLIENT_ID }}
95+
tenant-id: ${{ inputs.AZURE_TENANT_ID }}
96+
subscription-id: ${{ inputs.AZURE_SUBSCRIPTION_ID }}
97+
98+
- name: Prepare gallery release parameters
99+
shell: bash
100+
run: |
101+
# Prepare gallery release parameters
102+
image_file='${{ inputs.image_file }}'
103+
variant='${{ inputs.variant }}'
104+
arch='${{ inputs.arch }}'
105+
106+
test -f "${image_file}" || { echo "[Error] image file not found: ${image_file}"; exit 1; }
107+
108+
# Image type comes straight from the build inputs - no filename
109+
# guessing needed (unlike the standalone azure-to-gallery.yml which
110+
# starts from a bare URL).
111+
case "${arch}" in
112+
x86_64) image_type="default" ;;
113+
aarch64)
114+
image_type="arm64"
115+
[[ "${variant}" == *-64k ]] && image_type="arm64-64k"
116+
;;
117+
*) echo "[Error] Unknown architecture: ${arch}"; exit 1 ;;
118+
esac
119+
120+
# Distribution version for azure_uploader.sh -d. Parse from the
121+
# (self-produced, modern-format) filename:
122+
# AlmaLinux-9-Azure-9.8-20260526.0.x86_64.raw -> 9.8
123+
# AlmaLinux-10-Azure-10.2-20260526.0-64k.aarch64.raw -> 10.2
124+
# AlmaLinux-Kitten-Azure-10-20260526.0.x86_64.raw -> 10
125+
base=$(basename "${image_file}")
126+
regex='-([0-9]+\.?[0-9]*)-([0-9]{8,9}(\.[0-9])?).*\.(x86_64|aarch64)'
127+
if [[ ${base} =~ ${regex} ]]; then
128+
release_version="${BASH_REMATCH[1]}"
129+
timestamp="${BASH_REMATCH[2]}"
130+
else
131+
echo "[Error] Could not parse '${base}' file name!"
132+
exit 1
133+
fi
134+
135+
# AlmaLinux release string for reporting
136+
case "${release_version}" in
137+
10) release_string="AlmaLinux Kitten OS ${release_version} ${arch}" ;;
138+
*) release_string="AlmaLinux OS ${release_version} ${arch}" ;;
139+
esac
140+
141+
# Community gallery: AlmaLinux 10 and Kitten are not released to the
142+
# Community Gallery (same rule as azure-to-gallery.yml). The uploader
143+
# additionally self-enforces AL9 arm64-64k -> almalinux_ci.
144+
gallery_opt=''
145+
if [[ '${{ inputs.community_gallery }}' == 'true' && "${release_version}" != 10* ]]; then
146+
gallery_opt='-g almalinux'
147+
fi
148+
149+
# Kitten aarch64-64k has no Azure Marketplace plan - exclude it from
150+
# the publish stage up front.
151+
marketplace_eligible=true
152+
[[ "${variant}" == *kitten* && "${image_type}" == "arm64-64k" ]] \
153+
&& marketplace_eligible=false
154+
155+
{
156+
echo "GLR_IMAGE_FILE=${image_file}"
157+
echo "GLR_IMAGE_BASENAME=${base}"
158+
echo "GLR_RELEASE_VERSION=${release_version}"
159+
echo "GLR_TIMESTAMP=${timestamp}"
160+
echo "GLR_IMAGE_TYPE=${image_type}"
161+
echo "GLR_RELEASE_STRING=${release_string}"
162+
echo "GLR_GALLERY_OPT=${gallery_opt}"
163+
echo "GLR_MARKETPLACE_ELIGIBLE=${marketplace_eligible}"
164+
} >> "$GITHUB_ENV"
165+
166+
echo "[Debug] type=${image_type} version=${release_version} gallery_opt='${gallery_opt}' eligible=${marketplace_eligible}"
167+
168+
- name: Release the image to the Gallery
169+
shell: bash
170+
run: |
171+
# Release the image to the Gallery
172+
#
173+
# Run azure_uploader.sh next to the .raw file so the converted .vhd
174+
# lands on the same (large) volume. -f = perform, not dry-run.
175+
workdir=$(dirname "${GLR_IMAGE_FILE}")
176+
177+
# The output directory and the .raw inside it are owned by root -
178+
# packer runs under sudo in shared-steps. Take ownership so the
179+
# uploader (running as the runner user) can resize the .raw in
180+
# place, write the converted .vhd next to it, and upload it.
181+
sudo chown -R "$(id -u):$(id -g)" "${workdir}"
182+
183+
cp -a "${GITHUB_WORKSPACE}/tools/azure_uploader.sh" "${workdir}/"
184+
chmod +x "${workdir}/azure_uploader.sh"
185+
cd "${workdir}"
186+
187+
./azure_uploader.sh -f \
188+
-d "${GLR_RELEASE_VERSION}" \
189+
-t "${GLR_IMAGE_TYPE}" \
190+
-i "$(basename "${GLR_IMAGE_FILE}")" \
191+
${GLR_GALLERY_OPT} \
192+
|& tee ./azure_uploader.log
193+
194+
echo "GLR_LOG=${workdir}/azure_uploader.log" >> "$GITHUB_ENV"
195+
196+
- name: Collect gallery results, write manifest
197+
shell: bash
198+
run: |
199+
# Collect gallery results, write manifest
200+
test -f "${GLR_LOG}"
201+
202+
# VHD blob URL ("Image URI: https://...vhd")
203+
blob_url=$(grep 'Image URI:' "${GLR_LOG}" | head -1 | sed -E 's/^Image URI: //')
204+
[ -n "${blob_url}" ] || { echo "[Error] No 'Image URI:' in uploader log"; exit 1; }
205+
206+
# Image definition versions ("- Created: 'gallery/definition/version'")
207+
mapfile -t created < <(grep -oE "Created: '[^']+'" "${GLR_LOG}" \
208+
| sed -E "s/^Created: '(.+)'$/\1/" | sort -u)
209+
[ "${#created[@]}" -gt 0 ] || { echo "[Error] No 'Created:' lines in uploader log"; exit 1; }
210+
211+
# Test path: one per image. Intel images create gen1 + gen2
212+
# definitions - test only gen2. ARM images create a single gen2
213+
# definition. AlmaLinux 10 x86_64 definitions carry no gen suffix.
214+
test_path=''
215+
for p in "${created[@]}"; do
216+
[[ "${p}" == *-gen1/* ]] && continue
217+
test_path="${p}"
218+
break
219+
done
220+
[ -n "${test_path}" ] || test_path="${created[0]}"
221+
222+
image_key="${{ inputs.variant }}-${{ inputs.arch }}"
223+
224+
jq -n \
225+
--arg image_key "${image_key}" \
226+
--arg image_file "${GLR_IMAGE_BASENAME}" \
227+
--arg variant "${{ inputs.variant }}" \
228+
--arg arch "${{ inputs.arch }}" \
229+
--arg image_type "${GLR_IMAGE_TYPE}" \
230+
--arg blob_url "${blob_url}" \
231+
--arg test_path "${test_path}" \
232+
--argjson created "$(printf '%s\n' "${created[@]}" | jq -R . | jq -s .)" \
233+
--argjson eligible "${GLR_MARKETPLACE_ELIGIBLE}" \
234+
'{image_key: $image_key, image_file: $image_file, variant: $variant,
235+
arch: $arch, image_type: $image_type, blob_url: $blob_url,
236+
test_path: $test_path, created_paths: $created,
237+
marketplace_eligible: $eligible}' \
238+
| tee "azure-manifest-${image_key}.json"
239+
240+
echo "GLR_MANIFEST=azure-manifest-${image_key}.json" >> "$GITHUB_ENV"
241+
echo "GLR_BLOB_URL=${blob_url}" >> "$GITHUB_ENV"
242+
{
243+
echo 'GLR_CREATED_SUMMARY<<EOF'
244+
printf -- "- Created: '%s'\n" "${created[@]}"
245+
echo EOF
246+
} >> "$GITHUB_ENV"
247+
248+
# Job summary
249+
{
250+
echo "**${GLR_RELEASE_STRING}** \`${GLR_TIMESTAMP}\` released to Compute Gallery:"
251+
echo "- Image file: ${GLR_IMAGE_BASENAME}"
252+
echo "- Image URI: ${blob_url}"
253+
printf -- "- Created: '%s'\n" "${created[@]}"
254+
echo "- The Gallery is \"Community\": ${GLR_GALLERY_OPT:+✅}${GLR_GALLERY_OPT:-❌}"
255+
} >> "$GITHUB_STEP_SUMMARY"
256+
257+
- name: Store gallery manifest as artifact
258+
uses: actions/upload-artifact@v7
259+
with:
260+
name: ${{ env.GLR_MANIFEST }}
261+
path: ${{ env.GLR_MANIFEST }}
262+
if-no-files-found: error
263+
264+
- name: Send notification to Mattermost
265+
uses: mattermost/action-mattermost-notify@master
266+
if: inputs.notify_mattermost == 'true' && inputs.MATTERMOST_WEBHOOK_URL != ''
267+
with:
268+
MATTERMOST_WEBHOOK_URL: ${{ inputs.MATTERMOST_WEBHOOK_URL }}
269+
MATTERMOST_CHANNEL: ${{ inputs.MATTERMOST_CHANNEL }}
270+
MATTERMOST_USERNAME: ${{ github.triggering_actor }}
271+
TEXT: |
272+
:almalinux: **${{ env.GLR_RELEASE_STRING }}** `${{ env.GLR_TIMESTAMP }}` Azure image, by the GitHub [Action](${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }})
273+
- Image file: `${{ env.GLR_IMAGE_BASENAME }}`
274+
- Image URI: ${{ env.GLR_BLOB_URL }}
275+
${{ env.GLR_CREATED_SUMMARY }}
276+
- Released to Gallery: ✅
277+
- The Gallery is "Community": ${{ env.GLR_GALLERY_OPT != '' && '✅' || '❌' }}

0 commit comments

Comments
 (0)