@@ -110,39 +110,277 @@ on:
110110 required : false
111111
112112jobs :
113- gameplan :
113+ validate-and-plan :
114114 # this sets outputs that decide which actions to take in the workflow
115115 runs-on : ubuntu-latest
116116 outputs :
117117 build-platform-matrix : ${{ steps.build-platforms.outputs.matrix }}
118118 normalized-suffix : ${{ steps.suffix_norm_for_artifacts.outputs.norm }}
119119 steps :
120- - name : Validate image format
120+ - name : " Validate image format"
121121 id : validate
122122 run : |
123123 set -euo pipefail; echo "now: $(date -u +"%Y-%m-%dT%H:%M:%S.%3N")"
124124
125+ fail() { echo "::error::$1"; exit 1; }
126+
125127 registry="${{ inputs.image_registry }}"
126128 namespace="${{ inputs.image_namespace }}"
127129 image_name="${{ inputs.image_name }}"
128130
129- # Registry: alphanum + ._- and optional :port, no slash
131+ echo "──────────────────────────────────────────────"
132+ echo "🧩 VALIDATING IMAGE FORMAT INPUTS"
133+ echo "──────────────────────────────────────────────"
134+
135+ echo "→ Checking basic presence and whitespace..."
136+ if [[ -z "${registry}" || -z "${namespace}" || -z "${image_name}" ]]; then
137+ fail "All of 'image_registry', 'image_namespace', and 'image_name' must be non-empty."
138+ fi
139+ if [[ "${registry}${namespace}${image_name}" =~ [[:space:]] ]]; then
140+ fail "Inputs must not contain whitespace."
141+ fi
142+ echo "✅ Basic presence and whitespace OK."
143+
144+ echo "→ Checking registry format..."
145+ if [[ "$registry" == *"/"* ]]; then
146+ fail "'image_registry' must not contain '/'."
147+ fi
130148 if [[ ! "$registry" =~ ^[a-z0-9._-]+(:[0-9]+)?$ ]]; then
131- echo "::error::'registry' contains invalid characters. Found: '$registry'"
132- exit 1
149+ fail "'image_registry' contains invalid characters. Allowed: [a-z0-9._-] and optional :port"
133150 fi
151+ echo "✅ Registry format OK."
134152
135- # Namespace: may contain '/', but no tag/digest
136- if [[ "$namespace" == *":"* ]] || [[ "$namespace" == *"@"* ]]; then
137- echo "::error::'namespace' must not include a tag or digest. Found: '$namespace'"
138- exit 1
153+ echo "→ Checking namespace format..."
154+ if [[ "$namespace" == *":"* || "$namespace" == *"@"* ]]; then
155+ fail "'image_namespace' must not include a tag or digest."
156+ fi
157+ if [[ "$namespace" == /* || "$namespace" == */ || "$namespace" == *"//"* ]]; then
158+ fail "'image_namespace' cannot start/end with '/' or contain '//'."
139159 fi
160+ IFS='/' read -r -a parts <<< "$namespace"
161+ for seg in "${parts[@]}"; do
162+ if [[ ! "$seg" =~ ^[a-z0-9._-]+$ ]]; then
163+ fail "'image_namespace' segment '${seg}' contains invalid characters."
164+ fi
165+ done
166+ echo "✅ Namespace format OK."
140167
141- # Image name: alphanum + ._-, no slash
168+ echo "→ Checking image name format..."
169+ if [[ "$image_name" == *"/"* ]]; then
170+ fail "'image_name' must not contain '/'."
171+ fi
142172 if [[ ! "$image_name" =~ ^[a-z0-9._-]+$ ]]; then
143- echo "::error::'image_name' contains invalid characters. Found: '$image_name'"
144- exit 1
173+ fail "'image_name' contains invalid characters. Allowed: [a-z0-9._-]."
145174 fi
175+ echo "✅ Image name format OK."
176+
177+ echo "🎯 All image format validations passed successfully."
178+
179+ - name : " Validate conditional inputs"
180+ id : validate-conditional
181+ env :
182+ MODE : ${{ inputs.mode }}
183+ REGISTRY : ${{ inputs.image_registry }}
184+ TAG_SUFFIX : ${{ inputs.tag_suffix }}
185+ CVMFS_DEST_DIR : ${{ inputs.cvmfs_dest_dir }}
186+ CVMFS_REMOVE_TAGS : ${{ inputs.cvmfs_remove_tags }}
187+ EXTRA_BUILD_TAG : ${{ inputs.extra_build_tag }}
188+ BUILD_PLATFORMS_CSV : ${{ inputs.build_platforms_csv }}
189+ REG_USER : ${{ inputs.image_registry == 'ghcr.io' && '' || secrets.registry_username }}
190+ REG_TOKEN : ${{ inputs.image_registry == 'ghcr.io' && secrets.GITHUB_TOKEN || secrets.registry_token }}
191+ CVMFS_GH_TOKEN : ${{ secrets.cvmfs_github_token }}
192+ run : |
193+ set -euo pipefail; echo "now: $(date -u +"%Y-%m-%dT%H:%M:%S.%3N")"
194+
195+ fail() { echo "::error::$1"; exit 1; }
196+
197+ echo "──────────────────────────────────────────────"
198+ echo "🧩 VALIDATING CONDITIONAL INPUTS"
199+ echo "──────────────────────────────────────────────"
200+
201+ ########################################################################
202+ # MODE
203+ ########################################################################
204+ echo "→ Checking mode is one of {BUILD, CVMFS_BUILD, CVMFS_REMOVE, CVMFS_REMOVE_THEN_BUILD}..."
205+ if [[ "${MODE}" == "BUILD" ]]; then
206+ :
207+ elif [[ "${MODE}" == "CVMFS_BUILD" ]]; then
208+ :
209+ elif [[ "${MODE}" == "CVMFS_REMOVE" ]]; then
210+ :
211+ elif [[ "${MODE}" == "CVMFS_REMOVE_THEN_BUILD" ]]; then
212+ :
213+ else
214+ fail "Invalid 'mode'='${MODE}'. Must be one of: BUILD, CVMFS_BUILD, CVMFS_REMOVE, CVMFS_REMOVE_THEN_BUILD."
215+ fi
216+ echo "✅ Mode '${MODE}' OK."
217+
218+ ########################################################################
219+ # TAG_SUFFIX
220+ ########################################################################
221+ echo "→ Checking tag_suffix (optional, charset)..."
222+ if [[ -n "${TAG_SUFFIX:-}" ]]; then
223+ if [[ ! "${TAG_SUFFIX}" =~ ^[A-Za-z0-9._-]+$ ]]; then
224+ fail "'tag_suffix' contains invalid characters. Allowed: [A-Za-z0-9._-]"
225+ fi
226+ echo "✅ tag_suffix '${TAG_SUFFIX}' OK."
227+ else
228+ echo "✅ tag_suffix not provided (optional)."
229+ fi
230+
231+ ########################################################################
232+ # EXTRA_BUILD_TAG
233+ ########################################################################
234+ echo "→ Checking extra_build_tag (optional, charset, not 'latest' or 'latest-<suffix>')..."
235+ if [[ -n "${EXTRA_BUILD_TAG:-}" ]]; then
236+ if [[ ! "${EXTRA_BUILD_TAG}" =~ ^[A-Za-z0-9._-]+$ ]]; then
237+ fail "'extra_build_tag' contains invalid characters. Allowed: [A-Za-z0-9._-]"
238+ fi
239+ if [[ "${EXTRA_BUILD_TAG}" == "latest" ]]; then
240+ fail "'extra_build_tag' must not be 'latest'."
241+ fi
242+ if [[ -n "${TAG_SUFFIX:-}" ]]; then
243+ _forbidden="latest-${TAG_SUFFIX}"
244+ if [[ "${EXTRA_BUILD_TAG}" == "${_forbidden}" ]]; then
245+ fail "'extra_build_tag' must not be '${_forbidden}'."
246+ fi
247+ fi
248+ echo "✅ extra_build_tag '${EXTRA_BUILD_TAG}' OK."
249+ else
250+ echo "✅ extra_build_tag not provided (optional)."
251+ fi
252+
253+ ########################################################################
254+ # BUILD-MODE FLAG
255+ ########################################################################
256+ _is_build_mode=false
257+ if [[ "${MODE}" == "BUILD" ]]; then
258+ _is_build_mode=true
259+ elif [[ "${MODE}" == "CVMFS_BUILD" ]]; then
260+ _is_build_mode=true
261+ elif [[ "${MODE}" == "CVMFS_REMOVE_THEN_BUILD" ]]; then
262+ _is_build_mode=true
263+ fi
264+
265+ ########################################################################
266+ # REGISTRY AUTH (only when building)
267+ ########################################################################
268+ if [[ "${_is_build_mode}" == "true" ]]; then
269+ echo "→ Checking registry authentication requirements for '${REGISTRY}'..."
270+ if [[ "${REGISTRY}" == "ghcr.io" ]]; then
271+ if [[ -z "${REG_TOKEN:-}" ]]; then
272+ fail "'mode'=${MODE} requires publishing: missing token for ghcr.io (GITHUB_TOKEN)."
273+ fi
274+ echo "✅ GHCR token OK."
275+ else
276+ if [[ -z "${REG_USER:-}" && -n "${REG_TOKEN:-}" ]]; then
277+ fail "Publishing to ${REGISTRY} requires BOTH username and token. Username missing."
278+ fi
279+ if [[ -n "${REG_USER:-}" && -z "${REG_TOKEN:-}" ]]; then
280+ fail "Publishing to ${REGISTRY} requires BOTH username and token. Token missing."
281+ fi
282+ if [[ -z "${REG_USER:-}" ]]; then
283+ fail "'mode'=${MODE} requires publishing: username required for ${REGISTRY}."
284+ fi
285+ if [[ -z "${REG_TOKEN:-}" ]]; then
286+ fail "'mode'=${MODE} requires publishing: token required for ${REGISTRY}."
287+ fi
288+ echo "✅ ${REGISTRY} credentials (username + token) OK."
289+ fi
290+ else
291+ echo "✅ Registry auth not required for mode '${MODE}'."
292+ fi
293+
294+ ########################################################################
295+ # BUILD PLATFORMS (only when building)
296+ ########################################################################
297+ if [[ "${_is_build_mode}" == "true" ]]; then
298+ echo "→ Checking build_platforms_csv presence and format..."
299+ if [[ -z "${BUILD_PLATFORMS_CSV:-}" ]]; then
300+ fail "'build_platforms_csv' is empty but required when building."
301+ fi
302+ IFS=',' read -r -a _plats <<< "${BUILD_PLATFORMS_CSV}"
303+ if (( ${#_plats[@]} == 0 )); then
304+ fail "'build_platforms_csv' must list at least one platform (e.g., linux/amd64)."
305+ fi
306+ for p in "${_plats[@]}"; do
307+ _trimmed="$(echo "$p" | xargs)"
308+ if [[ -z "${_trimmed}" ]]; then
309+ fail "'build_platforms_csv' contains an empty entry."
310+ fi
311+ if [[ ! "${_trimmed}" =~ ^[A-Za-z0-9._-]+/[A-Za-z0-9._-]+$ ]]; then
312+ fail "'build_platforms_csv' entry '${_trimmed}' is not valid (e.g., linux/amd64)."
313+ fi
314+ done
315+ echo "✅ build_platforms_csv OK."
316+ else
317+ echo "✅ build_platforms_csv not required for mode '${MODE}'."
318+ fi
319+
320+ ########################################################################
321+ # CVMFS-MODE FLAG
322+ ########################################################################
323+ _touches_cvmfs=false
324+ if [[ "${MODE}" == "CVMFS_BUILD" ]]; then
325+ _touches_cvmfs=true
326+ elif [[ "${MODE}" == "CVMFS_REMOVE" ]]; then
327+ _touches_cvmfs=true
328+ elif [[ "${MODE}" == "CVMFS_REMOVE_THEN_BUILD" ]]; then
329+ _touches_cvmfs=true
330+ fi
331+
332+ ########################################################################
333+ # CVMFS PREREQUISITES (any mode that touches CVMFS)
334+ ########################################################################
335+ if [[ "${_touches_cvmfs}" == "true" ]]; then
336+ echo "→ Checking CVMFS prerequisites (dest_dir and github_token)..."
337+ if [[ -z "${CVMFS_DEST_DIR:-}" ]]; then
338+ fail "'mode'=${MODE} touches CVMFS: 'inputs.cvmfs_dest_dir' is required."
339+ fi
340+ if [[ "${CVMFS_DEST_DIR}" =~ [[:space:]] ]]; then
341+ fail "'cvmfs_dest_dir' must not contain whitespace."
342+ fi
343+ if [[ -z "${CVMFS_GH_TOKEN:-}" ]]; then
344+ fail "'mode'=${MODE} touches CVMFS: 'secrets.cvmfs_github_token' is required."
345+ fi
346+ echo "✅ CVMFS prerequisites OK."
347+ else
348+ echo "✅ CVMFS prerequisites not required for mode '${MODE}'."
349+ fi
350+
351+ ########################################################################
352+ # REMOVAL-MODE FLAG
353+ ########################################################################
354+ _is_remove_mode=false
355+ if [[ "${MODE}" == "CVMFS_REMOVE" ]]; then
356+ _is_remove_mode=true
357+ elif [[ "${MODE}" == "CVMFS_REMOVE_THEN_BUILD" ]]; then
358+ _is_remove_mode=true
359+ fi
360+
361+ ########################################################################
362+ # CVMFS REMOVAL TAGS (only in removal modes)
363+ ########################################################################
364+ if [[ "${_is_remove_mode}" == "true" ]]; then
365+ echo "→ Checking cvmfs_remove_tags content and charset..."
366+ if ! awk 'NF{exit 0} END{exit 1}' <<<"${CVMFS_REMOVE_TAGS:-}"; then
367+ fail "'mode'=${MODE} requires 'inputs.cvmfs_remove_tags' (one tag per line)."
368+ fi
369+ while IFS= read -r _line; do
370+ _trimmed="$(echo "${_line}" | xargs)"
371+ if [[ -z "${_trimmed}" ]]; then
372+ continue
373+ fi
374+ if [[ ! "${_trimmed}" =~ ^[A-Za-z0-9._-]+$ ]]; then
375+ fail "'cvmfs_remove_tags' entry '${_trimmed}' contains invalid characters. Allowed: [A-Za-z0-9._-]"
376+ fi
377+ done <<< "${CVMFS_REMOVE_TAGS:-}"
378+ echo "✅ cvmfs_remove_tags OK."
379+ else
380+ echo "✅ cvmfs_remove_tags not required for mode '${MODE}'."
381+ fi
382+
383+ echo "🎯 All conditional input validations passed successfully."
146384
147385 - name : " Normalize suffix for use in artifacts"
148386 id : suffix_norm_for_artifacts
@@ -277,7 +515,7 @@ jobs:
277515
278516
279517 maybe-remove-from-cvmfs :
280- needs : [ gameplan ]
518+ needs : [ validate-and-plan ]
281519 runs-on : ubuntu-latest
282520 if : >
283521 inputs.mode == 'CVMFS_REMOVE' ||
@@ -333,10 +571,10 @@ jobs:
333571 inputs.mode == 'BUILD' ||
334572 inputs.mode == 'CVMFS_BUILD' ||
335573 inputs.mode == 'CVMFS_REMOVE_THEN_BUILD'
336- needs : [ gameplan ]
574+ needs : [ validate-and-plan ]
337575 strategy :
338576 matrix :
339- include : ${{ fromJson(needs.gameplan .outputs.build-platform-matrix) }}
577+ include : ${{ fromJson(needs.validate-and-plan .outputs.build-platform-matrix) }}
340578 runs-on : ${{ matrix.runner }}
341579 steps :
342580 - uses : actions/checkout@v4
@@ -396,7 +634,7 @@ jobs:
396634 # === UPLOAD ===
397635 - uses : actions/upload-artifact@v4
398636 with :
399- name : platform-digests-${{ inputs.image_name }}-${{ needs.gameplan .outputs.normalized-suffix }}-${{ steps.save_digest.outputs.safe_platform }}
637+ name : platform-digests-${{ inputs.image_name }}-${{ needs.validate-and-plan .outputs.normalized-suffix }}-${{ steps.save_digest.outputs.safe_platform }}
400638 path : ${{ runner.temp }}/to-be-artifacts/*
401639 if-no-files-found : error
402640 retention-days : 1
@@ -409,7 +647,7 @@ jobs:
409647 inputs.mode == 'CVMFS_BUILD' ||
410648 inputs.mode == 'CVMFS_REMOVE_THEN_BUILD'
411649 needs : [
412- gameplan ,
650+ validate-and-plan ,
413651 build-w-platform-and-publish-by-sha, # needed b/c image(s) need to be published before tagging/merging
414652 ]
415653 runs-on : ubuntu-latest
@@ -475,7 +713,7 @@ jobs:
475713 # === DOWNLOAD ===
476714 - uses : actions/download-artifact@v4
477715 with :
478- pattern : platform-digests-${{ inputs.image_name }}-${{ needs.gameplan .outputs.normalized-suffix }}-*
716+ pattern : platform-digests-${{ inputs.image_name }}-${{ needs.validate-and-plan .outputs.normalized-suffix }}-*
479717 path : platform-digests
480718 merge-multiple : true
481719
@@ -596,7 +834,7 @@ jobs:
596834 inputs.mode == 'CVMFS_BUILD' ||
597835 inputs.mode == 'CVMFS_REMOVE_THEN_BUILD'
598836 needs : [
599- gameplan ,
837+ validate-and-plan ,
600838 maybe-remove-from-cvmfs, # needed b/c cvmfs removals need to happen before builds
601839 tag-and-merge-published-images, # needed for retrieving images
602840 ]
@@ -676,7 +914,7 @@ jobs:
676914 if : always()
677915 needs : [
678916 # after all jobs:
679- gameplan ,
917+ validate-and-plan ,
680918 maybe-remove-from-cvmfs,
681919 build-w-platform-and-publish-by-sha,
682920 tag-and-merge-published-images,
@@ -691,7 +929,7 @@ jobs:
691929 inputs.mode == 'CVMFS_REMOVE_THEN_BUILD'
692930 uses : actions/download-artifact@v4
693931 with :
694- pattern : platform-digests-${{ inputs.image_name }}-${{ needs.gameplan .outputs.normalized-suffix }}-*
932+ pattern : platform-digests-${{ inputs.image_name }}-${{ needs.validate-and-plan .outputs.normalized-suffix }}-*
695933 path : platform-digests
696934 merge-multiple : true
697935
0 commit comments