Skip to content

Commit 730afad

Browse files
committed
fix(ci): use find as single source of truth for framework project detection
Each framework workflow (anchor, native, pinocchio, quasar, solana-asm) previously had two competing answers to the question "which projects are build targets": 1. workflow_dispatch / schedule: `find . -type d -name <framework>` — exact directory-name match. Correct. 2. pull_request / push: a substring pipeline `dirname "$file" | grep <framework> | sed 's#/<framework>/.*#/<framework>#g'` — substring match. Wrong. The substring path let ghost paths leak into the build list. For example, a change to `tokens/nft-meta-data-pointer/anchor-example/app/pages/api/...` was matched by `grep anchor` (substring of "anchor-example") and was not collapsed by `sed 's#/anchor/.*#/anchor#g'` because the path contains no literal `/anchor/` segment. The path then survived the trailing `awk '$NF == "anchor"'` filter only because the previous commit added it as a post-hoc patch — but the underlying design still allowed paths that were never returned by `find -name anchor` to enter the candidate list. This commit removes the substring approach entirely. Every workflow now uses a single source of truth — `get_projects()`, which calls `find . -type d -name <framework>` — and a new `filter_by_changes()` helper that intersects that authoritative list with the changed files via a prefix match (`<project>/`). A path cannot enter the build list unless `find` already returned it, so ghost siblings like `anchor-example/`, `wasm/`, `plasma/`, `pinocchio-example/` are impossible by construction rather than filtered out after the fact. Behaviour for `workflow_dispatch`, `schedule`, and workflow-file-changed events is unchanged (those paths already called `get_projects()` directly). Empty `changed_files` on push still falls through to all projects.
1 parent 8abb5d1 commit 730afad

5 files changed

Lines changed: 146 additions & 17 deletions

File tree

.github/workflows/anchor.yml

