Skip to content

Commit a0bcf39

Browse files
committed
ci: replace dorny with custom git-diff step (push-incremental for pull_request)
1 parent 32be6ef commit a0bcf39

5 files changed

Lines changed: 217 additions & 126 deletions

File tree

.github/workflows/build-apps.yml

Lines changed: 44 additions & 103 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
1-
# Per-app build matrix gated by `dorny/paths-filter`. Adding a new model
2-
# directory, controller, or top-level module under packages/react-native-executorch/
3-
# probably means updating the filter block below — `core-shared` if it affects every
1+
# Per-app build matrix. Filter definitions live in scripts/build-app-filters.yml
2+
# and are evaluated by scripts/detect-changed-filters.ts against the diff of the
3+
# current push (not the cumulative PR diff). Adding a new model directory,
4+
# controller, or top-level module under packages/react-native-executorch/
5+
# probably means updating that filter file — `core-shared` if it affects every
46
# app, or one of the per-app `<app>-pkg` anchors otherwise. CI runs
57
# `scripts/check-ci-filter-coverage.ts` (in ci.yml's lint job) to flag uncovered
68
# files so this stays accurate.
@@ -24,110 +26,49 @@ jobs:
2426
steps:
2527
- name: Checkout
2628
uses: actions/checkout@v6
29+
with:
30+
# Enough history for `git diff $before..$head` against the previous
31+
# push. PR series rarely exceed this; if they do, the script falls
32+
# back to fetching the missing SHA on demand.
33+
fetch-depth: 100
34+
35+
- name: Setup
36+
if: github.event_name != 'workflow_dispatch'
37+
uses: ./.github/actions/setup
2738

