diff --git a/.github/workflows/anchor.yml b/.github/workflows/anchor.yml index 826e3a7ef..beac3421f 100644 --- a/.github/workflows/anchor.yml +++ b/.github/workflows/anchor.yml @@ -50,25 +50,52 @@ jobs: ignore_pattern=$(grep -v '^#' .github/.ghaignore | grep -v '^$' | tr '\n' '|' | sed 's/|$//') echo "Ignore pattern: $ignore_pattern" + # Single source of truth for "what is a framework project": a directory + # whose name is exactly "anchor". `find -type d -name anchor` gives us + # that by construction — no substring matching, no path-segment trickery, + # so siblings like "anchor-example/" or nested files such as + # "anchor-example/app/pages/api/foo.ts" can never enter the build list. function get_projects() { find . -type d -name "anchor" | grep -vE "$ignore_pattern" | sort } + # Filter the full project list down to projects touched by the given + # changed files. A file "touches" a project iff it lives inside that + # project directory (prefix match on "/"). This is an + # intersection against get_projects(), so the result is always a subset + # of the authoritative project list. + function filter_by_changes() { + local all_projects="$1" + shift + local changed_files=("$@") + echo "$all_projects" | while read -r project; do + [ -z "$project" ] && continue + # Strip leading ./ so prefix comparison matches git's output + local project_prefix="${project#./}/" + for file in "${changed_files[@]}"; do + if [[ "$file" == "$project_prefix"* ]]; then + echo "$project" + break + fi + done + done | sort -u + } + # Determine which projects to build and test if [[ "${{ github.event_name }}" == "schedule" || "${{ steps.changes.outputs.workflow }}" == "true" ]]; then # Workflow file changed or schedule — build everything projects=$(get_projects) elif [[ "${{ github.event_name }}" == "push" ]]; then # On push, only build projects with changes since parent commit - changed_files=$(git diff --name-only HEAD~1 HEAD 2>/dev/null || echo "") - if [ -z "$changed_files" ]; then + mapfile -t changed_files < <(git diff --name-only HEAD~1 HEAD 2>/dev/null || true) + if [ ${#changed_files[@]} -eq 0 ]; then projects=$(get_projects) else - projects=$(echo "$changed_files" | while read file; do dirname "$file" | grep anchor | sed 's#/anchor/.*#/anchor#g'; done | grep -vE "$ignore_pattern" | sort -u) + projects=$(filter_by_changes "$(get_projects)" "${changed_files[@]}") fi elif [[ "${{ steps.changes.outputs.anchor }}" == "true" ]]; then changed_files=(${{ steps.changes.outputs.anchor_files }}) - projects=$(for file in "${changed_files[@]}"; do dirname "${file}" | grep anchor | sed 's#/anchor/.*#/anchor#g'; done | grep -vE "$ignore_pattern" | sort -u) + projects=$(filter_by_changes "$(get_projects)" "${changed_files[@]}") else projects="" fi diff --git a/.github/workflows/native.yml b/.github/workflows/native.yml index b144d382c..ab4334cba 100644 --- a/.github/workflows/native.yml +++ b/.github/workflows/native.yml @@ -50,25 +50,50 @@ jobs: ignore_pattern=$(grep -v '^#' .github/.ghaignore | grep -v '^$' | tr '\n' '|' | sed 's/|$//') echo "Ignore pattern: $ignore_pattern" + # Single source of truth for "what is a framework project": a directory + # whose name is exactly "native". `find -type d -name native` gives us + # that by construction — no substring matching, no path-segment trickery, + # so siblings like "alternative/" can never enter the build list. function get_projects() { find . -type d -name "native" | grep -vE "$ignore_pattern" | sort } + # Filter the full project list down to projects touched by the given + # changed files. A file "touches" a project iff it lives inside that + # project directory (prefix match on "/"). This is an + # intersection against get_projects(), so the result is always a subset + # of the authoritative project list. + function filter_by_changes() { + local all_projects="$1" + shift + local changed_files=("$@") + echo "$all_projects" | while read -r project; do + [ -z "$project" ] && continue + local project_prefix="${project#./}/" + for file in "${changed_files[@]}"; do + if [[ "$file" == "$project_prefix"* ]]; then + echo "$project" + break + fi + done + done | sort -u + } + # Determine which projects to build and test if [[ "${{ github.event_name }}" == "schedule" || "${{ steps.changes.outputs.workflow }}" == "true" ]]; then # Workflow file changed or schedule — build everything projects=$(get_projects) elif [[ "${{ github.event_name }}" == "push" ]]; then # On push, only build projects with changes since parent commit - changed_files=$(git diff --name-only HEAD~1 HEAD 2>/dev/null || echo "") - if [ -z "$changed_files" ]; then + mapfile -t changed_files < <(git diff --name-only HEAD~1 HEAD 2>/dev/null || true) + if [ ${#changed_files[@]} -eq 0 ]; then projects=$(get_projects) else - projects=$(echo "$changed_files" | while read file; do dirname "$file" | grep native | sed 's#/native/.*#/native#g'; done | grep -vE "$ignore_pattern" | sort -u) + projects=$(filter_by_changes "$(get_projects)" "${changed_files[@]}") fi elif [[ "${{ steps.changes.outputs.native }}" == "true" ]]; then changed_files=(${{ steps.changes.outputs.native_files }}) - projects=$(for file in "${changed_files[@]}"; do dirname "${file}" | grep native | sed 's#/native/.*#/native#g'; done | grep -vE "$ignore_pattern" | sort -u) + projects=$(filter_by_changes "$(get_projects)" "${changed_files[@]}") else projects="" fi diff --git a/.github/workflows/pinocchio.yml b/.github/workflows/pinocchio.yml index bb85d0876..ccb793841 100644 --- a/.github/workflows/pinocchio.yml +++ b/.github/workflows/pinocchio.yml @@ -50,25 +50,51 @@ jobs: ignore_pattern=$(grep -v '^#' .github/.ghaignore | grep -v '^$' | tr '\n' '|' | sed 's/|$//') echo "Ignore pattern: $ignore_pattern" + # Single source of truth for "what is a framework project": a directory + # whose name is exactly "pinocchio". `find -type d -name pinocchio` gives + # us that by construction — no substring matching, no path-segment + # trickery, so siblings like "pinocchio-example/" can never enter the + # build list. function get_projects() { find . -type d -name "pinocchio" | grep -vE "$ignore_pattern" | sort } + # Filter the full project list down to projects touched by the given + # changed files. A file "touches" a project iff it lives inside that + # project directory (prefix match on "/"). This is an + # intersection against get_projects(), so the result is always a subset + # of the authoritative project list. + function filter_by_changes() { + local all_projects="$1" + shift + local changed_files=("$@") + echo "$all_projects" | while read -r project; do + [ -z "$project" ] && continue + local project_prefix="${project#./}/" + for file in "${changed_files[@]}"; do + if [[ "$file" == "$project_prefix"* ]]; then + echo "$project" + break + fi + done + done | sort -u + } + # Determine which projects to build and test if [[ "${{ github.event_name }}" == "schedule" || "${{ steps.changes.outputs.workflow }}" == "true" ]]; then # Workflow file changed or schedule — build everything projects=$(get_projects) elif [[ "${{ github.event_name }}" == "push" ]]; then # On push, only build projects with changes since parent commit - changed_files=$(git diff --name-only HEAD~1 HEAD 2>/dev/null || echo "") - if [ -z "$changed_files" ]; then + mapfile -t changed_files < <(git diff --name-only HEAD~1 HEAD 2>/dev/null || true) + if [ ${#changed_files[@]} -eq 0 ]; then projects=$(get_projects) else - projects=$(echo "$changed_files" | while read file; do dirname "$file" | grep pinocchio | sed 's#/pinocchio/.*#/pinocchio#g'; done | grep -vE "$ignore_pattern" | sort -u) + projects=$(filter_by_changes "$(get_projects)" "${changed_files[@]}") fi elif [[ "${{ steps.changes.outputs.pinocchio }}" == "true" ]]; then changed_files=(${{ steps.changes.outputs.pinocchio_files }}) - projects=$(for file in "${changed_files[@]}"; do dirname "${file}" | grep pinocchio | sed 's#/pinocchio/.*#/pinocchio#g'; done | grep -vE "$ignore_pattern" | sort -u) + projects=$(filter_by_changes "$(get_projects)" "${changed_files[@]}") else projects="" fi diff --git a/.github/workflows/quasar.yml b/.github/workflows/quasar.yml index 916d7d439..dbfc48b1e 100644 --- a/.github/workflows/quasar.yml +++ b/.github/workflows/quasar.yml @@ -52,25 +52,50 @@ jobs: ignore_pattern=$(grep -v '^#' .github/.ghaignore | grep -v '^$' | tr '\n' '|' | sed 's/|$//') echo "Ignore pattern: $ignore_pattern" + # Single source of truth for "what is a framework project": a directory + # whose name is exactly "quasar". `find -type d -name quasar` gives us + # that by construction — no substring matching, no path-segment trickery, + # so siblings like "quasar-example/" can never enter the build list. function get_projects() { find . -type d -name "quasar" | grep -vE "$ignore_pattern" | sort } + # Filter the full project list down to projects touched by the given + # changed files. A file "touches" a project iff it lives inside that + # project directory (prefix match on "/"). This is an + # intersection against get_projects(), so the result is always a subset + # of the authoritative project list. + function filter_by_changes() { + local all_projects="$1" + shift + local changed_files=("$@") + echo "$all_projects" | while read -r project; do + [ -z "$project" ] && continue + local project_prefix="${project#./}/" + for file in "${changed_files[@]}"; do + if [[ "$file" == "$project_prefix"* ]]; then + echo "$project" + break + fi + done + done | sort -u + } + # Determine which projects to build and test if [[ "${{ github.event_name }}" == "schedule" || "${{ steps.changes.outputs.workflow }}" == "true" ]]; then # Workflow file changed or schedule — build everything projects=$(get_projects) elif [[ "${{ github.event_name }}" == "push" ]]; then # On push, only build projects with changes since parent commit - changed_files=$(git diff --name-only HEAD~1 HEAD 2>/dev/null || echo "") - if [ -z "$changed_files" ]; then + mapfile -t changed_files < <(git diff --name-only HEAD~1 HEAD 2>/dev/null || true) + if [ ${#changed_files[@]} -eq 0 ]; then projects=$(get_projects) else - projects=$(echo "$changed_files" | while read file; do dirname "$file" | grep quasar | sed 's#/quasar/.*#/quasar#g'; done | grep -vE "$ignore_pattern" | sort -u) + projects=$(filter_by_changes "$(get_projects)" "${changed_files[@]}") fi elif [[ "${{ steps.changes.outputs.quasar }}" == "true" ]]; then changed_files=(${{ steps.changes.outputs.quasar_files }}) - projects=$(for file in "${changed_files[@]}"; do dirname "${file}" | grep quasar | sed 's#/quasar/.*#/quasar#g'; done | grep -vE "$ignore_pattern" | sort -u) + projects=$(filter_by_changes "$(get_projects)" "${changed_files[@]}") else projects="" fi diff --git a/.github/workflows/solana-asm.yml b/.github/workflows/solana-asm.yml index 408344466..d314032c8 100644 --- a/.github/workflows/solana-asm.yml +++ b/.github/workflows/solana-asm.yml @@ -44,16 +44,42 @@ jobs: ignore_pattern=$(grep -v '^#' .github/.ghaignore | grep -v '^$' | tr '\n' '|' | sed 's/|$//') echo "Ignore pattern: $ignore_pattern" + # Single source of truth for "what is a framework project": a directory + # whose name is exactly "asm". `find -type d -name asm` gives us that by + # construction — no substring matching, no path-segment trickery, so + # anything containing the substring "asm" (e.g. "wasm/", "plasma/") can + # never enter the build list. function get_projects() { find . -type d -name "asm" | grep -vE "$ignore_pattern" | sort } + # Filter the full project list down to projects touched by the given + # changed files. A file "touches" a project iff it lives inside that + # project directory (prefix match on "/"). This is an + # intersection against get_projects(), so the result is always a subset + # of the authoritative project list. + function filter_by_changes() { + local all_projects="$1" + shift + local changed_files=("$@") + echo "$all_projects" | while read -r project; do + [ -z "$project" ] && continue + local project_prefix="${project#./}/" + for file in "${changed_files[@]}"; do + if [[ "$file" == "$project_prefix"* ]]; then + echo "$project" + break + fi + done + done | sort -u + } + # Determine which projects to build and test if [[ "${{ github.event_name }}" == "push" || "${{ github.event_name }}" == "schedule" || "${{ steps.changes.outputs.workflow }}" == "true" ]]; then projects=$(get_projects) elif [[ "${{ steps.changes.outputs.asm }}" == "true" ]]; then changed_files=(${{ steps.changes.outputs.asm_files }}) - projects=$(for file in "${changed_files[@]}"; do dirname "${file}" | grep asm | sed 's#/asm/.*#/asm#g'; done | grep -vE "$ignore_pattern" | sort -u) + projects=$(filter_by_changes "$(get_projects)" "${changed_files[@]}") else projects="" fi