From 75c3b502d793e0a7bce639d012e93f186a1e34ee Mon Sep 17 00:00:00 2001 From: Wesley van Tilburg Date: Tue, 20 Oct 2026 09:13:03 +0000 Subject: [PATCH 1/4] fedora-intune: report full image version to Intune, Defender compliance script Himmelblau reports VERSION_ID from /etc/os-release as the device OS version, so Intune showed "44". Give himmelblaud and himmelblaud-tasks their own os-release (bind-mounted in their mount namespace only) with VERSION_ID set to OSTREE_VERSION, e.g. 44.20261019.0. The rest of the system keeps the stock VERSION_ID that toolbox, flatpak and dnf expect. Add an Intune custom compliance discovery script and rules for Microsoft Defender for Endpoint (installed, running, onboarded, licensed, healthy, real-time protection, definitions up to date). These are uploaded to Intune, not shipped in the image. Co-Authored-By: Claude Opus 5.5 (1M context) --- .../files/10-himmelblau-os-release.conf | 9 ++ .../fedora-intune/files/himmelblau-os-release | 23 ++++ .../files/himmelblau-os-release.service | 10 ++ images/fedora-intune/himmelblau.yaml | 6 ++ .../intune/defender-compliance.json | 102 ++++++++++++++++++ .../intune/defender-compliance.sh | 64 +++++++++++ 6 files changed, 214 insertions(+) create mode 100644 images/fedora-intune/files/10-himmelblau-os-release.conf create mode 100644 images/fedora-intune/files/himmelblau-os-release create mode 100644 images/fedora-intune/files/himmelblau-os-release.service create mode 100644 images/fedora-intune/intune/defender-compliance.json create mode 100755 images/fedora-intune/intune/defender-compliance.sh diff --git a/images/fedora-intune/files/10-himmelblau-os-release.conf b/images/fedora-intune/files/10-himmelblau-os-release.conf new file mode 100644 index 0000000..758289a --- /dev/null +++ b/images/fedora-intune/files/10-himmelblau-os-release.conf @@ -0,0 +1,9 @@ +# Report the full image version (VERSION_ID=44.20261019.0) to Entra ID and +# Intune instead of "44"; see /usr/libexec/himmelblau-os-release. Only this +# service's mount namespace sees the replacement os-release. +[Unit] +Requires=himmelblau-os-release.service +After=himmelblau-os-release.service + +[Service] +BindReadOnlyPaths=/run/himmelblau-os-release:/etc/os-release diff --git a/images/fedora-intune/files/himmelblau-os-release b/images/fedora-intune/files/himmelblau-os-release new file mode 100644 index 0000000..06d015b --- /dev/null +++ b/images/fedora-intune/files/himmelblau-os-release @@ -0,0 +1,23 @@ +#!/usr/bin/env bash +# Write the os-release that the Himmelblau daemons see (10-os-release.conf): +# the image's /usr/lib/os-release with VERSION_ID set to the full image version +# (OSTREE_VERSION, e.g. 44.20261019.0), so Intune shows the build and not just +# "44". Himmelblau reports VERSION_ID as the device's OS version and has no +# setting for it. The rest of the system keeps the stock VERSION_ID, which +# toolbox, flatpak and dnf expect to be the plain release. +set -euo pipefail + +SRC=/usr/lib/os-release +DEST=/run/himmelblau-os-release + +version="$(. "${SRC}" && echo "${OSTREE_VERSION:-${IMAGE_VERSION:-}}")" + +tmp="$(mktemp "${DEST}.XXXXXX")" +if [[ -n "${version}" ]]; then + sed "s/^VERSION_ID=.*/VERSION_ID=${version}/" "${SRC}" > "${tmp}" +else + # No image version (not an ostree/bootc deployment): pass it through as-is. + cat "${SRC}" > "${tmp}" +fi +chmod 0644 "${tmp}" +mv -f "${tmp}" "${DEST}" diff --git a/images/fedora-intune/files/himmelblau-os-release.service b/images/fedora-intune/files/himmelblau-os-release.service new file mode 100644 index 0000000..83aa21e --- /dev/null +++ b/images/fedora-intune/files/himmelblau-os-release.service @@ -0,0 +1,10 @@ +[Unit] +Description=Generate the os-release reported by Himmelblau to Intune +DefaultDependencies=no +After=local-fs.target +Before=himmelblaud.service himmelblaud-tasks.service + +[Service] +Type=oneshot +ExecStart=/usr/libexec/himmelblau-os-release +RemainAfterExit=yes diff --git a/images/fedora-intune/himmelblau.yaml b/images/fedora-intune/himmelblau.yaml index b8b8e96..4f378d3 100644 --- a/images/fedora-intune/himmelblau.yaml +++ b/images/fedora-intune/himmelblau.yaml @@ -25,6 +25,11 @@ add-files: - ["himmelblau.conf", "/etc/himmelblau/himmelblau.conf"] - ["50-himmelblau.preset", "/usr/lib/systemd/system-preset/50-himmelblau.preset"] - ["50-himmelblau-user.preset", "/usr/lib/systemd/user-preset/50-himmelblau-user.preset"] + # Report the full image version (44.20261019.0) as the OS version in Intune. + - ["himmelblau-os-release", "/usr/libexec/himmelblau-os-release"] + - ["himmelblau-os-release.service", "/usr/lib/systemd/system/himmelblau-os-release.service"] + - ["10-himmelblau-os-release.conf", "/usr/lib/systemd/system/himmelblaud.service.d/10-os-release.conf"] + - ["10-himmelblau-os-release.conf", "/usr/lib/systemd/system/himmelblaud-tasks.service.d/10-os-release.conf"] postprocess: # The package's %post selects its authselect vendor profile but drops @@ -38,6 +43,7 @@ postprocess: getent passwd "${user}" >/dev/null done grep -q pam_himmelblau.so /etc/pam.d/system-auth + chmod 0755 /usr/libexec/himmelblau-os-release # himmelblau-selinux builds and loads its module in %posttrans, which does # nothing in a compose; do it here. diff --git a/images/fedora-intune/intune/defender-compliance.json b/images/fedora-intune/intune/defender-compliance.json new file mode 100644 index 0000000..da37223 --- /dev/null +++ b/images/fedora-intune/intune/defender-compliance.json @@ -0,0 +1,102 @@ +{ + "Rules": [ + { + "SettingName": "DefenderInstalled", + "Operator": "IsEquals", + "DataType": "Boolean", + "Operand": true, + "MoreInfoUrl": "https://learn.microsoft.com/en-us/defender-endpoint/linux-install-manually", + "RemediationStrings": [ + { + "Language": "en_US", + "Title": "Microsoft Defender is not installed.", + "Description": "Microsoft Defender for Endpoint (mdatp) must be installed on this device." + } + ] + }, + { + "SettingName": "DefenderRunning", + "Operator": "IsEquals", + "DataType": "Boolean", + "Operand": true, + "MoreInfoUrl": "https://learn.microsoft.com/en-us/defender-endpoint/linux-install-manually", + "RemediationStrings": [ + { + "Language": "en_US", + "Title": "Microsoft Defender is not running.", + "Description": "The Microsoft Defender service (mdatp.service) is not responding. Restart the device or contact IT." + } + ] + }, + { + "SettingName": "DefenderOnboarded", + "Operator": "IsEquals", + "DataType": "Boolean", + "Operand": true, + "MoreInfoUrl": "https://learn.microsoft.com/en-us/defender-endpoint/linux-install-manually", + "RemediationStrings": [ + { + "Language": "en_US", + "Title": "Microsoft Defender is not onboarded.", + "Description": "This device is not onboarded to your organization's Microsoft Defender tenant. Contact IT." + } + ] + }, + { + "SettingName": "DefenderLicensed", + "Operator": "IsEquals", + "DataType": "Boolean", + "Operand": true, + "MoreInfoUrl": "https://learn.microsoft.com/en-us/defender-endpoint/linux-install-manually", + "RemediationStrings": [ + { + "Language": "en_US", + "Title": "Microsoft Defender is not licensed.", + "Description": "Microsoft Defender has no valid license on this device. Contact IT." + } + ] + }, + { + "SettingName": "DefenderHealthy", + "Operator": "IsEquals", + "DataType": "Boolean", + "Operand": true, + "MoreInfoUrl": "https://learn.microsoft.com/en-us/defender-endpoint/linux-install-manually", + "RemediationStrings": [ + { + "Language": "en_US", + "Title": "Microsoft Defender reports a problem.", + "Description": "Run 'mdatp health' to see the reported issues, or contact IT." + } + ] + }, + { + "SettingName": "DefenderRealTimeProtection", + "Operator": "IsEquals", + "DataType": "Boolean", + "Operand": true, + "MoreInfoUrl": "https://learn.microsoft.com/en-us/defender-endpoint/linux-install-manually", + "RemediationStrings": [ + { + "Language": "en_US", + "Title": "Real-time protection is off.", + "Description": "Microsoft Defender real-time protection must be enabled." + } + ] + }, + { + "SettingName": "DefenderDefinitionsUpToDate", + "Operator": "IsEquals", + "DataType": "Boolean", + "Operand": true, + "MoreInfoUrl": "https://learn.microsoft.com/en-us/defender-endpoint/linux-install-manually", + "RemediationStrings": [ + { + "Language": "en_US", + "Title": "Microsoft Defender definitions are out of date.", + "Description": "Connect the device to the network so Microsoft Defender can update its security intelligence." + } + ] + } + ] +} diff --git a/images/fedora-intune/intune/defender-compliance.sh b/images/fedora-intune/intune/defender-compliance.sh new file mode 100755 index 0000000..f5f3de7 --- /dev/null +++ b/images/fedora-intune/intune/defender-compliance.sh @@ -0,0 +1,64 @@ +#!/bin/bash +# Intune custom compliance discovery script for Linux: is Microsoft Defender +# for Endpoint (mdatp) installed, onboarded, licensed and protecting? +# +# Upload in Intune > Devices > Compliance > Scripts (Linux), and pair it with +# defender-compliance.json in a Linux compliance policy (Custom compliance). +# Himmelblau runs it as root inside himmelblaud-tasks.service and sends the +# JSON on stdout to Intune, which evaluates it against the rules. Every setting +# is always emitted, so a missing or stopped Defender reads as non-compliant +# instead of as a script error. + +MDATP=/usr/bin/mdatp + +# Print one `mdatp health` field without quotes or a trailing " [managed]". +# Unlicensed or unonboarded, mdatp prints "ATTENTION: ..." lines first. +field() { + "${MDATP}" health --field "$1" 2>/dev/null | grep -v '^ATTENTION' | tail -n1 \ + | sed -e 's/ \[managed\]$//' -e 's/^"//' -e 's/"$//' +} + +bool() { + [[ "$1" == true ]] && echo true || echo false +} + +installed=false +running=false +healthy=false +licensed=false +onboarded=false +rtp=false +definitions_ok=false +definitions_age=-1 +version="" + +if [[ -x "${MDATP}" ]]; then + installed=true + if [[ "$(field healthy)" =~ ^(true|false)$ ]]; then + # The daemon answered. + running=true + healthy="$(bool "$(field healthy)")" + licensed="$(bool "$(field licensed)")" + rtp="$(bool "$(field real_time_protection_enabled)")" + [[ -n "$(field org_id)" ]] && onboarded=true + [[ "$(field definitions_status)" == up_to_date ]] && definitions_ok=true + age="$(field definitions_updated_minutes_ago)" + [[ "${age}" =~ ^[0-9]+$ ]] && definitions_age=$(( age / 1440 )) + version="$(field app_version)" + version="${version//[^0-9A-Za-z._-]/}" + fi +fi + +cat < Date: Tue, 20 Oct 2026 09:15:28 +0000 Subject: [PATCH 2/4] fedora-intune: cron for Intune config scripts, Defender remediation script Himmelblau installs Intune Linux custom configuration scripts as /etc/cron.d jobs, and the image had no cron, so a script policy failed to apply. Add cronie and enable crond. Add a custom configuration script that repairs Microsoft Defender on its schedule: finishes first-boot setup, restarts a stopped daemon, onboards with the tenant's mdatp_onboard.json (pasted into the script in Intune), and re-enables real-time protection and updates definitions when needed. Co-Authored-By: Claude Opus 5.5 (1M context) --- .../fedora-intune/files/50-himmelblau.preset | 2 + images/fedora-intune/himmelblau.yaml | 3 + .../intune/defender-remediation.sh | 102 ++++++++++++++++++ 3 files changed, 107 insertions(+) create mode 100755 images/fedora-intune/intune/defender-remediation.sh diff --git a/images/fedora-intune/files/50-himmelblau.preset b/images/fedora-intune/files/50-himmelblau.preset index 9822af2..a86eaf7 100644 --- a/images/fedora-intune/files/50-himmelblau.preset +++ b/images/fedora-intune/files/50-himmelblau.preset @@ -2,3 +2,5 @@ enable himmelblaud.service enable himmelblaud-tasks.service enable himmelblau-hsm-pin-init.service +# Runs the Intune custom configuration scripts Himmelblau puts in /etc/cron.d. +enable crond.service diff --git a/images/fedora-intune/himmelblau.yaml b/images/fedora-intune/himmelblau.yaml index 4f378d3..f395150 100644 --- a/images/fedora-intune/himmelblau.yaml +++ b/images/fedora-intune/himmelblau.yaml @@ -20,6 +20,8 @@ packages: # himmelblau-selinux compiles its policy module at install time. - make - selinux-policy-devel + # Intune custom configuration scripts are installed as /etc/cron.d jobs. + - cronie add-files: - ["himmelblau.conf", "/etc/himmelblau/himmelblau.conf"] @@ -44,6 +46,7 @@ postprocess: done grep -q pam_himmelblau.so /etc/pam.d/system-auth chmod 0755 /usr/libexec/himmelblau-os-release + test -d /etc/cron.d # himmelblau-selinux builds and loads its module in %posttrans, which does # nothing in a compose; do it here. diff --git a/images/fedora-intune/intune/defender-remediation.sh b/images/fedora-intune/intune/defender-remediation.sh new file mode 100755 index 0000000..7f8c75c --- /dev/null +++ b/images/fedora-intune/intune/defender-remediation.sh @@ -0,0 +1,102 @@ +#!/bin/bash +# Intune custom configuration script for Linux: bring Microsoft Defender for +# Endpoint (mdatp) back to the state defender-compliance.sh checks for. +# +# Intune has no "run only when non-compliant" trigger for Linux, so this runs +# on the policy's schedule and only acts on what is wrong; on a healthy device +# it only reads `mdatp health`. Himmelblau installs it as an /etc/cron.d job. +# +# Intune > Devices > Configuration > Create > Linux > Settings catalog > +# Custom Configuration: +# Execution context: root +# Execution frequency: every 1 hour (or 15 minutes while rolling out) +# Execution retries: 1 +# Script: this file, with ONBOARDING_JSON filled in +# +# Exit status is non-zero when Defender is still not healthy afterwards. + +# Contents of mdatp_onboard.json from the Defender portal: Settings > Endpoints +# > Onboarding > Linux Server > "Local Script (Python)". The downloaded +# MicrosoftDefenderATPOnboardingLinuxServer.py writes this file to +# /etc/opt/microsoft/mdatp/ on the machine it runs on; paste that file's +# contents here. Leave the placeholder to skip onboarding. +ONBOARDING_JSON=$(cat <<'JSON' +REPLACE_WITH_MDATP_ONBOARD_JSON +JSON +) + +MDATP=/usr/bin/mdatp +ONBOARD_FILE=/etc/opt/microsoft/mdatp/mdatp_onboard.json + +log() { + logger -t defender-remediation -- "$*" + echo "$*" +} + +# Print one `mdatp health` field without quotes, a trailing " [managed]" or +# the "ATTENTION: ..." lines mdatp prints while unlicensed. +field() { + "${MDATP}" health --field "$1" 2>/dev/null | grep -v '^ATTENTION' | tail -n1 \ + | sed -e 's/ \[managed\]$//' -e 's/^"//' -e 's/"$//' +} + +# Wait up to 2 minutes for the daemon to answer. +wait_for_daemon() { + local i + for i in $(seq 1 24); do + [[ "$(field healthy)" =~ ^(true|false)$ ]] && return 0 + sleep 5 + done + return 1 +} + +if [[ ! -x "${MDATP}" ]]; then + log "mdatp is not installed; it ships in the image, so update or rebase the device." + exit 1 +fi + +# The image finishes the Defender install on first boot (mdatp-firstboot). +if [[ ! -f /var/lib/mdatp-firstboot.done ]]; then + log "Defender first-boot setup has not completed; starting it." + systemctl start mdatp-firstboot.service +fi + +if ! systemctl is-active --quiet mdatp.service; then + log "mdatp.service is not running; restarting it." + systemctl restart mdatp.service +fi +if ! wait_for_daemon; then + log "The Defender daemon does not respond." + exit 1 +fi + +if [[ -z "$(field org_id)" ]]; then + if [[ "${ONBOARDING_JSON}" == REPLACE_WITH_MDATP_ONBOARD_JSON ]]; then + log "Defender is not onboarded and this script has no onboarding JSON." + else + log "Defender is not onboarded; writing ${ONBOARD_FILE}." + install -d -m 0755 "$(dirname "${ONBOARD_FILE}")" + printf '%s\n' "${ONBOARDING_JSON}" > "${ONBOARD_FILE}.tmp" + chown root:mdatp "${ONBOARD_FILE}.tmp" + chmod 0600 "${ONBOARD_FILE}.tmp" + mv -f "${ONBOARD_FILE}.tmp" "${ONBOARD_FILE}" + systemctl restart mdatp.service + wait_for_daemon || { log "The Defender daemon does not respond after onboarding."; exit 1; } + fi +fi + +# Settings Defender's own policy manages show " [managed]" and can't be +# changed locally; this only fixes a locally disabled real-time protection. +if [[ "$(field real_time_protection_enabled)" != true ]]; then + log "Real-time protection is off; enabling it." + "${MDATP}" config real-time-protection --value enabled +fi + +if [[ "$(field definitions_status)" != up_to_date ]]; then + log "Definitions are not up to date; updating them." + "${MDATP}" definitions update +fi + +healthy="$(field healthy)" +log "healthy=${healthy} licensed=$(field licensed) org_id=$(field org_id) rtp=$(field real_time_protection_enabled) definitions=$(field definitions_status)" +[[ "${healthy}" == true ]] -- 2.54.0 From 2b7811312408349b35797d6c44e418d19e00b09a Mon Sep 17 00:00:00 2001 From: Wesley van Tilburg Date: Tue, 20 Oct 2026 09:19:16 +0000 Subject: [PATCH 3/4] fedora-intune: install Defender from Microsoft's Fedora 44 repo The RHEL 10 repo was there for the Intune app, which only ships for RHEL; that app is gone and the repo now only provides mdatp. Use Microsoft's Fedora 44 repo instead: Fedora is a supported Defender distribution (servers and the Linux desktop preview), and it carries a newer build (101.26091.0003 vs 101.26081.0010 in the RHEL 10 repo). Same signing key and signed metadata. Co-Authored-By: Claude Opus 5.5 (1M context) --- images/fedora-intune/build.conf | 2 +- images/fedora-intune/defender.yaml | 6 +++--- images/fedora-intune/manifest.yaml | 2 +- images/fedora-intune/repos/microsoft.repo | 12 ++++++++---- 4 files changed, 13 insertions(+), 9 deletions(-) diff --git a/images/fedora-intune/build.conf b/images/fedora-intune/build.conf index 967992b..a3bd73a 100644 --- a/images/fedora-intune/build.conf +++ b/images/fedora-intune/build.conf @@ -4,7 +4,7 @@ BUILD_MODE=upstream UPSTREAM_REPO="https://gitlab.com/fedora/ostree/ci-test.git" -# Stable only; Defender comes from Microsoft's RHEL 10 repo, not rawhide. +# Stable only; Microsoft's Defender repo (fedora/44) has no rawhide build. declare -A UPSTREAM_REFS=( [44]=f44 ) diff --git a/images/fedora-intune/defender.yaml b/images/fedora-intune/defender.yaml index 801d529..435b6a5 100644 --- a/images/fedora-intune/defender.yaml +++ b/images/fedora-intune/defender.yaml @@ -1,5 +1,5 @@ -# Microsoft Defender for Endpoint on Linux (mdatp), RHEL 10 build, from the -# microsoft-rhel10 repo (repos/microsoft.repo). +# Microsoft Defender for Endpoint on Linux (mdatp), Fedora 44 build, from the +# microsoft-fedora44 repo (repos/microsoft.repo). # # mdatp's %post does the real install: units, SELinux module, and per-machine # state under /var/opt/microsoft/mdatp (machine GUID, definitions database, @@ -11,7 +11,7 @@ # from the Defender portal on the device. repo-packages: - - repo: microsoft-rhel10 + - repo: microsoft-fedora44 packages: - mdatp diff --git a/images/fedora-intune/manifest.yaml b/images/fedora-intune/manifest.yaml index 84107b0..5d7e413 100644 --- a/images/fedora-intune/manifest.yaml +++ b/images/fedora-intune/manifest.yaml @@ -27,7 +27,7 @@ opt-usrlocal: root repos: - himmelblau - - microsoft-rhel10 + - microsoft-fedora44 - microsoft-edge include: diff --git a/images/fedora-intune/repos/microsoft.repo b/images/fedora-intune/repos/microsoft.repo index 3c6438f..723f7ea 100644 --- a/images/fedora-intune/repos/microsoft.repo +++ b/images/fedora-intune/repos/microsoft.repo @@ -1,9 +1,13 @@ # packages.microsoft.com, limited with includepkgs to the packages the image -# uses, so nothing else from the RHEL repo can replace a Fedora package. +# uses, so nothing else from the Microsoft repo can replace a Fedora package. +# +# Fedora 44 build of Defender (mdatp): Fedora is a supported Defender +# distribution, servers and desktops. Pinned to 44 rather than $releasever so a +# new Fedora release doesn't fail the build before Microsoft publishes its repo. -[microsoft-rhel10] -name=Microsoft Production (RHEL 10) -baseurl=https://packages.microsoft.com/rhel/10/prod/ +[microsoft-fedora44] +name=Microsoft Production (Fedora 44) +baseurl=https://packages.microsoft.com/fedora/44/prod/ enabled=1 gpgcheck=1 repo_gpgcheck=1 -- 2.54.0 From 1dbe7f6895bdb624a36cc6a4d30a8edd9162ee2c Mon Sep 17 00:00:00 2001 From: Wesley van Tilburg Date: Tue, 20 Oct 2026 09:21:08 +0000 Subject: [PATCH 4/4] ci: only rebuild changed images The changes job ran on the bare runner, which has no git (the build jobs install it before checkout). actions/checkout then fell back to a tarball without history, `git diff` failed, `|| true` turned that into an empty change list, and an empty list means "rebuild everything" - on every push and PR. Run the job in the buildroot container with git and nodejs, warn when the diff fails instead of hiding it, treat a diff with no changes as "build nothing", and let a manual run pick one image. The weekly scheduled run still rebuilds all images for package updates. Co-Authored-By: Claude Opus 5.5 (1M context) --- .gitea/workflows/build.yaml | 66 ++++++++++++++++++++++++++++++++----- 1 file changed, 57 insertions(+), 9 deletions(-) diff --git a/.gitea/workflows/build.yaml b/.gitea/workflows/build.yaml index 140a38f..fb58d06 100644 --- a/.gitea/workflows/build.yaml +++ b/.gitea/workflows/build.yaml @@ -2,6 +2,12 @@ name: Build containers on: workflow_dispatch: + inputs: + image: + description: Image to build + type: choice + options: [all, fedora-cosmic, fedora-remote, fedora-intune] + default: all pull_request: branches: ["main"] push: @@ -15,11 +21,17 @@ jobs: changes: name: Compute changes runs-on: job-v2 + # The runner has no git: without it actions/checkout falls back to a + # tarball with no history, `git diff` fails and every image is rebuilt. + container: + image: "quay.io/fedora-ostree-desktops/buildroot:44" outputs: cosmic: ${{ steps.changes.outputs.cosmic }} remote: ${{ steps.changes.outputs.remote }} intune: ${{ steps.changes.outputs.intune }} steps: + - name: Install tools + run: dnf install -y nodejs git - name: Checkout uses: actions/checkout@v4 with: @@ -27,17 +39,53 @@ jobs: - name: Detect changed images id: changes shell: bash + env: + EVENT: ${{ github.event_name }} + BEFORE: ${{ github.event.before }} + SHA: ${{ github.sha }} + BASE_REF: ${{ github.base_ref }} + DISPATCH_IMAGE: ${{ inputs.image }} run: | set -euo pipefail - if [[ "${{ github.event_name }}" == "push" \ - && -n "${{ github.event.before }}" \ - && "${{ github.event.before }}" != "0000000000000000000000000000000000000000" ]]; then - changed="$(git diff --name-only "${{ github.event.before }}" "${{ github.sha }}" || true)" - elif [[ "${{ github.event_name }}" == "pull_request" ]]; then - changed="$(git diff --name-only "origin/${{ github.base_ref }}...HEAD" || true)" - else - changed="" - fi + # Diff the event's commits; any failure rebuilds everything, but + # say so instead of hiding it. + diff_or_all() { + if ! changed="$(git diff --name-only "$@")"; then + echo "::warning::git diff $* failed; rebuilding every image" + changed="" + elif [[ -z "${changed}" ]]; then + # Nothing changed (e.g. an empty merge): build nothing rather + # than everything. + changed="(none)" + fi + } + case "${EVENT}" in + push) + if [[ -n "${BEFORE}" && "${BEFORE}" != 0000000000000000000000000000000000000000 ]]; then + diff_or_all "${BEFORE}" "${SHA}" + else + echo "::warning::push without a previous commit; rebuilding every image" + changed="" + fi + ;; + pull_request) + diff_or_all "origin/${BASE_REF}...HEAD" + ;; + workflow_dispatch) + # Pretend the chosen image's directory changed; "all" (or no + # input) rebuilds everything. + if [[ -n "${DISPATCH_IMAGE}" && "${DISPATCH_IMAGE}" != all ]]; then + changed="images/${DISPATCH_IMAGE}/" + else + changed="" + fi + ;; + *) + # schedule: the weekly rebuild picks up package updates for all. + changed="" + ;; + esac + printf 'Changed paths:\n%s\n' "${changed:-(all)}" eval "$(printf '%s\n' "${changed}" | ./ci-changes.sh)" echo "cosmic=${cosmic}" >> "$GITHUB_OUTPUT" echo "remote=${remote}" >> "$GITHUB_OUTPUT" -- 2.54.0