2839
- name: Detect changed apps
2940
id: filter
3041
if: github.event_name != 'workflow_dispatch'
31-
uses: dorny/paths-filter@v3
32-
with:
33-
# Diff against the previous commit (synchronize/push) or the PR base
34-
# (opened/reopened/ready_for_review). Without this, dorny defaults to
35-
# the PR base on every push, so each commit re-evaluates the
36-
# cumulative PR diff and pulls every app whose filter ever matched
37-
# into the matrix — even when the latest push only touched one app.
38-
base: ${{ github.event.before || github.event.pull_request.base.sha }}
39-
filters: |
40-
# Cross-platform shared infrastructure
41-
core-shared: &core-shared
42-
- .github/workflows/build-apps.yml
43-
- packages/react-native-executorch/{scripts,third-party}/**
44-
- packages/react-native-executorch/{package.json,tsconfig.json}
45-
- packages/react-native-executorch/common/{ada,runner}/**
46-
- packages/react-native-executorch/common/rnexecutorch/{Error,ErrorCodes,Log}.h
47-
- packages/react-native-executorch/common/rnexecutorch/RnExecutorchInstaller.{cpp,h}
48-
- packages/react-native-executorch/common/rnexecutorch/{data_processing,host_objects,jsi,metaprogramming,tests,threads,utils}/**
49-
- packages/react-native-executorch/common/rnexecutorch/models/BaseModel.{cpp,h}
50-
- packages/react-native-executorch/src/{common,constants,errors,native,types,utils}/**
51-
- packages/react-native-executorch/src/index.ts
52-
- packages/react-native-executorch/src/modules/{BaseModule.ts,general/**}
53-
- packages/react-native-executorch/src/hooks/{general/**,useModule.ts,useModuleFactory.ts}
54-
- "{package.json,yarn.lock}"
55-
# Platform-specific shared
56-
android-shared: &android-shared
57-
- .github/actions/build-android-app/**
58-
- packages/react-native-executorch/android/**
59-
ios-shared: &ios-shared
60-
- .github/actions/build-ios-app/**
61-
- packages/react-native-executorch/ios/**
62-
- packages/react-native-executorch/react-native-executorch.podspec
63-
# Resource fetchers
64-
expo-fetcher: &expo-fetcher
65-
- packages/expo-resource-fetcher/**
66-
bare-fetcher: &bare-fetcher
67-
- packages/bare-resource-fetcher/**
68-
# Per-app package paths (TS modules + C++ models for the app's domain)
69-
llm-pkg: &llm-pkg
70-
- packages/react-native-executorch/common/rnexecutorch/TokenizerModule.{cpp,h}
71-
- packages/react-native-executorch/common/rnexecutorch/models/llm/**
72-
- packages/react-native-executorch/common/rnexecutorch/models/privacy_filter/**
73-
- packages/react-native-executorch/src/modules/natural_language_processing/{LLMModule,PrivacyFilterModule,TokenizerModule}.ts
74-
- packages/react-native-executorch/src/hooks/natural_language_processing/{useLLM,usePrivacyFilter,useTokenizer}.ts
75-
- packages/react-native-executorch/src/controllers/LLMController.ts
76-
cv-pkg: &cv-pkg
77-
- packages/react-native-executorch/common/rnexecutorch/models/VisionModel.{cpp,h}
78-
- packages/react-native-executorch/common/rnexecutorch/models/{classification,instance_segmentation,object_detection,ocr,pose_estimation,semantic_segmentation,style_transfer,text_to_image,vertical_ocr}/**
79-
- packages/react-native-executorch/src/{modules,hooks}/computer_vision/**
80-
- packages/react-native-executorch/src/controllers/{BaseOCRController,OCRController,VerticalOCRController}.ts
81-
speech-pkg: &speech-pkg
82-
- packages/react-native-executorch/common/pfft/**
83-
- packages/react-native-executorch/common/rnexecutorch/TokenizerModule.{cpp,h}
84-
- packages/react-native-executorch/common/rnexecutorch/models/{speech_to_text,text_to_speech,voice_activity_detection}/**
85-
- packages/react-native-executorch/src/modules/natural_language_processing/{SpeechToTextModule,TextToSpeechModule,VADModule,TokenizerModule}.ts
86-
- packages/react-native-executorch/src/hooks/natural_language_processing/{useSpeechToText,useTextToSpeech,useVAD,useTokenizer}.ts
87-
text-embeddings-pkg: &text-embeddings-pkg
88-
- packages/react-native-executorch/common/rnexecutorch/TokenizerModule.{cpp,h}
89-
- packages/react-native-executorch/common/rnexecutorch/models/embeddings/**
90-
- packages/react-native-executorch/src/modules/natural_language_processing/{TextEmbeddingsModule,TokenizerModule}.ts
91-
- packages/react-native-executorch/src/modules/computer_vision/ImageEmbeddingsModule.ts
92-
- packages/react-native-executorch/src/hooks/natural_language_processing/{useTextEmbeddings,useTokenizer}.ts
93-
- packages/react-native-executorch/src/hooks/computer_vision/useImageEmbeddings.ts
94-
# Per-app bundle: core + fetcher + pkg + app dir
95-
llm-app: &llm-app
96-
- *core-shared
97-
- *expo-fetcher
98-
- *llm-pkg
99-
- apps/llm/**
100-
computer-vision-app: &computer-vision-app
101-
- *core-shared
102-
- *expo-fetcher
103-
- *cv-pkg
104-
- apps/computer-vision/**
105-
speech-app: &speech-app
106-
- *core-shared
107-
- *expo-fetcher
108-
- *speech-pkg
109-
- apps/speech/**
110-
text-embeddings-app: &text-embeddings-app
111-
- *core-shared
112-
- *expo-fetcher
113-
- *text-embeddings-pkg
114-
- apps/text-embeddings/**
115-
bare-rn-app: &bare-rn-app
116-
- *core-shared
117-
- *bare-fetcher
118-
- *llm-pkg
119-
- apps/bare-rn/**
120-
# Final per-platform per-app filters (the only ones the matrix consumes)
121-
llm-android: [*llm-app, *android-shared]
122-
llm-ios: [*llm-app, *ios-shared]
123-
computer-vision-android: [*computer-vision-app, *android-shared]
124-
computer-vision-ios: [*computer-vision-app, *ios-shared]
125-
speech-android: [*speech-app, *android-shared]
126-
speech-ios: [*speech-app, *ios-shared]
127-
text-embeddings-android: [*text-embeddings-app, *android-shared]
128-
text-embeddings-ios: [*text-embeddings-app, *ios-shared]
129-
bare-rn-android: [*bare-rn-app, *android-shared]
130-
bare-rn-ios: [*bare-rn-app, *ios-shared]
42+
run: |
43+
set -e
44+
ZERO=0000000000000000000000000000000000000000
45+
if [ "${{ github.event_name }}" = "pull_request" ]; then
46+
BEFORE="${{ github.event.before }}"
47+
HEAD_SHA="${{ github.event.pull_request.head.sha }}"
48+
if [ -z "$BEFORE" ] || [ "$BEFORE" = "$ZERO" ]; then
49+
BASE_SHA="${{ github.event.pull_request.base.sha }}"
50+
else
51+
BASE_SHA="$BEFORE"
52+
fi
53+
else
54+
BEFORE="${{ github.event.before }}"
55+
HEAD_SHA="${{ github.sha }}"
56+
if [ -z "$BEFORE" ] || [ "$BEFORE" = "$ZERO" ]; then
57+
BASE_SHA="$(git rev-parse origin/main^ 2>/dev/null || git rev-parse HEAD^)"
58+
else
59+
BASE_SHA="$BEFORE"
60+
fi
61+
fi
62+
echo "Diffing $BASE_SHA..$HEAD_SHA"
63+
# Ensure both SHAs are reachable; fetch deeper if shallow clone is missing one.
64+
if ! git cat-file -e "$BASE_SHA" 2>/dev/null || ! git cat-file -e "$HEAD_SHA" 2>/dev/null; then
65+
git fetch --no-tags --depth=500 origin "$BASE_SHA" "$HEAD_SHA" 2>/dev/null \
66+
|| git fetch --no-tags --unshallow origin 2>/dev/null \
67+
|| true
68+
fi
69+
CHANGES=$(npx ts-node scripts/detect-changed-filters.ts "$BASE_SHA" "$HEAD_SHA")
70+
echo "Matched filters: $CHANGES"
71+
echo "changes=$CHANGES" >> "$GITHUB_OUTPUT"
13172
13273
- name: Compute matrices
13374
id: matrix

scripts/build-app-filters.yml

Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
# Path filters consumed by the per-app build matrix in
2+
# `.github/workflows/build-apps.yml` and by the helper scripts in this
3+
# directory. Adding a new model directory, controller, or top-level module
4+
# under packages/react-native-executorch/ probably means updating an entry
5+
# below — `core-shared` if it affects every app, or one of the per-app
6+
# `<app>-pkg` anchors otherwise. CI runs `scripts/check-ci-filter-coverage.ts`
7+
# (in ci.yml's lint job) to flag uncovered files so this stays accurate.
8+
9+
# Cross-platform shared infrastructure
10+
core-shared: &core-shared
11+
- .github/workflows/build-apps.yml
12+
- scripts/build-app-filters.yml
13+
- scripts/detect-changed-filters.ts
14+
- packages/react-native-executorch/{scripts,third-party}/**
15+
- packages/react-native-executorch/{package.json,tsconfig.json}
16+
- packages/react-native-executorch/common/{ada,runner}/**
17+
- packages/react-native-executorch/common/rnexecutorch/{Error,ErrorCodes,Log}.h
18+
- packages/react-native-executorch/common/rnexecutorch/RnExecutorchInstaller.{cpp,h}
19+
- packages/react-native-executorch/common/rnexecutorch/{data_processing,host_objects,jsi,metaprogramming,tests,threads,utils}/**
20+
- packages/react-native-executorch/common/rnexecutorch/models/BaseModel.{cpp,h}
21+
- packages/react-native-executorch/src/{common,constants,errors,native,types,utils}/**
22+
- packages/react-native-executorch/src/index.ts
23+
- packages/react-native-executorch/src/modules/{BaseModule.ts,general/**}
24+
- packages/react-native-executorch/src/hooks/{general/**,useModule.ts,useModuleFactory.ts}
25+
- "{package.json,yarn.lock}"
26+
# Platform-specific shared
27+
android-shared: &android-shared
28+
- .github/actions/build-android-app/**
29+
- packages/react-native-executorch/android/**
30+
ios-shared: &ios-shared
31+
- .github/actions/build-ios-app/**
32+
- packages/react-native-executorch/ios/**
33+
- packages/react-native-executorch/react-native-executorch.podspec
34+
# Resource fetchers
35+
expo-fetcher: &expo-fetcher
36+
- packages/expo-resource-fetcher/**
37+
bare-fetcher: &bare-fetcher
38+
- packages/bare-resource-fetcher/**
39+
# Per-app package paths (TS modules + C++ models for the app's domain)
40+
llm-pkg: &llm-pkg
41+
- packages/react-native-executorch/common/rnexecutorch/TokenizerModule.{cpp,h}
42+
- packages/react-native-executorch/common/rnexecutorch/models/llm/**
43+
- packages/react-native-executorch/common/rnexecutorch/models/privacy_filter/**
44+
- packages/react-native-executorch/src/modules/natural_language_processing/{LLMModule,PrivacyFilterModule,TokenizerModule}.ts
45+
- packages/react-native-executorch/src/hooks/natural_language_processing/{useLLM,usePrivacyFilter,useTokenizer}.ts
46+
- packages/react-native-executorch/src/controllers/LLMController.ts
47+
cv-pkg: &cv-pkg
48+
- packages/react-native-executorch/common/rnexecutorch/models/VisionModel.{cpp,h}
49+
- packages/react-native-executorch/common/rnexecutorch/models/{classification,instance_segmentation,object_detection,ocr,pose_estimation,semantic_segmentation,style_transfer,text_to_image,vertical_ocr}/**
50+
- packages/react-native-executorch/src/{modules,hooks}/computer_vision/**
51+
- packages/react-native-executorch/src/controllers/{BaseOCRController,OCRController,VerticalOCRController}.ts
52+
speech-pkg: &speech-pkg
53+
- packages/react-native-executorch/common/pfft/**
54+
- packages/react-native-executorch/common/rnexecutorch/TokenizerModule.{cpp,h}
55+
- packages/react-native-executorch/common/rnexecutorch/models/{speech_to_text,text_to_speech,voice_activity_detection}/**
56+
- packages/react-native-executorch/src/modules/natural_language_processing/{SpeechToTextModule,TextToSpeechModule,VADModule,TokenizerModule}.ts
57+
- packages/react-native-executorch/src/hooks/natural_language_processing/{useSpeechToText,useTextToSpeech,useVAD,useTokenizer}.ts
58+
text-embeddings-pkg: &text-embeddings-pkg
59+
- packages/react-native-executorch/common/rnexecutorch/TokenizerModule.{cpp,h}
60+
- packages/react-native-executorch/common/rnexecutorch/models/embeddings/**
61+
- packages/react-native-executorch/src/modules/natural_language_processing/{TextEmbeddingsModule,TokenizerModule}.ts
62+
- packages/react-native-executorch/src/modules/computer_vision/ImageEmbeddingsModule.ts
63+
- packages/react-native-executorch/src/hooks/natural_language_processing/{useTextEmbeddings,useTokenizer}.ts
64+
- packages/react-native-executorch/src/hooks/computer_vision/useImageEmbeddings.ts
65+
# Per-app bundle: core + fetcher + pkg + app dir
66+
llm-app: &llm-app
67+
- *core-shared
68+
- *expo-fetcher
69+
- *llm-pkg
70+
- apps/llm/**
71+
computer-vision-app: &computer-vision-app
72+
- *core-shared
73+
- *expo-fetcher
74+
- *cv-pkg
75+
- apps/computer-vision/**
76+
speech-app: &speech-app
77+
- *core-shared
78+
- *expo-fetcher
79+
- *speech-pkg
80+
- apps/speech/**
81+
text-embeddings-app: &text-embeddings-app
82+
- *core-shared
83+
- *expo-fetcher
84+
- *text-embeddings-pkg
85+
- apps/text-embeddings/**
86+
bare-rn-app: &bare-rn-app
87+
- *core-shared
88+
- *bare-fetcher
89+
- *llm-pkg
90+
- apps/bare-rn/**
91+
# Final per-platform per-app filters (the only ones the matrix consumes)
92+
llm-android: [*llm-app, *android-shared]
93+
llm-ios: [*llm-app, *ios-shared]
94+
computer-vision-android: [*computer-vision-app, *android-shared]
95+
computer-vision-ios: [*computer-vision-app, *ios-shared]
96+
speech-android: [*speech-app, *android-shared]
97+
speech-ios: [*speech-app, *ios-shared]
98+
text-embeddings-android: [*text-embeddings-app, *android-shared]
99+
text-embeddings-ios: [*text-embeddings-app, *ios-shared]
100+
bare-rn-android: [*bare-rn-app, *android-shared]
101+
bare-rn-ios: [*bare-rn-app, *ios-shared]

scripts/check-ci-filter-coverage.ts

Lines changed: 7 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
#!/usr/bin/env ts-node
22
// Verifies every source file under packages/react-native-executorch/ is matched by
3-
// at least one filter in .github/workflows/build-apps.yml. Prevents new files (e.g.
3+
// at least one filter in scripts/build-app-filters.yml. Prevents new files (e.g.
44
// a new model directory or controller) from silently slipping past CI's per-app
55
// path triggers.
66

@@ -10,7 +10,7 @@ import * as cp from 'child_process';
1010
const yaml = require('js-yaml');
1111
const picomatch = require('picomatch');
1212

13-
const WORKFLOW = '.github/workflows/build-apps.yml';
13+
const FILTERS_FILE = 'scripts/build-app-filters.yml';
1414
const PACKAGE_ROOT = 'packages/react-native-executorch/';
1515

1616
// Files that legitimately don't belong to any per-app or shared filter.
@@ -25,11 +25,9 @@ type FilterValue = string | FilterValue[];
2525
const flatten = (x: FilterValue): string[] =>
2626
Array.isArray(x) ? x.flatMap(flatten) : [x];
2727

28-
const wf = yaml.load(fs.readFileSync(WORKFLOW, 'utf8'));
29-
const filtersStr = wf.jobs['detect-changes'].steps.find(
30-
(s: { id?: string }) => s.id === 'filter'
31-
).with.filters;
32-
const filters = yaml.load(filtersStr) as Record<string, FilterValue>;
28+
const filters = yaml.load(
29+
fs.readFileSync(FILTERS_FILE, 'utf8')
30+
) as Record<string, FilterValue>;
3331

3432
const patterns = new Set<string>();
3533
for (const v of Object.values(filters)) {
@@ -52,7 +50,7 @@ const orphans = tracked
5250

5351
if (orphans.length > 0) {
5452
console.error(
55-
`\n${WORKFLOW} does not cover ${orphans.length} file(s) under ${PACKAGE_ROOT}:\n`
53+
`\n${FILTERS_FILE} does not cover ${orphans.length} file(s) under ${PACKAGE_ROOT}:\n`
5654
);
5755
orphans.forEach((f) => console.error(' ' + f));
5856
console.error(
@@ -64,5 +62,5 @@ if (orphans.length > 0) {
6462
}
6563

6664
console.log(
67-
`OK: every file under ${PACKAGE_ROOT} is covered by build-apps.yml.`
65+
`OK: every file under ${PACKAGE_ROOT} is covered by ${FILTERS_FILE}.`
6866
);

scripts/compute-app-hash.ts

Lines changed: 15 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
#!/usr/bin/env ts-node
22
// Computes a stable content hash of every tracked file matched by a given
3-
// filter in .github/workflows/build-apps.yml. Used as a cache key in the
3+
// filter in scripts/build-app-filters.yml. Used as a cache key in the
44
// build-apps matrix so a previously-passing app/platform cell can be skipped
55
// when nothing relevant has changed since.
66

@@ -12,16 +12,21 @@ const yaml = require('js-yaml');
1212
const picomatch = require('picomatch');
1313

1414
// Files that the per-app filters reference for trigger purposes but that
15-
// should not invalidate cached build markers. The workflow file itself sits in
16-
// core-shared so editing it triggers CI on every app, but the vast majority of
17-
// edits are orchestration (filter paths, matrix shape, concurrency, triggers)
18-
// and don't change build behavior. Excluding it from the hash means workflow
19-
// edits re-run the matrix but each cell hits its existing marker and skips.
15+
// should not invalidate cached build markers. The workflow file, the filter
16+
// file, and the detection script all sit in core-shared so editing them
17+
// triggers CI on every app, but the vast majority of edits are orchestration
18+
// (filter paths, matrix shape, concurrency, triggers) and don't change build
19+
// behavior. Excluding them from the hash means orchestration edits re-run the
20+
// matrix but each cell hits its existing marker and skips.
2021
//
2122
// Caveat: workflow edits that DO change build behavior — `with:` inputs to a
2223
// composite, `runs-on:`, an `env:` var, an action's pinned version — won't be
2324
// caught here. Force-clear caches via the GitHub Actions UI when you make one.
24-
const HASH_EXCLUDE = new Set<string>(['.github/workflows/build-apps.yml']);
25+
const HASH_EXCLUDE = new Set<string>([
26+
'.github/workflows/build-apps.yml',
27+
'scripts/build-app-filters.yml',
28+
'scripts/detect-changed-filters.ts',
29+
]);
2530

2631
const filterName = process.argv[2];
2732
if (!filterName) {
@@ -31,13 +36,9 @@ if (!filterName) {
3136

3237
type FilterValue = string | FilterValue[];
3338

34-
const wf = yaml.load(
35-
fs.readFileSync('.github/workflows/build-apps.yml', 'utf8')
36-
);
37-
const filtersStr = wf.jobs['detect-changes'].steps.find(
38-
(s: { id?: string }) => s.id === 'filter'
39-
).with.filters;
40-
const filters = yaml.load(filtersStr) as Record<string, FilterValue>;
39+
const filters = yaml.load(
40+
fs.readFileSync('scripts/build-app-filters.yml', 'utf8')
41+
) as Record<string, FilterValue>;
4142

4243
const flatten = (x: FilterValue): string[] =>
4344
Array.isArray(x) ? x.flatMap(flatten) : [x];

0 commit comments

Comments
 (0)