Skip to content

Commit d70afda

Browse files
authored
image-publish: Validate conditional inputs and secrets (#15)
1 parent bf017a3 commit d70afda

File tree

1 file changed

+259
-21
lines changed

1 file changed

+259
-21
lines changed

.github/workflows/image-publish.yml

Lines changed: 259 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -110,39 +110,277 @@ on:
110110
required: false
111111

112112
jobs:
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

Comments
 (0)