Lines changed: 31 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -50,25 +50,52 @@ jobs:
5050
ignore_pattern=$(grep -v '^#' .github/.ghaignore | grep -v '^$' | tr '\n' '|' | sed 's/|$//')
5151
echo "Ignore pattern: $ignore_pattern"
5252
53+
# Single source of truth for "what is a framework project": a directory
54+
# whose name is exactly "anchor". `find -type d -name anchor` gives us
55+
# that by construction — no substring matching, no path-segment trickery,
56+
# so siblings like "anchor-example/" or nested files such as
57+
# "anchor-example/app/pages/api/foo.ts" can never enter the build list.
5358
function get_projects() {
5459
find . -type d -name "anchor" | grep -vE "$ignore_pattern" | sort
5560
}
5661
62+
# Filter the full project list down to projects touched by the given
63+
# changed files. A file "touches" a project iff it lives inside that
64+
# project directory (prefix match on "<project>/"). This is an
65+
# intersection against get_projects(), so the result is always a subset
66+
# of the authoritative project list.
67+
function filter_by_changes() {
68+
local all_projects="$1"
69+
shift
70+
local changed_files=("$@")
71+
echo "$all_projects" | while read -r project; do
72+
[ -z "$project" ] && continue
73+
# Strip leading ./ so prefix comparison matches git's output
74+
local project_prefix="${project#./}/"
75+
for file in "${changed_files[@]}"; do
76+
if [[ "$file" == "$project_prefix"* ]]; then
77+
echo "$project"
78+
break
79+
fi
80+
done
81+
done | sort -u
82+
}
83+
5784
# Determine which projects to build and test
5885
if [[ "${{ github.event_name }}" == "schedule" || "${{ steps.changes.outputs.workflow }}" == "true" ]]; then
5986
# Workflow file changed or schedule — build everything
6087
projects=$(get_projects)
6188
elif [[ "${{ github.event_name }}" == "push" ]]; then
6289
# On push, only build projects with changes since parent commit
63-
changed_files=$(git diff --name-only HEAD~1 HEAD 2>/dev/null || echo "")
64-
if [ -z "$changed_files" ]; then
90+
mapfile -t changed_files < <(git diff --name-only HEAD~1 HEAD 2>/dev/null || true)
91+
if [ ${#changed_files[@]} -eq 0 ]; then
6592
projects=$(get_projects)
6693
else
67-
projects=$(echo "$changed_files" | while read file; do dirname "$file" | grep anchor | sed 's#/anchor/.*#/anchor#g'; done | grep -vE "$ignore_pattern" | sort -u)
94+
projects=$(filter_by_changes "$(get_projects)" "${changed_files[@]}")
6895
fi
6996
elif [[ "${{ steps.changes.outputs.anchor }}" == "true" ]]; then
7097
changed_files=(${{ steps.changes.outputs.anchor_files }})
71-
projects=$(for file in "${changed_files[@]}"; do dirname "${file}" | grep anchor | sed 's#/anchor/.*#/anchor#g'; done | grep -vE "$ignore_pattern" | sort -u)
98+
projects=$(filter_by_changes "$(get_projects)" "${changed_files[@]}")
7299
else
73100
projects=""
74101
fi

.github/workflows/native.yml

Lines changed: 29 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -50,25 +50,50 @@ jobs:
5050
ignore_pattern=$(grep -v '^#' .github/.ghaignore | grep -v '^$' | tr '\n' '|' | sed 's/|$//')
5151
echo "Ignore pattern: $ignore_pattern"
5252
53+
# Single source of truth for "what is a framework project": a directory
54+
# whose name is exactly "native". `find -type d -name native` gives us
55+
# that by construction — no substring matching, no path-segment trickery,
56+
# so siblings like "alternative/" can never enter the build list.
5357
function get_projects() {
5458
find . -type d -name "native" | grep -vE "$ignore_pattern" | sort
5559
}
5660
61+
# Filter the full project list down to projects touched by the given
62+
# changed files. A file "touches" a project iff it lives inside that
63+
# project directory (prefix match on "<project>/"). This is an
64+
# intersection against get_projects(), so the result is always a subset
65+
# of the authoritative project list.
66+
function filter_by_changes() {
67+
local all_projects="$1"
68+
shift
69+
local changed_files=("$@")
70+
echo "$all_projects" | while read -r project; do
71+
[ -z "$project" ] && continue
72+
local project_prefix="${project#./}/"
73+
for file in "${changed_files[@]}"; do
74+
if [[ "$file" == "$project_prefix"* ]]; then
75+
echo "$project"
76+
break
77+
fi
78+
done
79+
done | sort -u
80+
}
81+
5782
# Determine which projects to build and test
5883
if [[ "${{ github.event_name }}" == "schedule" || "${{ steps.changes.outputs.workflow }}" == "true" ]]; then
5984
# Workflow file changed or schedule — build everything
6085
projects=$(get_projects)
6186
elif [[ "${{ github.event_name }}" == "push" ]]; then
6287
# On push, only build projects with changes since parent commit
63-
changed_files=$(git diff --name-only HEAD~1 HEAD 2>/dev/null || echo "")
64-
if [ -z "$changed_files" ]; then
88+
mapfile -t changed_files < <(git diff --name-only HEAD~1 HEAD 2>/dev/null || true)
89+
if [ ${#changed_files[@]} -eq 0 ]; then
6590
projects=$(get_projects)
6691
else
67-
projects=$(echo "$changed_files" | while read file; do dirname "$file" | grep native | sed 's#/native/.*#/native#g'; done | grep -vE "$ignore_pattern" | sort -u)
92+
projects=$(filter_by_changes "$(get_projects)" "${changed_files[@]}")
6893
fi
6994
elif [[ "${{ steps.changes.outputs.native }}" == "true" ]]; then
7095
changed_files=(${{ steps.changes.outputs.native_files }})
71-
projects=$(for file in "${changed_files[@]}"; do dirname "${file}" | grep native | sed 's#/native/.*#/native#g'; done | grep -vE "$ignore_pattern" | sort -u)
96+
projects=$(filter_by_changes "$(get_projects)" "${changed_files[@]}")
7297
else
7398
projects=""
7499
fi

.github/workflows/pinocchio.yml

Lines changed: 30 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -50,25 +50,51 @@ jobs:
5050
ignore_pattern=$(grep -v '^#' .github/.ghaignore | grep -v '^$' | tr '\n' '|' | sed 's/|$//')
5151
echo "Ignore pattern: $ignore_pattern"
5252
53+
# Single source of truth for "what is a framework project": a directory
54+
# whose name is exactly "pinocchio". `find -type d -name pinocchio` gives
55+
# us that by construction — no substring matching, no path-segment
56+
# trickery, so siblings like "pinocchio-example/" can never enter the
57+
# build list.
5358
function get_projects() {
5459
find . -type d -name "pinocchio" | grep -vE "$ignore_pattern" | sort
5560
}
5661
62+
# Filter the full project list down to projects touched by the given
63+
# changed files. A file "touches" a project iff it lives inside that
64+
# project directory (prefix match on "<project>/"). This is an
65+
# intersection against get_projects(), so the result is always a subset
66+
# of the authoritative project list.
67+
function filter_by_changes() {
68+
local all_projects="$1"
69+
shift
70+
local changed_files=("$@")
71+
echo "$all_projects" | while read -r project; do
72+
[ -z "$project" ] && continue
73+
local project_prefix="${project#./}/"
74+
for file in "${changed_files[@]}"; do
75+
if [[ "$file" == "$project_prefix"* ]]; then
76+
echo "$project"
77+
break
78+
fi
79+
done
80+
done | sort -u
81+
}
82+
5783
# Determine which projects to build and test
5884
if [[ "${{ github.event_name }}" == "schedule" || "${{ steps.changes.outputs.workflow }}" == "true" ]]; then
5985
# Workflow file changed or schedule — build everything
6086
projects=$(get_projects)
6187
elif [[ "${{ github.event_name }}" == "push" ]]; then
6288
# On push, only build projects with changes since parent commit
63-
changed_files=$(git diff --name-only HEAD~1 HEAD 2>/dev/null || echo "")
64-
if [ -z "$changed_files" ]; then
89+
mapfile -t changed_files < <(git diff --name-only HEAD~1 HEAD 2>/dev/null || true)
90+
if [ ${#changed_files[@]} -eq 0 ]; then
6591
projects=$(get_projects)
6692
else
67-
projects=$(echo "$changed_files" | while read file; do dirname "$file" | grep pinocchio | sed 's#/pinocchio/.*#/pinocchio#g'; done | grep -vE "$ignore_pattern" | sort -u)
93+
projects=$(filter_by_changes "$(get_projects)" "${changed_files[@]}")
6894
fi
6995
elif [[ "${{ steps.changes.outputs.pinocchio }}" == "true" ]]; then
7096
changed_files=(${{ steps.changes.outputs.pinocchio_files }})
71-
projects=$(for file in "${changed_files[@]}"; do dirname "${file}" | grep pinocchio | sed 's#/pinocchio/.*#/pinocchio#g'; done | grep -vE "$ignore_pattern" | sort -u)
97+
projects=$(filter_by_changes "$(get_projects)" "${changed_files[@]}")
7298
else
7399
projects=""
74100
fi

.github/workflows/quasar.yml

Lines changed: 29 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -52,25 +52,50 @@ jobs:
5252
ignore_pattern=$(grep -v '^#' .github/.ghaignore | grep -v '^$' | tr '\n' '|' | sed 's/|$//')
5353
echo "Ignore pattern: $ignore_pattern"
5454
55+
# Single source of truth for "what is a framework project": a directory
56+
# whose name is exactly "quasar". `find -type d -name quasar` gives us
57+
# that by construction — no substring matching, no path-segment trickery,
58+
# so siblings like "quasar-example/" can never enter the build list.
5559
function get_projects() {
5660
find . -type d -name "quasar" | grep -vE "$ignore_pattern" | sort
5761
}
5862
63+
# Filter the full project list down to projects touched by the given
64+
# changed files. A file "touches" a project iff it lives inside that
65+
# project directory (prefix match on "<project>/"). This is an
66+
# intersection against get_projects(), so the result is always a subset
67+
# of the authoritative project list.
68+
function filter_by_changes() {
69+
local all_projects="$1"
70+
shift
71+
local changed_files=("$@")
72+
echo "$all_projects" | while read -r project; do
73+
[ -z "$project" ] && continue
74+
local project_prefix="${project#./}/"
75+
for file in "${changed_files[@]}"; do
76+
if [[ "$file" == "$project_prefix"* ]]; then
77+
echo "$project"
78+
break
79+
fi
80+
done
81+
done | sort -u
82+
}
83+
5984
# Determine which projects to build and test
6085
if [[ "${{ github.event_name }}" == "schedule" || "${{ steps.changes.outputs.workflow }}" == "true" ]]; then
6186
# Workflow file changed or schedule — build everything
6287
projects=$(get_projects)
6388
elif [[ "${{ github.event_name }}" == "push" ]]; then
6489
# On push, only build projects with changes since parent commit
65-
changed_files=$(git diff --name-only HEAD~1 HEAD 2>/dev/null || echo "")
66-
if [ -z "$changed_files" ]; then
90+
mapfile -t changed_files < <(git diff --name-only HEAD~1 HEAD 2>/dev/null || true)
91+
if [ ${#changed_files[@]} -eq 0 ]; then
6792
projects=$(get_projects)
6893
else
69-
projects=$(echo "$changed_files" | while read file; do dirname "$file" | grep quasar | sed 's#/quasar/.*#/quasar#g'; done | grep -vE "$ignore_pattern" | sort -u)
94+
projects=$(filter_by_changes "$(get_projects)" "${changed_files[@]}")
7095
fi
7196
elif [[ "${{ steps.changes.outputs.quasar }}" == "true" ]]; then
7297
changed_files=(${{ steps.changes.outputs.quasar_files }})
73-
projects=$(for file in "${changed_files[@]}"; do dirname "${file}" | grep quasar | sed 's#/quasar/.*#/quasar#g'; done | grep -vE "$ignore_pattern" | sort -u)
98+
projects=$(filter_by_changes "$(get_projects)" "${changed_files[@]}")
7499
else
75100
projects=""
76101
fi

.github/workflows/solana-asm.yml

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,16 +44,42 @@ jobs:
4444
ignore_pattern=$(grep -v '^#' .github/.ghaignore | grep -v '^$' | tr '\n' '|' | sed 's/|$//')
4545
echo "Ignore pattern: $ignore_pattern"
4646
47+
# Single source of truth for "what is a framework project": a directory
48+
# whose name is exactly "asm". `find -type d -name asm` gives us that by
49+
# construction — no substring matching, no path-segment trickery, so
50+
# anything containing the substring "asm" (e.g. "wasm/", "plasma/") can
51+
# never enter the build list.
4752
function get_projects() {
4853
find . -type d -name "asm" | grep -vE "$ignore_pattern" | sort
4954
}
5055
56+
# Filter the full project list down to projects touched by the given
57+
# changed files. A file "touches" a project iff it lives inside that
58+
# project directory (prefix match on "<project>/"). This is an
59+
# intersection against get_projects(), so the result is always a subset
60+
# of the authoritative project list.
61+
function filter_by_changes() {
62+
local all_projects="$1"
63+
shift
64+
local changed_files=("$@")
65+
echo "$all_projects" | while read -r project; do
66+
[ -z "$project" ] && continue
67+
local project_prefix="${project#./}/"
68+
for file in "${changed_files[@]}"; do
69+
if [[ "$file" == "$project_prefix"* ]]; then
70+
echo "$project"
71+
break
72+
fi
73+
done
74+
done | sort -u
75+
}
76+
5177
# Determine which projects to build and test
5278
if [[ "${{ github.event_name }}" == "push" || "${{ github.event_name }}" == "schedule" || "${{ steps.changes.outputs.workflow }}" == "true" ]]; then
5379
projects=$(get_projects)
5480
elif [[ "${{ steps.changes.outputs.asm }}" == "true" ]]; then
5581
changed_files=(${{ steps.changes.outputs.asm_files }})
56-
projects=$(for file in "${changed_files[@]}"; do dirname "${file}" | grep asm | sed 's#/asm/.*#/asm#g'; done | grep -vE "$ignore_pattern" | sort -u)
82+
projects=$(filter_by_changes "$(get_projects)" "${changed_files[@]}")
5783
else
5884
projects=""
5985
fi

0 commit comments

Comments
 (0)