/ ci-build
ci-build
  1  #!/bin/env bash
  2  
  3  ##########
  4  # ci-build
  5  # Copyright 2025 by Dirk Gottschalk
  6  #
  7  # SPDX-License-Identifier: MIT
  8  #
  9  # This script executes CI/CD tasks (mostly) in containers.
 10  # It can be used either by the radicle-native-ci or from a git
 11  # post-recieve hook.
 12  ##########
 13  
 14  # Exit on error and on unbound variables
 15  set -euo pipefail
 16  
 17  # Initialize script variables because we use 'set -u' and set sane defaults.
 18  # These variables will mostly be overridden by the config file.
 19  PIPELINE=""
 20  PROJECT=""
 21  WORKDIR="."
 22  CONFIG=".radicle/build.conf"
 23  BUILDSCRIPT=""
 24  SCRIPT_ARGS=""
 25  RUNNER=""
 26  OUTPUT_DIR=""
 27  OUTPUT_MOUNT=""
 28  UPLOAD="false"
 29  TAG="latest"
 30  ARCHES=()
 31  SQUASH="false"
 32  DISABLE_CACHE="false"
 33  ENCRYPT="false"
 34  ENCRYPT_KEY=""
 35  SIGN="false"
 36  EMBED_KEYS="false"
 37  CONTAINERFILE=""
 38  BUILD_ARGS=()
 39  REG_USER=""
 40  REG_ADDR=""
 41  BOOTC_SECUREBOOT_SIGN="false"
 42  BOOTC_SECUREBOOT_SCOPE=""
 43  SIGSTORE_CONFIG=""
 44  BOOTC_SIGN_KEY=""
 45  BOOTC_SIGN_CERT=""
 46  PACKAGES=""
 47  
 48  # Parse command line options
 49  if ! ARGS=$(getopt -o c: --long config: -n "$(basename "$0")" -- "$@"); then
 50  	echo "Error: Failed to parse options. Please check your command."
 51  	exit 1
 52  fi
 53  
 54  eval set -- "${ARGS}"
 55  
 56  while true; do
 57  	case "$1" in
 58  		-c | --config)
 59  			CONFIG=$2
 60  			shift 2
 61  			;;
 62  		--)
 63  			shift
 64  			break
 65  			;;
 66  		*)
 67  			echo "Internal error in option parsing!" >&2
 68  			exit 1
 69  			;;
 70  	esac
 71  done
 72  
 73  # Check if config file is available
 74  [[ -z ${CONFIG} || ! -f ${CONFIG} ]] && {
 75  	echo "Missing config file!"
 76  	exit 1
 77  }
 78  
 79  # shellcheck source=/dev/null
 80  source  "${CONFIG}"
 81  
 82  [[ ${#ARCHES[@]} -eq 0 ]] && {
 83  	echo "No architectures specified!"
 84  	exit 1
 85  }
 86  
 87  # We no longer support setting remote hosts in the projects config, or reading
 88  # credentials and ssh keys from the repository so overwrite them if set.
 89  AMD64_REMOTE=""
 90  ARM64_REMOTE=""
 91  RISCV64_REMOTE=""
 92  SSH_KEYFILE="${HOME}/.secrets/${REG_USER}.pub"
 93  SIGSTORE_CONFIG="${HOME}/.config/rad-ci/${REG_USER}.yaml"
 94  
 95  # This parameters shall also never be read from the config file, so override them if set.
 96  BUILD_ID="$(date -u +%Y%m%d.%H%M)"
 97  COMMIT="$(git rev-parse --short HEAD)"
 98  
 99  # Abort if project name is empty
100  [[ -z "${PROJECT}" ]] && {
101  	echo "Project name not set in config file!"
102  	exit 1
103  }
104  
105  case "${PIPELINE,,}" in
106  	podman | docker | podman-image | docker-image | oci-image | bootc | bootc-image)
107  		# Check for sigstore configuration if signing of e.g. podman images is enabled.
108  		[[ ${SIGN} =~ ^(true|yes)$ && ! -f ${SIGSTORE_CONFIG} ]] && {
109  			echo "Missing image signature config!"
110  			exit 1
111  		}
112  
113  		# We handle "rpi" as a special tag for Raspberry PI images
114  		# So we build only arm64 in this case
115  		[[ "${TAG}" == "rpi" ]] && ARCHES=("arm64")
116  
117  		# If secureboot signature of bootc images is enabled, make sure the
118  		# 'scope' is set and the files exist.
119  		[[ ${BOOTC_SECUREBOOT_SIGN,,} =~ ^(true|yes)$ ]] && {
120  			[[ -z ${BOOTC_SECUREBOOT_SCOPE} ]] && {
121  				echo "Missing secureboot scope!"
122  				exit 1
123  			}
124  
125  			[[ ! -f ${BOOTC_SIGN_KEY} || ! -f ${BOOTC_SIGN_CERT} ]] && {
126  				echo "Missing keys!"
127  				exit 1
128  			}
129  		}
130  
131  		# Check vor upload information if upload is requested
132  		[[ ${UPLOAD,,} =~ ^(true|yes)$ && -z ${REG_USER} || -z ${REG_ADDR} ]] && {
133  			echo "UPLOAD is true but no target is specified, disabling Upload."
134  			UPLOAD="false"
135  		}
136  
137  		# Delete Manifest if it exists
138  		podman manifest exists "${PROJECT}:${TAG}" &&
139  			podman manifest rm "${PROJECT}:${TAG}"
140  
141  		# Create an empty new Manifest
142  		podman manifest create "${PROJECT}:${TAG}"
143  
144  		# Build each requested architecture
145  		for arch in "${ARCHES[@]}"; do
146  			# Set apropriate build hosts
147  			case "$arch" in
148  				amd64) RUN_HOST="${AMD64_REMOTE}" ;;
149  				arm64) RUN_HOST="${ARM64_REMOTE}" ;;
150  				riscv64) RUN_HOST="${RISCV64_REMOTE}" ;;
151  				*) RUN_HOST="" ;;
152  			esac
153  
154  			# Initialize arguments array for podman
155  			podman_args=()
156  
157  			# Add custom build args from config
158  			for arg in "${BUILD_ARGS[@]}"; do
159  				podman_args+=(--build-arg "${arg}")
160  			done
161  
162  			# Override riscv64 image since Fedora does not yet provide an
163  			# official image
164  			[[ ${arch} == "riscv64" ]] &&
165  				podman_args+=(--from docker.io/dirk1980/fedora-riscv64:latest)
166  
167  			# Set other arguments
168  			[[ ${BOOTC_SECUREBOOT_SIGN,,} =~ ^(true|yes)$ ]] &&
169  				podman_args+=(
170  					"--secret id=secure_key,src=${BOOTC_SIGN_KEY}"
171  					"--secret id=secure_cert,src=${BOOTC_SIGN_CERT}"
172  				)
173  
174  			[[ ${EMBED_KEYS,,} =~ ^(true|yes)$ ]] &&
175  				podman_args+=(--build-arg "sshkeys=$(cat "${SSH_KEYFILE}")")
176  
177  			[[ -n ${COMMIT} ]] && podman_args+=(--build-arg "commit=${COMMIT}")
178  			[[ -n ${RUN_HOST} ]] && podman_args+=(-c "${RUN_HOST}")
179  			[[ -n ${CONTAINERFILE} ]] && podman_args+=(-f "${CONTAINERFILE}")
180  			[[ ${SQUASH,,} =~ ^(true|yes)$ ]] && podman_args+=(--squash-all)
181  			[[ ${DISABLE_CACHE,,} =~ ^(true|yes)$ ]] && podman_args+=(--no-cache)
182  
183  			[[ ${arch} == "arm64" && "${TAG}" == "rpi" ]] &&
184  				podman_args+=(--build-arg "build_rpi=true")
185  
186  			podman_args+=(
187  				--rm
188  				--arch "${arch}"
189  				--build-arg "buildid=${BUILD_ID}"
190  				--security-opt label=type:unconfined_t
191  				--pull=always
192  				--network host
193  				-t "${PROJECT}:${TAG}-${arch}"
194  				"${WORKDIR}"
195  			)
196  
197  			podman build "${podman_args[@]}"
198  
199  			# Copy the image if this was a remote build
200  			[[ -n ${RUN_HOST} ]] &&
201  				podman image scp "${RUN_HOST}::${PROJECT}:${TAG}-${arch}" localhost::
202  
203  			# Add image to manifest
204  			podman manifest add "${PROJECT}:${TAG}" "${PROJECT}:${TAG}-${arch}"
205  		done
206  
207  		# Push image if UPLOAD is yes or true
208  		if [[ ${UPLOAD,,} =~ ^(true|yes)$ ]]; then
209  			push_args=()
210  
211  			[[ ${ENCRYPT,,} =~ ^(true|yes)$ ]] &&
212  				push_args+=(--encryption-key="${ENCRYPT_KEY}")
213  
214  			[[ ${SIGN,,} =~ ^(true|yes)$ ]] &&
215  				push_args+=(--sign-by-sigstore "${SIGSTORE_CONFIG}")
216  
217  			push_args+=("${PROJECT}:${TAG}")
218  			push_args+=("${REG_ADDR}/${REG_USER}/${PROJECT}:${TAG}")
219  			podman manifest push "${push_args[@]}"
220  		fi
221  		;;
222  
223  	generic | src-build)
224  		# Generic Workflow (Uses 'podman run')
225  		for arch in "${ARCHES[@]}"; do
226  			case "$arch" in
227  				amd64) RUN_HOST="${AMD64_REMOTE}" ;;
228  				arm64) RUN_HOST="${ARM64_REMOTE}" ;;
229  				riscv64) RUN_HOST="${RISCV64_REMOTE}" ;;
230  				*) RUN_HOST="" ;;
231  			esac
232  
233  			podman_args=()
234  			[[ -n ${RUN_HOST} ]] && podman_args+=(-c "${RUN_HOST}")
235  
236  			podman_args+=(
237  				--rm
238  				--pull=always
239  				--arch "${arch}"
240  				--security-opt label=type:unconfined_t
241  				-v "${WORKDIR}:/workdir"
242  			)
243  
244  			[[ -n ${OUTPUT_DIR} && -n ${OUTPUT_MOUNT} ]] &&
245  				podman_args+=(-v "${OUTPUT_DIR}":"${OUTPUT_MOUNT}")
246  
247  			[[ -n "${PACKAGES}" ]] && podman_args+=(--env "PACKAGES=${PACKAGES}")
248  			[[ -n "${PIPELINE}" ]] && podman_args+=(--env "PIPELINE=${PIPELINE}")
249  			[[ -z ${RUNNER} ]] && RUNNER="docker.io/dirk1980/ci-fedora-rpm:latest"
250  			podman_args+=("${RUNNER}")
251  
252  			[[ -n ${BUILDSCRIPT} ]] &&
253  				podman_args+=("${BUILDSCRIPT}" "${SCRIPT_ARGS}")
254  
255  			podman run "${podman_args[@]}"
256  		done
257  		;;
258  	*)
259  		echo "Error: Unknown pipeline '${PIPELINE}' specified in config file."
260  		exit 1
261  		;;
262  esac