Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
175 changes: 175 additions & 0 deletions .github/scripts/recipe/classify-targets.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,175 @@
#!/usr/bin/env bash
set -euo pipefail

# Inputs (env):
# RECIPE - recipe name (from setup parse step)
#
# Compares the branch against origin/master (three-dot) and emits a JSON
# array of matrix include entries for the CI targets actually affected.
# Merging master into the branch is a no-op for classification because the
# merge-base advances with master.

: "${RECIPE:?RECIPE required}"

ALL_TARGETS=(ubuntu mac debian-trixie fedora-43 fedora-44 opensuse-tumbleweed cachyos)

declare -A TARGET_JSON=(
[ubuntu]='{"name":"ubuntu","kind":"ubuntu","runs-on":"ubuntu-24.04","container":"","pkg":""}'
[mac]='{"name":"mac","kind":"mac","runs-on":"macos-26","container":"","pkg":""}'
[debian-trixie]='{"name":"debian-trixie","kind":"linux-container","runs-on":"ubuntu-24.04","container":"docker.io/library/debian:trixie","pkg":"apt"}'
[fedora-43]='{"name":"fedora-43","kind":"linux-container","runs-on":"ubuntu-24.04","container":"quay.io/fedora/fedora:43","pkg":"dnf"}'
[fedora-44]='{"name":"fedora-44","kind":"linux-container","runs-on":"ubuntu-24.04","container":"quay.io/fedora/fedora:44","pkg":"dnf"}'
[opensuse-tumbleweed]='{"name":"opensuse-tumbleweed","kind":"linux-container","runs-on":"ubuntu-24.04","container":"registry.opensuse.org/opensuse/tumbleweed:latest","pkg":"zypper"}'
[cachyos]='{"name":"cachyos","kind":"linux-container","runs-on":"ubuntu-24.04","container":"docker.io/cachyos/cachyos:latest","pkg":"pacman"}'
)

declare -A affected=()

mark() { affected[$1]=1; }

mark_all() {
local t
for t in "${ALL_TARGETS[@]}"; do mark "$t"; done
}

# Map a case-branch OS label to one or more target names.
mark_os_label() {
case "$1" in
mac) mark mac ;;
cachyos) mark cachyos ;;
opensuse-tumbleweed) mark opensuse-tumbleweed ;;
fedora) mark fedora-43; mark fedora-44 ;;
ubuntu) mark ubuntu ;;
debian) mark debian-trixie ;;
all|*) mark_all ;;
esac
}

# Walk a script, label each line by the OS case branch it belongs to.
# Lines outside any `case "$os" in … esac` block, or inside `*)` arms,
# or inside an unrecognized case header, get the label "all".
extract_sections() {
awk '
function trim(s) { sub(/^[[:space:]]+/, "", s); sub(/[[:space:]]+$/, "", s); return s }
BEGIN { in_case=0; labels="all" }
{
line=$0
if (!in_case) {
if (line ~ /^[[:space:]]*case[[:space:]]+"?\$\{?os\}?"?[[:space:]]+in[[:space:]]*$/) {
in_case=1; labels="all"
print labels "\t" line; next
}
print "all\t" line; next
}
if (line ~ /^[[:space:]]*esac([[:space:]]|$)/) {
print "all\t" line; in_case=0; labels="all"; next
}
if (line ~ /^[[:space:]]*[A-Za-z0-9_*][A-Za-z0-9_*|-]*\)/) {
hdr=line; sub(/\).*/, "", hdr); hdr=trim(hdr)
if (hdr ~ /\*/) { labels="all" }
else {
labels=""
n=split(hdr, parts, "|")
for (i=1;i<=n;i++) {
p=trim(parts[i])
if (p=="") continue
if (p ~ /^(mac|cachyos|opensuse-tumbleweed|fedora|ubuntu|debian)$/) {
labels = (labels=="" ? p : labels "," p)
} else {
labels="all"; break
}
}
if (labels=="") labels="all"
}
print labels "\t" line; next
}
if (line ~ /^[[:space:]]*;;[[:space:]]*$/) {
print labels "\t" line; labels="all"; next
}
print labels "\t" line
}
'
}

# Print only lines whose label set contains $1, in their original order.
sections_for() {
awk -F'\t' -v want="$1" '
{
n=split($1, parts, ",")
for (i=1;i<=n;i++) if (parts[i]==want) { print $2; next }
}
'
}

# Hash all labels present in either base or head, compare per label.
classify_script() {
local rel="$1"
local base_sections head_sections
base_sections=$(git show "origin/master:$rel" 2>/dev/null | extract_sections || true)
head_sections=$(extract_sections < "$rel")

local labels lbl base_hash head_hash
labels=$(printf '%s\n%s\n' "$base_sections" "$head_sections" \
| awk -F'\t' 'NF { print $1 }' \
| tr ',' '\n' \
| sort -u)

for lbl in $labels; do
base_hash=$(printf '%s\n' "$base_sections" | sections_for "$lbl" | sha1sum | awk '{print $1}')
head_hash=$(printf '%s\n' "$head_sections" | sections_for "$lbl" | sha1sum | awk '{print $1}')
if [[ "$base_hash" != "$head_hash" ]]; then
mark_os_label "$lbl"
fi
done
}

