From d34e27e33f1d7fcb059bb654cc8a6c7c81a393f5 Mon Sep 17 00:00:00 2001 From: Diego Garcia Date: Fri, 6 Nov 2026 14:26:33 -0500 Subject: [PATCH 1/5] perf(ci): Pull prebuilt package builder images from ghcr Mount the builder scripts, spec, debian/, VERSION.json, plugins and run-with-retry.sh at run time, so each Dockerfile is the only input of its image. Tag every builder image as ghcr.io/wazuh/pkg_dashboard__builder_:-, where is the short sha256 of the Dockerfile. build-packages.sh pulls that tag and builds it locally when the pull fails, so a missing tag or registry access keeps today's behavior. Generate packages logs in to ghcr with GITHUB_TOKEN to pull it. Signed-off-by: Diego Garcia --- .../workflows/5_builderpackage_dashboard.yml | 14 ++++ .../build-packages/base/Docker/Dockerfile | 12 +--- dev-tools/build-packages/build-packages.sh | 69 +++++++++++-------- .../build-packages/common/builder-image.sh | 61 ++++++++++++++++ .../build-packages/deb/Docker/Dockerfile | 10 +-- .../build-packages/rpm/Docker/Dockerfile | 10 +-- 6 files changed, 125 insertions(+), 51 deletions(-) create mode 100755 dev-tools/build-packages/common/builder-image.sh diff --git a/.github/workflows/5_builderpackage_dashboard.yml b/.github/workflows/5_builderpackage_dashboard.yml index 428a8025b288..1f1716975825 100644 --- a/.github/workflows/5_builderpackage_dashboard.yml +++ b/.github/workflows/5_builderpackage_dashboard.yml @@ -350,6 +350,12 @@ jobs: needs: [setup-variables, get-outputs-plugins, build-dashboard, build-plugins] runs-on: ${{ (inputs.architecture == 'arm64' || inputs.architecture == 'aarch64') && format('codebuild-github-actions-codebuild-runner-dashboard-arm-{0}-{1}', github.run_id, github.run_attempt) || format('codebuild-github-actions-codebuild-runner-dashboard-amd-{0}-{1}', github.run_id, github.run_attempt) }} name: Generate packages + # Job-level permissions replace the workflow ones: keep those and add + # packages: read to pull the prebuilt builder images from ghcr. + permissions: + id-token: write + contents: read + packages: read outputs: PACKAGE_NAME: ${{ steps.get-package-name.outputs.PACKAGE_NAME }} steps: @@ -438,6 +444,14 @@ jobs: sudo nohup dockerd --storage-driver vfs > /var/log/dockerd.log 2>&1 & timeout 60 bash -c 'until docker info &>/dev/null 2>&1; do sleep 1; done' + # build-packages.sh builds the builder images locally when it can't pull + # them, so a failed login must not stop the job. + - name: Login to GitHub Container Registry + continue-on-error: true + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: echo "$GITHUB_TOKEN" | docker login ghcr.io -u "$GITHUB_ACTOR" --password-stdin + - name: Build package run: | cd ${{ github.workspace }}/dev-tools/build-packages diff --git a/dev-tools/build-packages/base/Docker/Dockerfile b/dev-tools/build-packages/base/Docker/Dockerfile index efd99054cde3..ccd560a354c2 100644 --- a/dev-tools/build-packages/base/Docker/Dockerfile +++ b/dev-tools/build-packages/base/Docker/Dockerfile @@ -28,12 +28,6 @@ RUN cd brotli && chmod +x ./bootstrap && ./bootstrap && ./configure --prefix=/us RUN mkdir -p /usr/local/lib/wazuh -# Add the scripts to build the RPM package -ADD base-builder.sh /usr/local/bin/builder -ADD VERSION.json /usr/local/bin/VERSION.json -ADD plugins /usr/local/bin/plugins -ADD run-with-retry.sh /usr/local/lib/wazuh/run-with-retry.sh -RUN chmod +x /usr/local/bin/builder - -# Set the entrypoint -ENTRYPOINT ["/usr/local/bin/builder"] +# The builder script and its inputs are mounted at run time by +# build-packages.sh, so this Dockerfile is the only input of the image. +ENTRYPOINT ["/bin/bash", "/usr/local/bin/builder"] diff --git a/dev-tools/build-packages/build-packages.sh b/dev-tools/build-packages/build-packages.sh index 6b49d9f6747d..e525fec593b1 100755 --- a/dev-tools/build-packages/build-packages.sh +++ b/dev-tools/build-packages/build-packages.sh @@ -2,7 +2,7 @@ set -e current_path="$( cd $(dirname $0) ; pwd -P )" -root_dir="${current_path}/../.." +root_dir="$( cd "${current_path}/../.." ; pwd -P )" app="" base="" revision="1" @@ -30,6 +30,7 @@ RETRY_DELAY_SECONDS="${RETRY_DELAY_SECONDS:-15}" export RETRY_MAX_ATTEMPTS RETRY_DELAY_SECONDS source "${current_path}/common/run-with-retry.sh" +source "${current_path}/common/builder-image.sh" trap clean INT trap clean EXIT @@ -47,13 +48,6 @@ clean() { echo # Clean the files rm -rf ${tmp_dir} - rm -f ${current_path}/base/Docker/base-builder.sh - rm -f ${current_path}/base/Docker/plugins - rm -f ${current_path}/base/Docker/run-with-retry.sh - rm -f ${current_path}/rpm/Docker/rpm-builder.sh - rm -f ${current_path}/rpm/Docker/wazuh-dashboard.spec - rm -f ${current_path}/deb/Docker/deb-builder.sh - rm -rf ${current_path}/deb/Docker/debian trap '' EXIT exit ${exit_code} } @@ -94,6 +88,28 @@ get_packages(){ cd .. } +builder_arch() { + if [ "${architecture}" = "arm64" ]; then + echo "arm64" + else + echo "amd64" + fi +} + +# Pulls the published builder image and builds it locally when the pull fails +# (tag not published yet, no registry access, ...). Never pushes. +prepare_builder_image() { + local image="$1" + local ref="$2" + log "Pulling builder image ${ref}..." + if docker pull "${ref}"; then + log "Using the published builder image ${ref}" + return 0 + fi + echo "Notice: could not pull ${ref}. Building it locally." + run_with_retry docker build -t "${ref}" "${current_path}/${image}/Docker" +} + build_tar() { log log "Building base package..." @@ -102,35 +118,33 @@ build_tar() { cp -r ${config_dir} ${tmp_dir} cp -r ${current_path}/credentials ${tmp_dir} cd ./base - dockerfile_path="${current_path}/base/Docker" - container_name="dashboard-base-builder" - cp ./base-builder.sh ${dockerfile_path} - cp ./plugins ${dockerfile_path} - cp ${root_dir}/VERSION.json ${dockerfile_path} - cp ${current_path}/common/run-with-retry.sh ${dockerfile_path} - run_with_retry docker build -t "${container_name}" "${dockerfile_path}" || return 1 + builder_image="$(builder_image_ref base "$(builder_arch)")" || return 1 + prepare_builder_image base "${builder_image}" || return 1 run_with_retry docker run -t --rm \ -e "RETRY_MAX_ATTEMPTS=${RETRY_MAX_ATTEMPTS}" \ -e "RETRY_DELAY_SECONDS=${RETRY_DELAY_SECONDS}" \ -v "${tmp_dir}/:/tmp:Z" -v "${output_dir}/:/output:Z" \ - "${container_name}" "${version}" "${revision}" "${architecture}" "${verbose}" || return 1 + -v "${current_path}/base/base-builder.sh:/usr/local/bin/builder:ro,Z" \ + -v "${current_path}/base/plugins:/usr/local/bin/plugins:ro,Z" \ + -v "${root_dir}/VERSION.json:/usr/local/bin/VERSION.json:ro,Z" \ + -v "${current_path}/common/run-with-retry.sh:/usr/local/lib/wazuh/run-with-retry.sh:ro,Z" \ + "${builder_image}" "${version}" "${revision}" "${architecture}" "${verbose}" || return 1 cd .. } build_rpm() { log "Building rpm package..." cd ./rpm - dockerfile_path="${current_path}/rpm/Docker" - container_name="dashboard-rpm-builder" cp -r ${package_config_dir} ${tmp_dir} - cp ./rpm-builder.sh ${dockerfile_path} - cp ./wazuh-dashboard.spec ${dockerfile_path} - run_with_retry docker build -t "${container_name}" "${dockerfile_path}" || return 1 + builder_image="$(builder_image_ref rpm "$(builder_arch)")" || return 1 + prepare_builder_image rpm "${builder_image}" || return 1 run_with_retry docker run -t --rm \ -e "RETRY_MAX_ATTEMPTS=${RETRY_MAX_ATTEMPTS}" \ -e "RETRY_DELAY_SECONDS=${RETRY_DELAY_SECONDS}" \ -v "${tmp_dir}/:/tmp:Z" -v "${output_dir}/:/output:Z" \ - "${container_name}" "${version}" "${revision}" "${architecture}" \ + -v "${current_path}/rpm/rpm-builder.sh:/usr/local/bin/builder:ro,Z" \ + -v "${current_path}/rpm/wazuh-dashboard.spec:/usr/local/src/wazuh-dashboard.spec:ro,Z" \ + "${builder_image}" "${version}" "${revision}" "${architecture}" \ "${commit_sha}" "${production}" "${verbose}" || return 1 cd ../ } @@ -139,17 +153,16 @@ build_rpm() { build_deb() { log "Building deb package..." cd ./deb - dockerfile_path="${current_path}/deb/Docker" - container_name="dashboard-deb-builder" cp -r ${package_config_dir} ${tmp_dir} - cp ./deb-builder.sh ${dockerfile_path} - cp -r ./debian ${dockerfile_path} - run_with_retry docker build -t "${container_name}" "${dockerfile_path}" || return 1 + builder_image="$(builder_image_ref deb "$(builder_arch)")" || return 1 + prepare_builder_image deb "${builder_image}" || return 1 run_with_retry docker run -t --rm \ -e "RETRY_MAX_ATTEMPTS=${RETRY_MAX_ATTEMPTS}" \ -e "RETRY_DELAY_SECONDS=${RETRY_DELAY_SECONDS}" \ -v "${tmp_dir}/:/tmp:Z" -v "${output_dir}/:/output:Z" \ - "${container_name}" "${version}" "${revision}" "${architecture}" \ + -v "${current_path}/deb/deb-builder.sh:/usr/local/bin/builder:ro,Z" \ + -v "${current_path}/deb/debian:/usr/local/src/debian:ro,Z" \ + "${builder_image}" "${version}" "${revision}" "${architecture}" \ "${commit_sha}" "${production}" "${verbose}" || return 1 cd .. } diff --git a/dev-tools/build-packages/common/builder-image.sh b/dev-tools/build-packages/common/builder-image.sh new file mode 100755 index 000000000000..a79e501aa151 --- /dev/null +++ b/dev-tools/build-packages/common/builder-image.sh @@ -0,0 +1,61 @@ +#!/bin/bash + +# Prints the reference of a dashboard package builder image: +# ghcr.io/wazuh/pkg_dashboard__builder_:- +# is `.version` in the repository VERSION.json and the first +# 12 hex chars of the sha256 of the image's Dockerfile, so a Dockerfile change +# always gets a new tag and published tags never need to be overwritten. +# +# Usage: +# bash builder-image.sh +# or source it and call: +# builder_image_ref + +builder_image_build_packages_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd -P)" + +builder_image_ref() { + local image="$1" + local arch="$2" + local dockerfile + local version + local hash + + case "${image}" in + base | rpm | deb) ;; + *) + echo "Unknown builder image '${image}'. Expected base, rpm or deb." >&2 + return 1 + ;; + esac + + case "${arch}" in + amd64 | arm64) ;; + *) + echo "Unknown builder architecture '${arch}'. Expected amd64 or arm64." >&2 + return 1 + ;; + esac + + dockerfile="${builder_image_build_packages_dir}/${image}/Docker/Dockerfile" + version="$(jq -r '.version' "${builder_image_build_packages_dir}/../../VERSION.json")" || return 1 + if [ -z "${version}" ] || [ "${version}" = "null" ]; then + echo "Could not read the version from VERSION.json." >&2 + return 1 + fi + + if command -v sha256sum >/dev/null 2>&1; then + hash="$(sha256sum "${dockerfile}")" || return 1 + else + hash="$(shasum -a 256 "${dockerfile}")" || return 1 + fi + + echo "ghcr.io/wazuh/pkg_dashboard_${image}_builder_${arch}:${version}-${hash:0:12}" +} + +if [ "${BASH_SOURCE[0]}" = "$0" ]; then + if [ $# -ne 2 ]; then + echo "Usage: $0 " >&2 + exit 1 + fi + builder_image_ref "$1" "$2" +fi diff --git a/dev-tools/build-packages/deb/Docker/Dockerfile b/dev-tools/build-packages/deb/Docker/Dockerfile index dbdc3f2d64a8..20250f440b6d 100644 --- a/dev-tools/build-packages/deb/Docker/Dockerfile +++ b/dev-tools/build-packages/deb/Docker/Dockerfile @@ -8,10 +8,6 @@ RUN apt-get update && apt-get install -y --allow-change-held-packages apt-utils curl sudo wget expect gnupg build-essential \ devscripts equivs selinux-basics procps gawk jq -# Add the script to build the Debian package -ADD deb-builder.sh /usr/local/bin/builder -ADD debian /usr/local/src/debian -RUN chmod +x /usr/local/bin/builder - -# Set the entrypoint -ENTRYPOINT ["/usr/local/bin/builder"] +# The builder script and its inputs are mounted at run time by +# build-packages.sh, so this Dockerfile is the only input of the image. +ENTRYPOINT ["/bin/bash", "/usr/local/bin/builder"] diff --git a/dev-tools/build-packages/rpm/Docker/Dockerfile b/dev-tools/build-packages/rpm/Docker/Dockerfile index c62abf3820c1..2b02e0244e9f 100644 --- a/dev-tools/build-packages/rpm/Docker/Dockerfile +++ b/dev-tools/build-packages/rpm/Docker/Dockerfile @@ -13,10 +13,6 @@ RUN yum update -y && yum install -y python3 RUN ln -fs /usr/bin/python3.6 /usr/bin/python -# Add the scripts to build the RPM package -ADD rpm-builder.sh /usr/local/bin/builder -ADD wazuh-dashboard.spec /usr/local/src/wazuh-dashboard.spec -RUN chmod +x /usr/local/bin/builder - -# Set the entrypoint -ENTRYPOINT ["/usr/local/bin/builder"] +# The builder script and its inputs are mounted at run time by +# build-packages.sh, so this Dockerfile is the only input of the image. +ENTRYPOINT ["/bin/bash", "/usr/local/bin/builder"] From bca2b42ea9f72bad3073ce356ab45e15954e43d2 Mon Sep 17 00:00:00 2001 From: Diego Garcia Date: Fri, 6 Nov 2026 14:26:33 -0500 Subject: [PATCH 2/5] feat(ci): Publish and clean up the package builder images Upload the base, rpm and deb builder images for amd64 and arm64 to ghcr when their Dockerfile changes on a version branch or main, or on demand. A tag that already exists is never pushed again. A weekly cleanup keeps the 3 latest versions of each image and deletes untagged ones. A branch whose tag was deleted builds the image locally. Signed-off-by: Diego Garcia --- ...piled_dashboard_builder-images-cleanup.yml | 48 ++++++++++ ...mpiled_dashboard_builder-images-upload.yml | 96 +++++++++++++++++++ 2 files changed, 144 insertions(+) create mode 100644 .github/workflows/5_builderprecompiled_dashboard_builder-images-cleanup.yml create mode 100644 .github/workflows/5_builderprecompiled_dashboard_builder-images-upload.yml diff --git a/.github/workflows/5_builderprecompiled_dashboard_builder-images-cleanup.yml b/.github/workflows/5_builderprecompiled_dashboard_builder-images-cleanup.yml new file mode 100644 index 000000000000..7fdeb24fb02c --- /dev/null +++ b/.github/workflows/5_builderprecompiled_dashboard_builder-images-cleanup.yml @@ -0,0 +1,48 @@ +# Keeps the 3 latest versions of each dashboard package builder image in ghcr +# and deletes the untagged ones. +# +# A branch whose image tag was deleted still works: build-packages.sh can't +# pull the tag and builds the image locally. + +run-name: Clean up dashboard builder images +name: (5.x) Clean up dashboard package builder images + +on: + schedule: + - cron: '0 3 * * 1' + workflow_dispatch: + +permissions: + packages: write + +jobs: + cleanup: + name: Clean up ${{ matrix.package }} + runs-on: ubuntu-24.04 + strategy: + fail-fast: false + matrix: + package: + - pkg_dashboard_base_builder_amd64 + - pkg_dashboard_base_builder_arm64 + - pkg_dashboard_rpm_builder_amd64 + - pkg_dashboard_rpm_builder_arm64 + - pkg_dashboard_deb_builder_amd64 + - pkg_dashboard_deb_builder_arm64 + steps: + # With delete-only-untagged-versions, min-versions-to-keep counts + # untagged versions only, so the two rules need two steps. + - name: Delete untagged versions + uses: actions/delete-package-versions@v5 + with: + package-name: ${{ matrix.package }} + package-type: container + min-versions-to-keep: 0 + delete-only-untagged-versions: 'true' + + - name: Keep the 3 latest versions + uses: actions/delete-package-versions@v5 + with: + package-name: ${{ matrix.package }} + package-type: container + min-versions-to-keep: 3 diff --git a/.github/workflows/5_builderprecompiled_dashboard_builder-images-upload.yml b/.github/workflows/5_builderprecompiled_dashboard_builder-images-upload.yml new file mode 100644 index 000000000000..d7dd62e64672 --- /dev/null +++ b/.github/workflows/5_builderprecompiled_dashboard_builder-images-upload.yml @@ -0,0 +1,96 @@ +# Publishes the dashboard package builder images (base, rpm, deb on amd64 and +# arm64) to ghcr, so `Generate packages` pulls them instead of building them. +# +# Tags are immutable: -, computed by +# dev-tools/build-packages/common/builder-image.sh. A tag that already exists +# is never pushed again. + +run-name: Upload dashboard builder images from ${{ github.ref_name }} +name: (5.x) Upload dashboard package builder images + +on: + push: + branches: + - 'main' + - '[0-9]+.[0-9]+.[0-9]+' + paths: + - 'dev-tools/build-packages/base/Docker/Dockerfile' + - 'dev-tools/build-packages/rpm/Docker/Dockerfile' + - 'dev-tools/build-packages/deb/Docker/Dockerfile' + - 'dev-tools/build-packages/common/builder-image.sh' + - '.github/workflows/5_builderprecompiled_dashboard_builder-images-upload.yml' + workflow_dispatch: + +permissions: + contents: read + packages: write + +jobs: + # Computes every image reference first so the upload jobs can use it as + # their concurrency group. + get-references: + name: Get image references + runs-on: ubuntu-24.04 + outputs: + matrix: ${{ steps.references.outputs.matrix }} + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Compute image references + id: references + run: | + entries=() + for image in base rpm deb; do + for arch in amd64 arm64; do + ref="$(bash dev-tools/build-packages/common/builder-image.sh "${image}" "${arch}")" + echo "${image} ${arch}: ${ref}" + entries+=("$(jq -nc --arg image "${image}" --arg arch "${arch}" --arg ref "${ref}" \ + '{image: $image, arch: $arch, ref: $ref}')") + done + done + echo "matrix=$(printf '%s\n' "${entries[@]}" | jq -sc '{include: .}')" >> "$GITHUB_OUTPUT" + + upload: + needs: get-references + name: Upload ${{ matrix.image }} builder image (${{ matrix.arch }}) + runs-on: ${{ matrix.arch == 'arm64' && 'ubuntu-24.04-arm' || 'ubuntu-24.04' }} + concurrency: + group: builder-image-${{ matrix.ref }} + cancel-in-progress: false + strategy: + fail-fast: false + matrix: ${{ fromJSON(needs.get-references.outputs.matrix) }} + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Login to GitHub Container Registry + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: echo "$GITHUB_TOKEN" | docker login ghcr.io -u "$GITHUB_ACTOR" --password-stdin + + - name: Build and push the image + env: + IMAGE_REF: ${{ matrix.ref }} + CONTEXT_DIR: dev-tools/build-packages/${{ matrix.image }}/Docker + SOURCE_URL: ${{ github.server_url }}/${{ github.repository }} + run: | + if output="$(docker manifest inspect "${IMAGE_REF}" 2>&1)"; then + echo "${IMAGE_REF} already exists. Tags are immutable: nothing to push." + exit 0 + fi + # ghcr answers "denied" for a package that doesn't exist yet. Any + # other error could hide an existing tag, so don't overwrite it. + if ! grep -qiE 'no such manifest|manifest unknown|name unknown|not found|denied' <<< "${output}"; then + echo "::error::Could not check whether ${IMAGE_REF} exists: ${output}" + exit 1 + fi + echo "${IMAGE_REF} doesn't exist yet. Building and pushing it." + # The source label links the package to this repository. No + # provenance: an attestation adds untagged versions that the + # cleanup workflow would delete. + docker build --provenance=false \ + --label "org.opencontainers.image.source=${SOURCE_URL}" \ + -t "${IMAGE_REF}" "${CONTEXT_DIR}" + docker push "${IMAGE_REF}" From 62728b7ef209549c630237a92715e8862f71becf Mon Sep 17 00:00:00 2001 From: Diego Garcia Date: Fri, 6 Nov 2026 15:09:07 -0500 Subject: [PATCH 3/5] fix(ci): Keep the current builder image of every version branch - Cleanup: replace actions/delete-package-versions (keep the 3 latest) with a jq selection that deletes untagged versions and a tagged one only when all its tags are - and none is the newest of its version. With 4 active version branches, "keep 3" deleted the image of one of them for good. A missing package now gives a notice instead of failing, and workflow_dispatch has a dry run. The filter has its own test script. - Upload: run on VERSION.json changes, since the tag holds the version. - build-packages.sh: try the pull twice before building locally, and warn in GitHub Actions when it falls back to the local build. - Delete the Docker/.gitignore files of base, rpm and deb: they only listed files that build-packages.sh no longer copies into the contexts. Signed-off-by: Diego Garcia --- ...piled_dashboard_builder-images-cleanup.yml | 119 +++++++++++++----- ...mpiled_dashboard_builder-images-upload.yml | 2 + .../build-packages/base/Docker/.gitignore | 4 - dev-tools/build-packages/build-packages.sh | 9 +- .../common/builder-image-retention.jq | 30 +++++ .../common/test-builder-image-retention.sh | 111 ++++++++++++++++ .../build-packages/deb/Docker/.gitignore | 1 - .../build-packages/rpm/Docker/.gitignore | 2 - 8 files changed, 237 insertions(+), 41 deletions(-) delete mode 100644 dev-tools/build-packages/base/Docker/.gitignore create mode 100644 dev-tools/build-packages/common/builder-image-retention.jq create mode 100755 dev-tools/build-packages/common/test-builder-image-retention.sh delete mode 100644 dev-tools/build-packages/deb/Docker/.gitignore delete mode 100644 dev-tools/build-packages/rpm/Docker/.gitignore diff --git a/.github/workflows/5_builderprecompiled_dashboard_builder-images-cleanup.yml b/.github/workflows/5_builderprecompiled_dashboard_builder-images-cleanup.yml index 7fdeb24fb02c..9a9802461fc6 100644 --- a/.github/workflows/5_builderprecompiled_dashboard_builder-images-cleanup.yml +++ b/.github/workflows/5_builderprecompiled_dashboard_builder-images-cleanup.yml @@ -1,48 +1,103 @@ -# Keeps the 3 latest versions of each dashboard package builder image in ghcr -# and deletes the untagged ones. +# Deletes the old versions of the dashboard package builder images in ghcr. # -# A branch whose image tag was deleted still works: build-packages.sh can't -# pull the tag and builds the image locally. +# Rule (dev-tools/build-packages/common/builder-image-retention.jq): untagged +# versions are deleted, and a tagged version is deleted only when all its tags +# are - and none is the newest tag of its . +# So the current image of every version branch is kept, and versions with any +# other tag are never touched. +# +# A branch whose tag is deleted anyway (e.g. a Dockerfile reverted to an older +# content) still works: build-packages.sh can't pull the tag and builds the +# image locally. -run-name: Clean up dashboard builder images +run-name: Clean up dashboard builder images${{ inputs.dry_run && ' (dry run)' || '' }} name: (5.x) Clean up dashboard package builder images on: schedule: - cron: '0 3 * * 1' workflow_dispatch: + inputs: + dry_run: + description: 'Only list the versions that would be deleted' + type: boolean + required: false + default: false permissions: - packages: write + contents: read jobs: cleanup: - name: Clean up ${{ matrix.package }} + name: Clean up the builder images runs-on: ubuntu-24.04 - strategy: - fail-fast: false - matrix: - package: - - pkg_dashboard_base_builder_amd64 - - pkg_dashboard_base_builder_arm64 - - pkg_dashboard_rpm_builder_amd64 - - pkg_dashboard_rpm_builder_arm64 - - pkg_dashboard_deb_builder_amd64 - - pkg_dashboard_deb_builder_arm64 + timeout-minutes: 30 + permissions: + contents: read + packages: write steps: - # With delete-only-untagged-versions, min-versions-to-keep counts - # untagged versions only, so the two rules need two steps. - - name: Delete untagged versions - uses: actions/delete-package-versions@v5 - with: - package-name: ${{ matrix.package }} - package-type: container - min-versions-to-keep: 0 - delete-only-untagged-versions: 'true' + - name: Checkout code + uses: actions/checkout@v4 + + - name: Test the retention filter + run: bash dev-tools/build-packages/common/test-builder-image-retention.sh + + - name: Delete old versions + env: + GH_TOKEN: ${{ github.token }} + OWNER: ${{ github.repository_owner }} + # Scheduled runs have no inputs: they delete. + DRY_RUN: ${{ inputs.dry_run || false }} + FILTER: dev-tools/build-packages/common/builder-image-retention.jq + run: | + set -euo pipefail + packages=() + for image in base rpm deb; do + for arch in amd64 arm64; do + packages+=("pkg_dashboard_${image}_builder_${arch}") + done + done + for package in "${packages[@]}"; do + if [[ ! "${package}" =~ ^pkg_dashboard_(base|rpm|deb)_builder_(amd64|arm64)$ ]]; then + echo "::error::Invalid package name '${package}': nothing deleted" + exit 1 + fi + done + echo "Deleting old builder image versions (dry run: ${DRY_RUN})" - - name: Keep the 3 latest versions - uses: actions/delete-package-versions@v5 - with: - package-name: ${{ matrix.package }} - package-type: container - min-versions-to-keep: 3 + err="${RUNNER_TEMP:-/tmp}/gh.err" + failed=0 + for package in "${packages[@]}"; do + api="/orgs/${OWNER}/packages/container/${package}/versions" + if ! versions="$(gh api --paginate --slurp "${api}?per_page=100" 2>"${err}" | jq -c '[.[][]]')"; then + if grep -q 'HTTP 404' "${err}"; then + echo "::notice::${package} does not exist yet: nothing to delete" + continue + fi + cat "${err}" + echo "::error::Could not list the versions of ${package}" + failed=1 + continue + fi + if ! delete="$(jq -r -f "${FILTER}" <<<"${versions}")"; then + echo "::error::Could not select the versions of ${package} to delete" + failed=1 + continue + fi + echo "${package}: $(jq length <<<"${versions}") versions, $(grep -c . <<<"${delete}" || true) to delete" + while IFS=$'\t' read -r id reason; do + [ -n "${id}" ] || continue + if [[ ! "${id}" =~ ^[0-9]+$ ]]; then + echo "::error::Invalid version id '${id}' for ${package}" + failed=1 + continue + fi + echo "Delete ${package} version ${id} (${reason})" + [ "${DRY_RUN}" = true ] && continue + if ! gh api -X DELETE "${api}/${id}" >/dev/null; then + echo "::error::Could not delete ${package} version ${id}" + failed=1 + fi + done <<<"${delete}" + done + exit "${failed}" diff --git a/.github/workflows/5_builderprecompiled_dashboard_builder-images-upload.yml b/.github/workflows/5_builderprecompiled_dashboard_builder-images-upload.yml index d7dd62e64672..d17199b9e6d2 100644 --- a/.github/workflows/5_builderprecompiled_dashboard_builder-images-upload.yml +++ b/.github/workflows/5_builderprecompiled_dashboard_builder-images-upload.yml @@ -14,6 +14,8 @@ on: - 'main' - '[0-9]+.[0-9]+.[0-9]+' paths: + # The tag holds the version, so a version bump needs new tags too. + - 'VERSION.json' - 'dev-tools/build-packages/base/Docker/Dockerfile' - 'dev-tools/build-packages/rpm/Docker/Dockerfile' - 'dev-tools/build-packages/deb/Docker/Dockerfile' diff --git a/dev-tools/build-packages/base/Docker/.gitignore b/dev-tools/build-packages/base/Docker/.gitignore deleted file mode 100644 index 18cab4f01187..000000000000 --- a/dev-tools/build-packages/base/Docker/.gitignore +++ /dev/null @@ -1,4 +0,0 @@ -base-builder.sh -plugins -output/ -VERSION.json diff --git a/dev-tools/build-packages/build-packages.sh b/dev-tools/build-packages/build-packages.sh index e525fec593b1..4da5a6cd357d 100755 --- a/dev-tools/build-packages/build-packages.sh +++ b/dev-tools/build-packages/build-packages.sh @@ -97,16 +97,21 @@ builder_arch() { } # Pulls the published builder image and builds it locally when the pull fails -# (tag not published yet, no registry access, ...). Never pushes. +# (tag not published yet, no registry access, ...). Never pushes. The pull is +# tried twice only: a missing tag fails every attempt, and the local build is +# the fallback anyway. prepare_builder_image() { local image="$1" local ref="$2" log "Pulling builder image ${ref}..." - if docker pull "${ref}"; then + if RETRY_MAX_ATTEMPTS=2 run_with_retry docker pull "${ref}"; then log "Using the published builder image ${ref}" return 0 fi echo "Notice: could not pull ${ref}. Building it locally." + if [ "${GITHUB_ACTIONS:-}" = "true" ]; then + echo "::warning::${ref} was not found in ghcr or couldn't be pulled; building it locally" + fi run_with_retry docker build -t "${ref}" "${current_path}/${image}/Docker" } diff --git a/dev-tools/build-packages/common/builder-image-retention.jq b/dev-tools/build-packages/common/builder-image-retention.jq new file mode 100644 index 000000000000..e9a254621c25 --- /dev/null +++ b/dev-tools/build-packages/common/builder-image-retention.jq @@ -0,0 +1,30 @@ +# Selects the versions of a dashboard package builder image to delete from ghcr. +# +# Input: the versions of one container package, as returned by +# GET /orgs/{org}/packages/container/{name}/versions (all pages merged). +# Output (with jq -r): one "\t" line per version to delete. +# +# Builder image tags are -<12 hex chars of the Dockerfile sha256> +# (see builder-image.sh). A version is deleted when: +# - it has no tags, or +# - every tag has that format and none of them is the newest one (by the +# version's created_at) for its prefix, the part before the last "-". +# A version with any tag in another format is never deleted. +# +# This keeps the current image of every version branch, whatever the number of +# branches. Tests: test-builder-image-retention.sh. + +def parse: capture("^(?.+)-(?[0-9a-f]{12})$"); + +. as $all +# The id of the newest version of each prefix. +| [ $all[] | .created_at as $created_at | .id as $id + | (.metadata.container.tags // [])[] | select(test("^.+-[0-9a-f]{12}$")) + | parse + {id: $id, created_at: $created_at} ] +| group_by(.prefix) | map(max_by(.created_at)) | map(.id) as $current +| $all[] +| (.metadata.container.tags // []) as $tags +| if ($tags | length) == 0 then "\(.id)\tuntagged" + elif any($tags[]; test("^.+-[0-9a-f]{12}$") | not) then empty + elif ([.id] | inside($current)) then empty + else "\(.id)\tsuperseded: \($tags | join(","))" end diff --git a/dev-tools/build-packages/common/test-builder-image-retention.sh b/dev-tools/build-packages/common/test-builder-image-retention.sh new file mode 100755 index 000000000000..df1245466be8 --- /dev/null +++ b/dev-tools/build-packages/common/test-builder-image-retention.sh @@ -0,0 +1,111 @@ +#!/bin/bash + +# Tests builder-image-retention.jq, the filter the builder images cleanup +# workflow uses to pick the ghcr versions to delete. +# +# Usage: +# bash dev-tools/build-packages/common/test-builder-image-retention.sh +# +# Exits non-zero when a case doesn't select exactly the expected versions. + +set -euo pipefail + +filter="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd -P)/builder-image-retention.jq" +failures=0 + +# check +# Compares the "\t" lines selected by the filter with the expected +# ones, ignoring their order. +check() { + local name="$1" + local expected="$2" + local versions="$3" + local actual + + if ! actual="$(jq -r -f "${filter}" <<<"${versions}" 2>&1)"; then + echo "FAIL: ${name}: the filter failed: ${actual}" + failures=$((failures + 1)) + return + fi + + actual="$(sort <<<"${actual}")" + expected="$(sort <<<"${expected}")" + if [ "${actual}" = "${expected}" ]; then + echo "ok: ${name}" + else + echo "FAIL: ${name}" + echo " expected:" + sed 's/^/ /' <<<"${expected}" + echo " actual:" + sed 's/^/ /' <<<"${actual}" + failures=$((failures + 1)) + fi +} + +tab=$'\t' + +# Every active branch has its own version but the same Dockerfile. +check "4 branches with the same hash: nothing deleted" "" '[ + {"id": 1, "created_at": "2026-11-01T00:00:00Z", "metadata": {"container": {"tags": ["5.0.0-aaaaaaaaaaaa"]}}}, + {"id": 2, "created_at": "2026-11-02T00:00:00Z", "metadata": {"container": {"tags": ["5.0.1-aaaaaaaaaaaa"]}}}, + {"id": 3, "created_at": "2026-11-03T00:00:00Z", "metadata": {"container": {"tags": ["5.1.0-aaaaaaaaaaaa"]}}}, + {"id": 4, "created_at": "2026-11-04T00:00:00Z", "metadata": {"container": {"tags": ["6.0.0-aaaaaaaaaaaa"]}}} +]' + +check "a branch with 3 successive hashes: the 2 oldest deleted" \ + "10${tab}superseded: 5.0.0-111111111111 +11${tab}superseded: 5.0.0-222222222222" '[ + {"id": 10, "created_at": "2026-11-01T00:00:00Z", "metadata": {"container": {"tags": ["5.0.0-111111111111"]}}}, + {"id": 11, "created_at": "2026-11-02T00:00:00Z", "metadata": {"container": {"tags": ["5.0.0-222222222222"]}}}, + {"id": 12, "created_at": "2026-11-03T00:00:00Z", "metadata": {"container": {"tags": ["5.0.0-333333333333"]}}}, + {"id": 13, "created_at": "2026-11-01T12:00:00Z", "metadata": {"container": {"tags": ["5.0.1-111111111111"]}}} +]' + +check "a version tagged for 2 prefixes: kept while it is the newest for one" "" '[ + {"id": 20, "created_at": "2026-11-01T00:00:00Z", "metadata": {"container": {"tags": ["5.0.0-aaaaaaaaaaaa", "5.0.1-aaaaaaaaaaaa"]}}}, + {"id": 21, "created_at": "2026-11-02T00:00:00Z", "metadata": {"container": {"tags": ["5.0.0-bbbbbbbbbbbb"]}}} +]' + +check "a version tagged for 2 prefixes: deleted once superseded for both" \ + "20${tab}superseded: 5.0.0-aaaaaaaaaaaa,5.0.1-aaaaaaaaaaaa" '[ + {"id": 20, "created_at": "2026-11-01T00:00:00Z", "metadata": {"container": {"tags": ["5.0.0-aaaaaaaaaaaa", "5.0.1-aaaaaaaaaaaa"]}}}, + {"id": 21, "created_at": "2026-11-02T00:00:00Z", "metadata": {"container": {"tags": ["5.0.0-bbbbbbbbbbbb"]}}}, + {"id": 22, "created_at": "2026-11-03T00:00:00Z", "metadata": {"container": {"tags": ["5.0.1-cccccccccccc"]}}} +]' + +check "untagged versions: deleted" \ + "30${tab}untagged +31${tab}untagged" '[ + {"id": 30, "created_at": "2026-11-01T00:00:00Z", "metadata": {"container": {"tags": []}}}, + {"id": 31, "created_at": "2026-11-02T00:00:00Z", "metadata": {"container": {}}}, + {"id": 32, "created_at": "2026-11-03T00:00:00Z", "metadata": {"container": {"tags": ["5.0.0-aaaaaaaaaaaa"]}}} +]' + +# Only -<12 lowercase hex> tags are managed: a version with any other +# tag was pushed by hand or by another tool and is never touched. +check "tags in another format: kept" "" '[ + {"id": 40, "created_at": "2026-11-01T00:00:00Z", "metadata": {"container": {"tags": ["latest"]}}}, + {"id": 41, "created_at": "2026-11-02T00:00:00Z", "metadata": {"container": {"tags": ["latest", "5.0.0-111111111111"]}}}, + {"id": 42, "created_at": "2026-11-03T00:00:00Z", "metadata": {"container": {"tags": ["5.0.0-ABCDEFABCDEF"]}}}, + {"id": 43, "created_at": "2026-11-04T00:00:00Z", "metadata": {"container": {"tags": ["5.0.0-abc"]}}}, + {"id": 44, "created_at": "2026-11-05T00:00:00Z", "metadata": {"container": {"tags": ["5.0.0-222222222222"]}}} +]' + +check "no versions: nothing deleted" "" '[]' + +# Known edge case: when a Dockerfile is reverted to an older content, its tag +# already exists, so the upload doesn't push it again and the newer tag stays +# the newest for that version. The cleanup then deletes the tag the branch +# uses again, and package builds fall back to building the image locally +# until the Dockerfile changes. +check "a Dockerfile reverted to an older hash: the older, in-use tag deleted" \ + "50${tab}superseded: 5.0.0-aaaaaaaaaaaa" '[ + {"id": 50, "created_at": "2026-11-01T00:00:00Z", "metadata": {"container": {"tags": ["5.0.0-aaaaaaaaaaaa"]}}}, + {"id": 51, "created_at": "2026-11-02T00:00:00Z", "metadata": {"container": {"tags": ["5.0.0-bbbbbbbbbbbb"]}}} +]' + +if [ "${failures}" -ne 0 ]; then + echo "${failures} case(s) failed" + exit 1 +fi +echo "All cases passed" diff --git a/dev-tools/build-packages/deb/Docker/.gitignore b/dev-tools/build-packages/deb/Docker/.gitignore deleted file mode 100644 index 76b757f1527a..000000000000 --- a/dev-tools/build-packages/deb/Docker/.gitignore +++ /dev/null @@ -1 +0,0 @@ -deb-builder.sh diff --git a/dev-tools/build-packages/rpm/Docker/.gitignore b/dev-tools/build-packages/rpm/Docker/.gitignore deleted file mode 100644 index 3498c7f9b3c5..000000000000 --- a/dev-tools/build-packages/rpm/Docker/.gitignore +++ /dev/null @@ -1,2 +0,0 @@ -rpm-builder.sh -wazuh-dashboard.spec From 054b9049c47863d6de20e7ac32a1352fcc1a2626 Mon Sep 17 00:00:00 2001 From: Diego Garcia Date: Fri, 6 Nov 2026 15:21:49 -0500 Subject: [PATCH 4/5] fix(ci): Grant ghcr read access at the workflow level The Generate packages job header also changes in the build-both-systems work, so granting packages: read for the whole workflow keeps both changes from conflicting. Jobs with their own permissions, like Test package, are not affected. Signed-off-by: Diego Garcia --- .github/workflows/5_builderpackage_dashboard.yml | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/.github/workflows/5_builderpackage_dashboard.yml b/.github/workflows/5_builderpackage_dashboard.yml index 1f1716975825..78ea7169a2e6 100644 --- a/.github/workflows/5_builderpackage_dashboard.yml +++ b/.github/workflows/5_builderpackage_dashboard.yml @@ -97,6 +97,8 @@ on: permissions: id-token: write contents: read + # Pull the prebuilt builder images from ghcr in Generate packages + packages: read env: CI_DEV_INTERNAL_BUCKET: ${{ secrets.CI_DEV_INTERNAL_BUCKET }} @@ -350,12 +352,6 @@ jobs: needs: [setup-variables, get-outputs-plugins, build-dashboard, build-plugins] runs-on: ${{ (inputs.architecture == 'arm64' || inputs.architecture == 'aarch64') && format('codebuild-github-actions-codebuild-runner-dashboard-arm-{0}-{1}', github.run_id, github.run_attempt) || format('codebuild-github-actions-codebuild-runner-dashboard-amd-{0}-{1}', github.run_id, github.run_attempt) }} name: Generate packages - # Job-level permissions replace the workflow ones: keep those and add - # packages: read to pull the prebuilt builder images from ghcr. - permissions: - id-token: write - contents: read - packages: read outputs: PACKAGE_NAME: ${{ steps.get-package-name.outputs.PACKAGE_NAME }} steps: From ff3dacd7ed85d14897052cc663518f6d7f74e348 Mon Sep 17 00:00:00 2001 From: Diego Garcia Date: Fri, 6 Nov 2026 18:07:22 -0500 Subject: [PATCH 5/5] fix(ci): Keep local package builds as before Mount the repository inputs with a shared SELinux label (z) so other containers that mount the repository keep access to them, wait 5s instead of 15s between pull attempts, since a local build without a ghcr login always falls back to building the images, and restore the Docker context .gitignore files. Signed-off-by: Diego Garcia --- .../build-packages/base/Docker/.gitignore | 4 ++++ dev-tools/build-packages/build-packages.sh | 18 +++++++++--------- dev-tools/build-packages/deb/Docker/.gitignore | 1 + dev-tools/build-packages/rpm/Docker/.gitignore | 2 ++ 4 files changed, 16 insertions(+), 9 deletions(-) create mode 100644 dev-tools/build-packages/base/Docker/.gitignore create mode 100644 dev-tools/build-packages/deb/Docker/.gitignore create mode 100644 dev-tools/build-packages/rpm/Docker/.gitignore diff --git a/dev-tools/build-packages/base/Docker/.gitignore b/dev-tools/build-packages/base/Docker/.gitignore new file mode 100644 index 000000000000..18cab4f01187 --- /dev/null +++ b/dev-tools/build-packages/base/Docker/.gitignore @@ -0,0 +1,4 @@ +base-builder.sh +plugins +output/ +VERSION.json diff --git a/dev-tools/build-packages/build-packages.sh b/dev-tools/build-packages/build-packages.sh index 4da5a6cd357d..caab19fc69b1 100755 --- a/dev-tools/build-packages/build-packages.sh +++ b/dev-tools/build-packages/build-packages.sh @@ -104,7 +104,7 @@ prepare_builder_image() { local image="$1" local ref="$2" log "Pulling builder image ${ref}..." - if RETRY_MAX_ATTEMPTS=2 run_with_retry docker pull "${ref}"; then + if RETRY_MAX_ATTEMPTS=2 RETRY_DELAY_SECONDS=5 run_with_retry docker pull "${ref}"; then log "Using the published builder image ${ref}" return 0 fi @@ -129,10 +129,10 @@ build_tar() { -e "RETRY_MAX_ATTEMPTS=${RETRY_MAX_ATTEMPTS}" \ -e "RETRY_DELAY_SECONDS=${RETRY_DELAY_SECONDS}" \ -v "${tmp_dir}/:/tmp:Z" -v "${output_dir}/:/output:Z" \ - -v "${current_path}/base/base-builder.sh:/usr/local/bin/builder:ro,Z" \ - -v "${current_path}/base/plugins:/usr/local/bin/plugins:ro,Z" \ - -v "${root_dir}/VERSION.json:/usr/local/bin/VERSION.json:ro,Z" \ - -v "${current_path}/common/run-with-retry.sh:/usr/local/lib/wazuh/run-with-retry.sh:ro,Z" \ + -v "${current_path}/base/base-builder.sh:/usr/local/bin/builder:ro,z" \ + -v "${current_path}/base/plugins:/usr/local/bin/plugins:ro,z" \ + -v "${root_dir}/VERSION.json:/usr/local/bin/VERSION.json:ro,z" \ + -v "${current_path}/common/run-with-retry.sh:/usr/local/lib/wazuh/run-with-retry.sh:ro,z" \ "${builder_image}" "${version}" "${revision}" "${architecture}" "${verbose}" || return 1 cd .. } @@ -147,8 +147,8 @@ build_rpm() { -e "RETRY_MAX_ATTEMPTS=${RETRY_MAX_ATTEMPTS}" \ -e "RETRY_DELAY_SECONDS=${RETRY_DELAY_SECONDS}" \ -v "${tmp_dir}/:/tmp:Z" -v "${output_dir}/:/output:Z" \ - -v "${current_path}/rpm/rpm-builder.sh:/usr/local/bin/builder:ro,Z" \ - -v "${current_path}/rpm/wazuh-dashboard.spec:/usr/local/src/wazuh-dashboard.spec:ro,Z" \ + -v "${current_path}/rpm/rpm-builder.sh:/usr/local/bin/builder:ro,z" \ + -v "${current_path}/rpm/wazuh-dashboard.spec:/usr/local/src/wazuh-dashboard.spec:ro,z" \ "${builder_image}" "${version}" "${revision}" "${architecture}" \ "${commit_sha}" "${production}" "${verbose}" || return 1 cd ../ @@ -165,8 +165,8 @@ build_deb() { -e "RETRY_MAX_ATTEMPTS=${RETRY_MAX_ATTEMPTS}" \ -e "RETRY_DELAY_SECONDS=${RETRY_DELAY_SECONDS}" \ -v "${tmp_dir}/:/tmp:Z" -v "${output_dir}/:/output:Z" \ - -v "${current_path}/deb/deb-builder.sh:/usr/local/bin/builder:ro,Z" \ - -v "${current_path}/deb/debian:/usr/local/src/debian:ro,Z" \ + -v "${current_path}/deb/deb-builder.sh:/usr/local/bin/builder:ro,z" \ + -v "${current_path}/deb/debian:/usr/local/src/debian:ro,z" \ "${builder_image}" "${version}" "${revision}" "${architecture}" \ "${commit_sha}" "${production}" "${verbose}" || return 1 cd .. diff --git a/dev-tools/build-packages/deb/Docker/.gitignore b/dev-tools/build-packages/deb/Docker/.gitignore new file mode 100644 index 000000000000..76b757f1527a --- /dev/null +++ b/dev-tools/build-packages/deb/Docker/.gitignore @@ -0,0 +1 @@ +deb-builder.sh diff --git a/dev-tools/build-packages/rpm/Docker/.gitignore b/dev-tools/build-packages/rpm/Docker/.gitignore new file mode 100644 index 000000000000..3498c7f9b3c5 --- /dev/null +++ b/dev-tools/build-packages/rpm/Docker/.gitignore @@ -0,0 +1,2 @@ +rpm-builder.sh +wazuh-dashboard.spec