99 required : true
1010 type : string
1111 image-name :
12- description : " Name of the Docker image to build"
12+ description : " Name of the Docker image to build, without registry or tag. E.g. 'my-image' or 'my-org/my-image' "
1313 required : true
1414 type : string
15+ devcontainer-metadata :
16+ description : " Path to a JSON file containing devcontainer metadata to add as a label to the built image"
17+ required : false
18+ type : string
1519 registry :
16- description : " Docker registry to push built containers to"
20+ description : " Docker registry to push built containers to, DOCKER_USERNAME and DOCKER_PASSWORD secrets must be set if not using GitHub Container Registry "
1721 required : false
1822 type : string
1923 default : " ghcr.io"
2529 required : false
2630 type : string
2731 default : ' {"runner": ["ubuntu-latest", "ubuntu-24.04-arm"]}'
28- merge -runner :
29- description : " Runner label for the merge-image job "
32+ default -runner :
33+ description : " Runner label for the non-build jobs "
3034 required : false
3135 type : string
3236 default : ubuntu-latest
3842 description : " Password or token for Docker login, if not provided the GITHUB_TOKEN will be used"
3943 required : false
4044
41- permissions :
42- contents : read
43-
44- env :
45- REGISTRY : ${{ inputs.registry }}
46- FULLY_QUALIFIED_IMAGE_NAME : ${{ inputs.registry }}/${{ inputs.image-name }}
45+ permissions : {}
4746
4847jobs :
48+ sanitize-inputs :
49+ runs-on : ${{ inputs.default-runner }}
50+ outputs :
51+ image-name : ${{ steps.sanitize-image-name.outputs.sanitized-image-name }}
52+ fully-qualified-image-name : ${{ inputs.registry }}/${{ steps.sanitize-image-name.outputs.sanitized-image-name }}
53+ image-basename : ${{ steps.sanitize-image-name.outputs.sanitized-basename }}
54+ steps :
55+ - name : Sanitize image name
56+ id : sanitize-image-name
57+ env :
58+ IMAGE_NAME : ${{ inputs.image-name }}
59+ run : |
60+ set -Eeuo pipefail
61+
62+ # Split all image name components (on '/') and sanitize each component independently.
63+ # Rules: lowercase; allowed chars a-z0-9._- ; collapse invalid sequences to single '-'; trim leading/trailing '-'.
64+ IFS='/' read -r -a PARTS <<< "$IMAGE_NAME"
65+ SANITIZED_PARTS=()
66+
67+ for PART in "${PARTS[@]}"; do
68+ SANITIZED_PART=$(echo "$PART" | tr '[:upper:]' '[:lower:]' | sed -E 's/[^a-z0-9._-]+/-/g' | sed -E 's/^-+|-+$//g')
69+ if [ -z "$SANITIZED_PART" ]; then
70+ echo "Invalid or empty component after sanitization in image component: '$PART', please correct your image name: '$IMAGE_NAME'" >&2
71+ exit 1
72+ fi
73+ SANITIZED_PARTS+=("$SANITIZED_PART")
74+ done
75+
76+ SANITIZED_IMAGE_NAME=$(IFS='/'; echo "${SANITIZED_PARTS[*]}")
77+ SANITIZED_BASENAME=${SANITIZED_PARTS[-1]}
78+ echo "sanitized-image-name=$SANITIZED_IMAGE_NAME" >> "$GITHUB_OUTPUT"
79+ echo "sanitized-basename=$SANITIZED_BASENAME" >> "$GITHUB_OUTPUT"
80+
4981 build-push :
5082 strategy :
5183 matrix : ${{ fromJson(inputs.build-matrix) }}
5284 runs-on : ${{ matrix.runner }}
85+ needs : sanitize-inputs
5386 permissions :
87+ contents : read
5488 packages : write
5589 steps :
5690 - uses : step-security/harden-runner@f4a75cfd619ee5ce8d5b864b0d183aff3c69b55a # v2.13.1
@@ -66,15 +100,15 @@ jobs:
66100 USERNAME : ${{ secrets.DOCKER_USERNAME || github.actor }}
67101 PASSWORD : ${{ secrets.DOCKER_PASSWORD || secrets.GITHUB_TOKEN }}
68102 with :
69- registry : ${{ env.REGISTRY }}
103+ registry : ${{ inputs.registry }}
70104 username : ${{ env.USERNAME }}
71105 password : ${{ env.PASSWORD }}
72106 - uses : docker/metadata-action@c1e51972afc2121e065aed6d45c65596fe445f3f # v5.8.0
73107 env :
74108 DOCKER_METADATA_SET_OUTPUT_ENV : false
75109 id : metadata
76110 with :
77- images : ${{ env.FULLY_QUALIFIED_IMAGE_NAME }}
111+ images : ${{ needs.sanitize-inputs.outputs.fully-qualified-image-name }}
78112 # Generate image LABEL for devcontainer.metadata
79113 # the sed expression is a workaround for quotes being eaten in arrays (e.g. ["x", "y", "z"] -> ["x",y,"z"])
80114 - run : echo "metadata=$(jq -cj '[.]' ".devcontainer/${CONTAINER_FLAVOR}/devcontainer-metadata-vscode.json" | sed 's/,"/, "/g')" >> "$GITHUB_OUTPUT"
90124 with :
91125 file : ${{ inputs.dockerfile }}
92126 push : true
93- tags : ${{ env.FULLY_QUALIFIED_IMAGE_NAME }}
127+ tags : ${{ needs.sanitize-inputs.outputs.fully-qualified-image-name }}
94128 labels : |
95129 ${{ steps.metadata.outputs.labels }}
96130 devcontainer.metadata=${{ steps.devcontainer-metadata.outputs.metadata }}
@@ -107,13 +141,13 @@ jobs:
107141 RUNNER_TEMP : ${{ runner.temp }}
108142 - uses : actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
109143 with :
110- name : digests-${{ inputs.image-name }}-${{ steps.devcontainer-arch.outputs.arch }}
144+ name : digests-${{ needs.sanitize- inputs.outputs. image-basename }}-${{ steps.devcontainer-arch.outputs.arch }}
111145 path : ${{ runner.temp }}/digests/*
112146 if-no-files-found : error
113147 retention-days : 1
114148
115149 merge-image :
116- runs-on : ${{ inputs.merge -runner }}
150+ runs-on : ${{ inputs.default -runner }}
117151 needs : build-push
118152 permissions :
119153 actions : read
@@ -135,15 +169,15 @@ jobs:
135169 - uses : actions/download-artifact@634f93cb2916e3fdff6788551b99b062d0335ce0 # v5.0.0
136170 with :
137171 path : ${{ runner.temp }}/digests
138- pattern : digests-${{ inputs.image-name }}-*
172+ pattern : digests-${{ needs.sanitize- inputs.outputs. image-basename }}-*
139173 merge-multiple : true
140174 - uses : docker/setup-buildx-action@e468171a9de216ec08956ac3ada2f0791b6bd435 # v3.11.1
141175 - uses : docker/login-action@5e57cd118135c172c3672efd75eb46360885c0ef # v3.6.0
142176 env :
143177 USERNAME : ${{ secrets.DOCKER_USERNAME || github.actor }}
144178 PASSWORD : ${{ secrets.DOCKER_PASSWORD || secrets.GITHUB_TOKEN }}
145179 with :
146- registry : ${{ env.REGISTRY }}
180+ registry : ${{ inputs.registry }}
147181 username : ${{ env.USERNAME }}
148182 password : ${{ env.PASSWORD }}
149183 - uses : docker/metadata-action@c1e51972afc2121e065aed6d45c65596fe445f3f # v5.8.0
@@ -152,7 +186,7 @@ jobs:
152186 DOCKER_METADATA_ANNOTATIONS_LEVELS : index
153187 DOCKER_METADATA_SET_OUTPUT_ENV : false
154188 with :
155- images : ${{ env.FULLY_QUALIFIED_IMAGE_NAME }}
189+ images : ${{ needs.sanitize-inputs.outputs.fully-qualified-image-name }}
156190 # Generate Docker tags based on the following events/attributes.
157191 # To prevent unnecessary image builds we simulate the `type=edge` tag
158192 # with `type=raw,value=edge,enable=...` which only enables the tag
@@ -165,6 +199,8 @@ jobs:
165199 type=semver,pattern={{major}}.{{minor}}
166200 type=semver,pattern={{major}}
167201 - name : Create manifest list and push
202+ env :
203+ FULLY_QUALIFIED_IMAGE_NAME : ${{ needs.sanitize-inputs.outputs.fully-qualified-image-name }}
168204 run : |
169205 import os
170206 import json
@@ -189,6 +225,8 @@ jobs:
189225 working-directory : ${{ runner.temp }}/digests
190226 - name : Inspect manifest and extract digest
191227 id : inspect-manifest
228+ env :
229+ FULLY_QUALIFIED_IMAGE_NAME : ${{ needs.sanitize-inputs.outputs.fully-qualified-image-name }}
192230 run : |
193231 set -Eeuo pipefail
194232 output=$(docker buildx imagetools inspect "${FULLY_QUALIFIED_IMAGE_NAME}:${CONTAINER_VERSION}" --format '{{json .}}')
@@ -201,35 +239,35 @@ jobs:
201239 chmod +x diffoci
202240 ./diffoci diff --semantic --report-file=container-diff.json "${FROM_CONTAINER}" "${TO_CONTAINER}" || true
203241 env:
204- FROM_CONTAINER: ${{ env.FULLY_QUALIFIED_IMAGE_NAME }}:edge
205- TO_CONTAINER: ${{ env.FULLY_QUALIFIED_IMAGE_NAME }}:${{ steps.metadata.outputs.version }}
242+ FROM_CONTAINER: ${{ needs.sanitize-inputs.outputs.fully-qualified-image-name }}:edge
243+ TO_CONTAINER: ${{ needs.sanitize-inputs.outputs.fully-qualified-image-name }}:${{ steps.metadata.outputs.version }}
206244 - uses : actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
207245 with :
208- name : container-diff-${{ inputs.image-name }}
246+ name : container-diff-${{ needs.sanitize- inputs.outputs. image-basedname }}
209247 path : container-diff.json
210248 retention-days : 10
211249 - uses : ./.github/actions/container-size-diff
212250 id : container-size-diff
213251 with :
214- from-container : ${{ env.FULLY_QUALIFIED_IMAGE_NAME }}:edge
215- to-container : ${{ env.FULLY_QUALIFIED_IMAGE_NAME }}:${{ steps.metadata.outputs.version }}
252+ from-container : ${{ needs.sanitize-inputs.outputs.fully-qualified-image-name }}:edge
253+ to-container : ${{ needs.sanitize-inputs.outputs.fully-qualified-image-name }}:${{ steps.metadata.outputs.version }}
216254 - uses : marocchino/sticky-pull-request-comment@773744901bac0e8cbb5a0dc842800d45e9b2b405 # v2.9.4
217255 with :
218- header : container-size-diff-${{ inputs.flavor }}
256+ header : container-size-diff-${{ needs.sanitize- inputs.outputs.image-basename }}
219257 message : |
220258 ${{ steps.container-size-diff.outputs.size-diff-markdown }}
221259 - uses : anchore/sbom-action@f8bdd1d8ac5e901a77a92f111440fdb1b593736b # v0.20.6
222260 with :
223- image : ${{ env.FULLY_QUALIFIED_IMAGE_NAME }}@${{ steps.inspect-manifest.outputs.digest }}
261+ image : ${{ needs.sanitize-inputs.outputs.fully-qualified-image-name }}@${{ steps.inspect-manifest.outputs.digest }}
224262 dependency-snapshot : true
225263 - uses : actions/attest-build-provenance@977bb373ede98d70efdf65b84cb5f73e068dcc2a # v3.0.0
226264 with :
227- subject-name : ${{ env.FULLY_QUALIFIED_IMAGE_NAME }}
265+ subject-name : ${{ needs.sanitize-inputs.outputs.fully-qualified-image-name }}
228266 subject-digest : ${{ steps.inspect-manifest.outputs.digest }}
229267 show-summary : false
230268 push-to-registry : true
231269 - name : Verify attestation
232- run : gh attestation verify --repo "${GH_REPO}" "oci://${FULLY_QUALIFIED_IMAGE_NAME }@${DIGEST}"
270+ run : gh attestation verify --repo "${GH_REPO}" "oci://${{ needs.sanitize-inputs.outputs.fully-qualified-image-name } }@${DIGEST}"
233271 env :
234272 DIGEST : ${{ steps.inspect-manifest.outputs.digest }}
235273 GH_REPO : ${{ github.repository }}
0 commit comments