# --- Walk changed files ---
files=$(git diff --name-only origin/master...HEAD)

while IFS= read -r path; do
[[ -z "$path" ]] && continue
case "$path" in
justfile|init.sh|scripts/shell-init.sh|.github/workflows/recipe.yml)
mark_all ;;
.github/scripts/recipe/*|.github/actions/linux-container-setup/*)
mark_all ;;
flake.nix|flake.lock|nix/*|hosts/*|.github/actions/mac-nix-setup/*)
mark mac ;;
os-init/debian-init.sh)
mark debian-trixie; mark ubuntu ;;
os-init/ubuntu-init.sh)
mark ubuntu ;;
os-init/fedora-init.sh)
mark fedora-43; mark fedora-44 ;;
os-init/opensuse-tumbleweed-init.sh)
mark opensuse-tumbleweed ;;
os-init/cachyos-init.sh)
mark cachyos ;;
"scripts/install-${RECIPE}.sh")
if [[ -f "$path" ]]; then
classify_script "$path"
else
mark_all
fi
;;
*)
: ;;
esac
done <<< "$files"

# flatpak has no mac path (mac case is a no-op message).
if [[ "$RECIPE" == "flatpak" ]]; then
unset 'affected[mac]'
fi

# Emit JSON.
out="["
sep=""
for t in "${ALL_TARGETS[@]}"; do
if [[ -n "${affected[$t]:-}" ]]; then
out+="${sep}${TARGET_JSON[$t]}"
sep=","
fi
done
out+="]"
echo "$out"
82 changes: 40 additions & 42 deletions .github/workflows/recipe.yml
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ jobs:
runs-on: ubuntu-24.04
outputs:
recipe: ${{ steps.parse.outputs.recipe }}
targets: ${{ steps.classify.outputs.targets }}
steps:
- id: parse
env:
Expand All @@ -30,69 +31,66 @@ jobs:
echo "recipe=$recipe" >> "$GITHUB_OUTPUT"
echo "Recipe: $recipe"

ubuntu:
needs: setup
runs-on: ubuntu-24.04
steps:
- uses: actions/checkout@v6
- name: bootstrap + just ${{ needs.setup.outputs.recipe }}
with:
fetch-depth: 0

- name: fetch origin/master
run: git fetch --no-tags --depth=1 origin master

- id: classify
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
RECIPE: ${{ steps.parse.outputs.recipe }}
run: |
bash init.sh
just ${{ needs.setup.outputs.recipe }}
set -euo pipefail
targets=$(bash .github/scripts/recipe/classify-targets.sh)
echo "targets=$targets" >> "$GITHUB_OUTPUT"
echo "$targets" | python3 -m json.tool

linux-container:
target:
needs: setup
runs-on: ubuntu-24.04
if: needs.setup.outputs.targets != '[]'
strategy:
fail-fast: true
matrix:
include:
- name: debian-trixie
image: docker.io/library/debian:trixie
pkg: apt
- name: fedora-43
image: quay.io/fedora/fedora:43
pkg: dnf
- name: fedora-44
image: quay.io/fedora/fedora:44
pkg: dnf
- name: opensuse-tumbleweed
image: registry.opensuse.org/opensuse/tumbleweed:latest
pkg: zypper
- name: cachyos
image: docker.io/cachyos/cachyos:latest
pkg: pacman
include: ${{ fromJSON(needs.setup.outputs.targets) }}
runs-on: ${{ matrix.runs-on }}
container: ${{ matrix.container }}
name: ${{ matrix.name }}
container:
image: ${{ matrix.image }}
steps:
- uses: actions/checkout@v6
- uses: ./.github/actions/linux-container-setup

- if: matrix.kind == 'linux-container'
uses: ./.github/actions/linux-container-setup
with:
pkg: ${{ matrix.pkg }}
- name: bootstrap + just ${{ needs.setup.outputs.recipe }}

- if: matrix.kind == 'mac'
uses: ./.github/actions/mac-nix-setup
with:
github-token: ${{ secrets.GITHUB_TOKEN }}

- if: matrix.kind == 'linux-container'
name: bootstrap + just ${{ needs.setup.outputs.recipe }} (container)
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
chown -R ci:ci "$GITHUB_WORKSPACE"
su -l ci -c "export GITHUB_TOKEN='$GITHUB_TOKEN'; cd '$GITHUB_WORKSPACE' && bash init.sh && just ${{ needs.setup.outputs.recipe }}"

mac:
needs: setup
if: needs.setup.outputs.recipe != 'flatpak'
runs-on: macos-26
env:
HOSTNAME: ci
steps:
- uses: actions/checkout@v6
- uses: ./.github/actions/mac-nix-setup
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
- name: bootstrap + just ${{ needs.setup.outputs.recipe }}
- if: matrix.kind == 'ubuntu'
name: bootstrap + just ${{ needs.setup.outputs.recipe }} (ubuntu)
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
bash init.sh
just ${{ needs.setup.outputs.recipe }}

- if: matrix.kind == 'mac'
name: bootstrap + just ${{ needs.setup.outputs.recipe }} (mac)
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
HOSTNAME: ci
run: |
# shellcheck disable=SC1091
. /nix/var/nix/profiles/default/etc/profile.d/nix-daemon.sh
Expand Down