/ 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