diff --git a/.github/workflows/reusable-build.yml b/.github/workflows/reusable-build.yml index 4025c3d4ffe..c1602f43354 100644 --- a/.github/workflows/reusable-build.yml +++ b/.github/workflows/reusable-build.yml @@ -18,6 +18,9 @@ on: kernel_pin: description: "The full kernel version to pin" type: string + previous_build: + description: 'Set a reference to a previous build to make updates smaller' + type: string env: IMAGE_REGISTRY: ghcr.io/${{ github.repository_owner }} @@ -52,8 +55,14 @@ jobs: with: submodules: recursive - - name: Maximize build space - uses: ublue-os/remove-unwanted-software@695eb75bc387dbcd9685a8e72d23439d8686cba6 # v10 + - name: Mount BTRFS for podman storage + id: container-storage-action + uses: ublue-os/container-storage-action@dc1f4c8f17b672069e921f001132f7cf98a423a6 + continue-on-error: true + with: + target-dir: /var/lib/containers + mount-opts: compress-force=zstd:2 + loopback-free: '1' - name: Install Just run: | @@ -135,12 +144,26 @@ jobs: path: /var/tmp/buildah-cache-* key: ${{ runner.os }}-${{ runner.arch }}-buildah-${{ env.CACHE_NAME }} + - name: Rechunk Image with rpm-ostree + id: rechunker + env: + MATRIX_BASE_NAME: ${{ matrix.base_name }} + MATRIX_STREAM_NAME: ${{ matrix.stream_name }} + MATRIX_IMAGE_FLAVOR: ${{ matrix.image_flavor }} + DEFAULT_TAG: ${{ env.DEFAULT_TAG }} + PREVIOUS_BUILD: ${{ github.event.inputs.previous_build || '1' }} + run: | + sudo -E $(command -v just) rechunk "${MATRIX_BASE_NAME}" \ + "${MATRIX_STREAM_NAME}" \ + "${MATRIX_IMAGE_FLAVOR}" \ + "1" \ + "0" \ + "${PREVIOUS_BUILD}" + - name: Setup Syft id: setup-syft if: github.event_name != 'pull_request' - uses: anchore/sbom-action/download-syft@e22c389904149dbc22b58101806040fa8d37a610 # v0 - with: - syft-version: v1.39.0 + uses: anchore/sbom-action/download-syft@e22c389904149dbc22b58101806040fa8d37a610 # v0.24.0 - name: Generate SBOM if: github.event_name != 'pull_request' @@ -156,32 +179,45 @@ jobs: "${MATRIX_IMAGE_FLAVOR}" \ "${SYFT_CMD}" - - name: Rechunk Image - id: rechunk-image + - name: Secureboot Check + id: secureboot shell: bash - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} run: | - sudo -E $(command -v just) rechunk "${{ matrix.base_name }}" \ - "${{ matrix.stream_name }}" \ - "${{ matrix.image_flavor }}" \ - "1" + sudo -E $(command -v just) secureboot "${{ matrix.base_name }}" \ + "${{ env.DEFAULT_TAG }}" \ + "${{ matrix.image_flavor }}" - - name: Load Image into Podman - id: load-rechunk - shell: bash + - name: Export to OCI Archive + if: github.event_name == 'pull_request' + id: oci-archive + env: + IMAGE_NAME: ${{ env.IMAGE_NAME }} + DEFAULT_TAG: ${{ env.DEFAULT_TAG }} run: | - sudo -E $(command -v just) load-rechunk "${{ matrix.base_name }}" \ + sudo -E $(command -v just) export-oci "${{ matrix.base_name }}" \ "${{ env.DEFAULT_TAG }}" \ "${{ matrix.image_flavor }}" - - name: Secureboot Check - id: secureboot - shell: bash + - name: Upload OCI dir as Artifact + if: github.event_name == 'pull_request' + uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7 + with: + name: ${{ env.IMAGE_NAME }}.oci + path: ${{ env.IMAGE_NAME }}.oci + archive: false + if-no-files-found: error + retention-days: 1 + + - name: PR Testing Instructions + if: github.event_name == 'pull_request' + id: pr-summary + env: + IMAGE_NAME: ${{ env.IMAGE_NAME }} + MATRIX_STREAM_NAME: "${{ matrix.stream_name }}" run: | - sudo -E $(command -v just) secureboot "${{ matrix.base_name }}" \ - "${{ env.DEFAULT_TAG }}" \ - "${{ matrix.image_flavor }}" + echo "Download the .oci file" >> $GITHUB_STEP_SUMMARY + echo "Rebase: sudo bootc switch --transport oci-archive /path/to/${IMAGE_NAME}.oci" >> $GITHUB_STEP_SUMMARY + echo "Go back to the production image e.g.: sudo bootc switch --enforce-container-sigpolicy ghcr.io/ublue-os/${IMAGE_NAME}:${MATRIX_STREAM_NAME}" >> $GITHUB_STEP_SUMMARY - name: Generate tags id: generate-tags @@ -218,10 +254,18 @@ jobs: with: string: ${{ env.IMAGE_REGISTRY }} + # TODO: remove me when we have a new podman in 26.04 runners + # needed because old podman doesn't push layer annotations for + # the rpm-ostree rechunker at all + - name: install podman from brew + if: github.event_name != 'pull_request' + run: | + /home/linuxbrew/.linuxbrew/bin/brew install podman + - name: Login to GitHub Container Registry if: github.event_name != 'pull_request' run: | - echo ${{ secrets.GITHUB_TOKEN }} | podman login ghcr.io -u ${{ github.actor }} --password-stdin + echo ${{ secrets.GITHUB_TOKEN }} | /home/linuxbrew/.linuxbrew/bin/podman login ghcr.io -u ${{ github.actor }} --password-stdin echo ${{ secrets.GITHUB_TOKEN }} | docker login ghcr.io -u ${{ github.actor }} --password-stdin - name: Push to GHCR @@ -234,6 +278,12 @@ jobs: timeout_minutes: 30 command: | set -euox pipefail + # HACK: push a second time so layer annotations are pushed + # TODO: remove me when https://github.com/containers/podman/issues/27796 fixed + + for tag in ${{ steps.generate-tags.outputs.alias_tags }}; do + sudo -E /home/linuxbrew/.linuxbrew/bin/podman push --compression-format=zstd --compression-level=3 ${{ env.IMAGE_NAME }}:${tag} ${{ steps.registry_case.outputs.lowercase }}/${{ env.IMAGE_NAME }}:${tag} + done digest_file="$PWD/.push-digest" for tag in ${{ steps.generate-tags.outputs.alias_tags }}; do diff --git a/Justfile b/Justfile index 34819e84a7b..6d1838658b7 100644 --- a/Justfile +++ b/Justfile @@ -1,5 +1,4 @@ repo_organization := "ublue-os" -rechunker_image := "ghcr.io/ublue-os/legacy-rechunk:v1.0.1-x86_64@sha256:2627cbf92ca60ab7372070dcf93b40f457926f301509ffba47a04d6a9e1ddaf7" common_image := "ghcr.io/projectbluefin/common:latest" brew_image := "ghcr.io/ublue-os/brew:latest" images := '( @@ -56,6 +55,7 @@ clean: rm -f previous.manifest.json rm -f changelog.md rm -f output.env + rm -rf sbom_out # Check if valid combo [group('Utility')] @@ -258,13 +258,12 @@ build-pipeline image="bluefin" tag="latest" flavor="main" kernel_pin="": #!/usr/bin/bash ${SUDOIF} {{ just }} build {{ image }} {{ tag }} {{ flavor }} 1 1 1 {{ kernel_pin }} -# Rechunk Image +# Rechunk Image with rpm-ostree [group('Image')] [private] -rechunk $image="bluefin" $tag="latest" $flavor="main" ghcr="0" pipeline="0": +rechunk $image="bluefin" $tag="latest" $flavor="main" ghcr="0" pipeline="0" previous_build="0": #!/usr/bin/bash - echo "::group:: Rechunk Prep" set -eoux pipefail # Validate @@ -272,154 +271,67 @@ rechunk $image="bluefin" $tag="latest" $flavor="main" ghcr="0" pipeline="0": # Image Name image_name=$({{ just }} image_name {{ image }} {{ tag }} {{ flavor }}) + fedora_version=$({{ just }} fedora_version {{ image }} {{ tag }} {{ flavor }}) + DEFAULT_TAG=$({{ just }} generate-default-tag {{ tag }} {{ ghcr }}) - # Check if image is already built - ID=$(${PODMAN} images --filter reference=localhost/"${image_name}":"${tag}" --format "'{{ '{{.ID}}' }}'") - if [[ -z "$ID" ]]; then - {{ just }} build "${image}" "${tag}" "${flavor}" - fi - - # Load into Rootful Podman - ID=$(${SUDOIF} ${PODMAN} images --filter reference=localhost/"${image_name}":"${tag}" --format "'{{ '{{.ID}}' }}'") - if [[ -z "$ID" && ! ${PODMAN} =~ docker ]]; then - COPYTMP=$(mktemp -p "${PWD}" -d -t podman_scp.XXXXXXXXXX) - ${SUDOIF} TMPDIR=${COPYTMP} ${PODMAN} image scp ${UID}@localhost::localhost/"${image_name}":"${tag}" root@localhost::localhost/"${image_name}":"${tag}" - rm -rf "${COPYTMP}" + if [[ "{{ ghcr }}" == "0" ]]; then + {{ just }} load-rootful "${image}" "${tag}" "${flavor}" fi - # Prep Container - CREF=$(${SUDOIF} ${PODMAN} create localhost/"${image_name}":"${tag}" bash) - OLD_IMAGE=$(${SUDOIF} ${PODMAN} inspect $CREF | jq -r '.[].Image') - OUT_NAME="${image_name}_build" - MOUNT=$(${SUDOIF} ${PODMAN} mount "${CREF}") + # TODO: Redo everything here with --previous-build in rpm-ostree 2026.1+ + # so we don't have to pull an old image + rename it + if [[ "{{ previous_build }}" == "1" ]]; then + PREVIOUS_IMAGE=ghcr.io/{{ repo_organization }}/"${image_name}":"${DEFAULT_TAG}" - # Fedora Version - fedora_version=$(${SUDOIF} ${PODMAN} inspect $CREF | jq -r '.[].Config.Labels["ostree.linux"]' | grep -oP 'fc\K[0-9]+') - - # Label Version - VERSION=$(${SUDOIF} ${PODMAN} inspect $CREF | jq -r '.[].Config.Labels["org.opencontainers.image.version"]') + # https://github.com/coreos/rpm-ostree/blob/7e2f2065a4aa4d5965b4537bb7d74e0b2898650e/rust/src/compose.rs#L522-L529 + if skopeo inspect docker://"${PREVIOUS_IMAGE}" | jq -e '.LayersData[1:] | all(.Annotations?["ostree.components"]?)'; then + ${SUDOIF} ${PODMAN} pull ${PREVIOUS_IMAGE} + else + echo "${PREVIOUS_IMAGE} doesn't exist. Making a fresh layer Plan instead." + fi + fi - # Git SHA - SHA="dedbeef" - if [[ -z "$(git status -s)" ]]; then - SHA=$(git rev-parse HEAD) - fi - - # Rest of Labels - LABELS=" - io.artifacthub.package.deprecated=false - io.artifacthub.package.keywords=bootc,fedora,bluefin,ublue,universal-blue - io.artifacthub.package.logo-url=https://avatars.githubusercontent.com/u/120078124?s=200&v=4 - io.artifacthub.package.maintainers=[{\"name\": \"castrojo\", \"email\": \"jorge.castro@gmail.com\"}] - io.artifacthub.package.readme-url=https://raw.githubusercontent.com/ublue-os/bluefin/refs/heads/main/README.md - org.opencontainers.image.created=$(date -u +%Y\-%m\-%d\T%H\:%M\:%S\Z) - org.opencontainers.image.license=Apache-2.0 - org.opencontainers.image.source=https://raw.githubusercontent.com/ublue-os/bluefin/refs/heads/main/Containerfile - org.opencontainers.image.title=${image_name} - org.opencontainers.image.url=https://projectbluefin.io - org.opencontainers.image.vendor={{ repo_organization }} - ostree.linux=$(${SUDOIF} ${PODMAN} inspect $CREF | jq -r '.[].Config.Labels["ostree.linux"]') - containers.bootc=1 - " - - # Cleanup Space during Github Action if [[ "{{ ghcr }}" == "1" ]]; then - base_image_name=silverblue-main - if [[ "${tag}" =~ stable ]]; then - tag="stable-daily" - fi - ID=$(${SUDOIF} ${PODMAN} images --filter reference=ghcr.io/{{ repo_organization }}/"${base_image_name}":${fedora_version} --format "{{ '{{.ID}}' }}") - if [[ -n "$ID" ]]; then - ${PODMAN} rmi "$ID" + CHUNKED_IMAGE="localhost/${image_name}:${DEFAULT_TAG}" + if [[ "{{ previous_build }}" == "1" ]]; then + CHUNKED_IMAGE="${PREVIOUS_IMAGE}" fi + else + # keep the original unrechunked image for local builds + CHUNKED_IMAGE="localhost/${image_name}:${DEFAULT_TAG}-chunked" fi - # Rechunk Container - rechunker="{{ rechunker_image }}" - - echo "::endgroup::" - echo "::group:: Prune" - - # Run Rechunker's Prune - ${SUDOIF} ${PODMAN} run --rm \ - --pull=${PULL_POLICY} \ - --security-opt label=disable \ - --volume "$MOUNT":/var/tree \ - --env TREE=/var/tree \ - --user 0:0 \ - "${rechunker}" \ - /sources/rechunk/1_prune.sh - - echo "::endgroup::" - echo "::group:: Create ostree tree" - - # Run Rechunker's Create - ${SUDOIF} ${PODMAN} run --rm \ - --security-opt label=disable \ - --volume "$MOUNT":/var/tree \ - --volume "cache_ostree:/var/ostree" \ - --env TREE=/var/tree \ - --env REPO=/var/ostree/repo \ - --env RESET_TIMESTAMP=1 \ - --user 0:0 \ - "${rechunker}" \ - /sources/rechunk/2_create.sh - - # Cleanup Temp Container Reference - ${SUDOIF} ${PODMAN} unmount "$CREF" - ${SUDOIF} ${PODMAN} rm "$CREF" - ${SUDOIF} ${PODMAN} rmi "$OLD_IMAGE" - - echo "::endgroup::" - echo "::group:: Rechunker" - - # Run Rechunker + # 128 layers, conservative default + # In CI this renames stable to stable-daily ${SUDOIF} ${PODMAN} run --rm \ --pull=${PULL_POLICY} \ - --security-opt label=disable \ - --volume "$PWD:/workspace" \ - --volume "$PWD:/var/git" \ - --volume cache_ostree:/var/ostree \ - --env REPO=/var/ostree/repo \ - --env PREV_REF=ghcr.io/ublue-os/"${image_name}":"${tag}" \ - --env OUT_NAME="$OUT_NAME" \ - --env LABELS="${LABELS}" \ - --env "DESCRIPTION='An interpretation of the Ubuntu spirit built on Fedora technology'" \ - --env "VERSION=${VERSION}" \ - --env VERSION_FN=/workspace/version.txt \ - --env OUT_REF="oci:$OUT_NAME" \ - --env GIT_DIR="/var/git" \ - --env REVISION="$SHA" \ - --user 0:0 \ - "${rechunker}" \ - /sources/rechunk/3_chunk.sh - - # Fix Permissions of OCI - ${SUDOIF} find ${OUT_NAME} -type d -exec chmod 0755 {} \; || true - ${SUDOIF} find ${OUT_NAME}* -type f -exec chmod 0644 {} \; || true - - if [[ "${UID}" -gt "0" ]]; then - ${SUDOIF} chown "${UID}:${GROUPS}" -R "${PWD}" - elif [[ -n "${SUDO_UID:-}" ]]; then - chown "${SUDO_UID}":"${SUDO_GID}" -R "${PWD}" + --privileged \ + -v "/var/lib/containers:/var/lib/containers" \ + --entrypoint /usr/bin/rpm-ostree \ + "quay.io/fedora-ostree-desktops/silverblue:${fedora_version}" \ + compose build-chunked-oci \ + --max-layers 127 \ + --format-version=2 \ + --bootc \ + --from "localhost/${image_name}:${tag}" \ + --output containers-storage:${CHUNKED_IMAGE} + + # rename the image to localhost + if [[ "{{ ghcr }}" == "1" && "{{ previous_build }}" == "1" ]]; then + ${SUDOIF} ${PODMAN} tag ${CHUNKED_IMAGE} "localhost/${image_name}:${DEFAULT_TAG}" + ${SUDOIF} ${PODMAN} image rm -f ${CHUNKED_IMAGE} fi - # Remove cache_ostree - ${SUDOIF} ${PODMAN} volume rm cache_ostree - - echo "::endgroup::" - # Pipeline Checks if [[ {{ pipeline }} == "1" && -n "${SUDO_USER:-}" ]]; then - sudo -u "${SUDO_USER}" {{ just }} load-rechunk "${image}" "${tag}" "${flavor}" sudo -u "${SUDO_USER}" {{ just }} secureboot "${image}" "${tag}" "${flavor}" fi -# Load OCI into Podman Store +# For Rechunk [group('Image')] -load-rechunk image="bluefin" tag="latest" flavor="main": +load-rootful $image="bluefin" $tag="latest" $flavor="main": #!/usr/bin/bash - set -eou pipefail + set -eoux pipefail # Validate {{ just }} validate {{ image }} {{ tag }} {{ flavor }} @@ -427,14 +339,27 @@ load-rechunk image="bluefin" tag="latest" flavor="main": # Image Name image_name=$({{ just }} image_name {{ image }} {{ tag }} {{ flavor }}) - # Load Image - OUT_NAME="${image_name}_build" - IMAGE=$(${PODMAN} pull oci:"${PWD}"/"${OUT_NAME}") - ${PODMAN} tag ${IMAGE} localhost/"${image_name}":{{ tag }} + if [[ ! "$(id -u)" == 0 && ! ${PODMAN} =~ docker ]]; then + ID=$(${PODMAN} images --filter reference=localhost/"${image_name}":"${tag}" --format "'{{ '{{.ID}}' }}'") + if [[ -z "$ID" ]]; then + {{ just }} build "$image" "$tag" "$flavor" + fi + ${PODMAN} image scp localhost/"${image_name}":"${tag}" root@localhost:: + fi - # Cleanup - rm -rf "${OUT_NAME}*" - rm -f previous.manifest.json +# Generate OCI Archive for PR Testing +[group('Image')] +export-oci $image="bluefin" $tag="latest" $flavor="main": + #!/usr/bin/bash + set -eoux pipefail + + # Validate + {{ just }} validate {{ image }} {{ tag }} {{ flavor }} + + # Image Name + image_name=$({{ just }} image_name {{ image }} {{ tag }} {{ flavor }}) + + ${PODMAN} push --compression-format=zstd --compression-level=3 localhost/"${image_name}":"${tag}" oci-archive:"${image_name}".oci # Run Container [group('Image')] diff --git a/build_files/base/17-cleanup.sh b/build_files/base/17-cleanup.sh index 45e4178a48e..b11f69cdc43 100755 --- a/build_files/base/17-cleanup.sh +++ b/build_files/base/17-cleanup.sh @@ -16,6 +16,10 @@ systemctl enable rpm-ostree-countme.service systemctl enable tailscaled.service systemctl enable ublue-system-setup.service +# see /usr/bin/rechunker-group-fix +# DO NOT REMOVE THIS +systemctl enable rechunker-group-fix.service + systemctl enable flatpak-preinstall.service # Updater diff --git a/build_files/base/20-tests.sh b/build_files/base/20-tests.sh index 81daadefe49..b1e7baa8bdc 100755 --- a/build_files/base/20-tests.sh +++ b/build_files/base/20-tests.sh @@ -79,6 +79,7 @@ if [[ "${IMAGE_NAME}" =~ nvidia ]]; then fi IMPORTANT_UNITS=( + rechunker-group-fix.service rpm-ostree-countme.timer tailscaled.service ublue-system-setup.service diff --git a/system_files/shared/usr/bin/rechunker-group-fix b/system_files/shared/usr/bin/rechunker-group-fix new file mode 100755 index 00000000000..78bfbaafdce --- /dev/null +++ b/system_files/shared/usr/bin/rechunker-group-fix @@ -0,0 +1,18 @@ +#!/usr/bin/env bash + +# To use this script, you'll want to put this in your systemd service: +# rm /etc/gshadow +# systemd-sysusers +# (run this script) +# systemd-tmpfiles --create --remove --boot --exclude-prefix=/dev +# This will populate /etc/group successfully, and then populate /etc/gshadow +# with any missing groups that we nuked when we removed /etc/gshadow + +GSHADOW_FILE="/etc/gshadow" +GROUP_FILE="/etc/group" + +while IFS= read -r f; do + [ -z "$f" ] && continue + group_name=$(echo "$f" | cut -f1 -d':') + grep -q "^${group_name}:" "$GSHADOW_FILE" || echo "${group_name}:!*::" >> "$GSHADOW_FILE" +done < "$GROUP_FILE" diff --git a/system_files/shared/usr/lib/systemd/system/rechunker-group-fix.service b/system_files/shared/usr/lib/systemd/system/rechunker-group-fix.service new file mode 100644 index 00000000000..8864ffcd771 --- /dev/null +++ b/system_files/shared/usr/lib/systemd/system/rechunker-group-fix.service @@ -0,0 +1,32 @@ +# We have this script so that people using images with `nss-altfiles` (`/usr/lib/g{roup,shadow}`) +# do not break their systems when rebasing to an image without that +# This usually happens when using https://github.com/hhd-dev/rechunk then rebasing to an image without it. +# Please DO NOT remove this unless this is fully, completely obsolete. +# This is exactly what is making it break: https://github.com/ublue-os/legacy-rechunk/blob/1d2b0c2e99afbdc2eb06788ae28e157a88b03d70/1_prune.sh#L41-L47 +# Users WILL experience black screens and systems will NOT boot if this script malfunctions. Please test this properly and always make sure this works +# Relevant issues: +# - https://github.com/bootc-dev/bootc/issues/1179#issuecomment-2708305926 +# - https://github.com/ublue-os/main/issues/759 +# - https://github.com/ublue-os/bluefin-lts/issues/918 +# - https://github.com/ublue-os/image-template/issues/177 +# - https://github.com/ublue-os/aurora/issues/1468 +# - https://github.com/ublue-os/bluefin/issues/3852 +# This got created on Tue, 16 Dec 2025 00:44:58 -0300 +[Unit] +Description=Fix groups for Legacy rechunker +ConditionPathExists=/run/ostree-booted +ConditionPathExists=!/var/lib/.rechunker-group-fix-done +Wants=local-fs.target +After=local-fs.target +Before=systemd-user-sessions.service + +[Service] +Type=oneshot +ExecStart=/usr/bin/bash -c 'rm -f /etc/gshadow' +ExecStart=/usr/bin/systemd-sysusers +ExecStart=/usr/bin/rechunker-group-fix +ExecStart=/usr/bin/systemd-tmpfiles --create --remove --boot --exclude-prefix=/dev +ExecStart=/usr/bin/bash -c 'mkdir -p /var/lib && touch /var/lib/.rechunker-group-fix-done' + +[Install] +WantedBy=default.target multi-user.target