apify-test-tools 0.10.2-beta.1 → 0.10.2-beta.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (99) hide show
  1. package/.gitattributes +2 -0
  2. package/.github/actions/checkout-restore-dependencies/action.yaml +13 -29
  3. package/.github/cli/apify-test-tools.mjs +361 -0
  4. package/.github/scripts/run-with-apify-tokens.mjs +2 -2
  5. package/.github/workflows/_check_code.yaml +25 -0
  6. package/.github/workflows/_move_major_tag.yaml +3 -81
  7. package/.github/workflows/_tests.yaml +34 -0
  8. package/.github/workflows/_update_release_metadata.yaml +0 -12
  9. package/.github/workflows/manual_move_major_tag.yaml +3 -3
  10. package/.github/workflows/manual_release_stable.yaml +3 -13
  11. package/.github/workflows/on_master.yaml +9 -5
  12. package/.github/workflows/on_pull_request.yaml +3 -48
  13. package/.github/workflows/public_platform-tests.yaml +1 -1
  14. package/.github/workflows/public_pr-build-test.yaml +2 -2
  15. package/.github/workflows/public_push-build-latest.yaml +2 -2
  16. package/.husky/pre-commit +5 -0
  17. package/.prettierignore +3 -0
  18. package/CHANGELOG.md +5 -0
  19. package/CONTRIBUTING.md +8 -18
  20. package/README.md +94 -44
  21. package/bin/affected-actors.ts +166 -0
  22. package/bin/build-from-local.ts +47 -13
  23. package/bin/build.ts +35 -19
  24. package/bin/diff-changes.ts +25 -17
  25. package/bin/git.ts +52 -41
  26. package/bin/main.ts +63 -78
  27. package/bin/slack.ts +28 -6
  28. package/bin/test-report.ts +6 -9
  29. package/bin/types.ts +9 -0
  30. package/dist/bin/affected-actors.d.ts +50 -0
  31. package/dist/bin/affected-actors.d.ts.map +1 -0
  32. package/dist/bin/affected-actors.js +89 -0
  33. package/dist/bin/affected-actors.js.map +1 -0
  34. package/dist/bin/build-from-local.d.ts +10 -2
  35. package/dist/bin/build-from-local.d.ts.map +1 -1
  36. package/dist/bin/build-from-local.js +29 -11
  37. package/dist/bin/build-from-local.js.map +1 -1
  38. package/dist/bin/build.d.ts +15 -5
  39. package/dist/bin/build.d.ts.map +1 -1
  40. package/dist/bin/build.js +24 -15
  41. package/dist/bin/build.js.map +1 -1
  42. package/dist/bin/diff-changes.d.ts +8 -2
  43. package/dist/bin/diff-changes.d.ts.map +1 -1
  44. package/dist/bin/diff-changes.js +16 -16
  45. package/dist/bin/diff-changes.js.map +1 -1
  46. package/dist/bin/git.d.ts +23 -13
  47. package/dist/bin/git.d.ts.map +1 -1
  48. package/dist/bin/git.js +44 -32
  49. package/dist/bin/git.js.map +1 -1
  50. package/dist/bin/main.js +57 -57
  51. package/dist/bin/main.js.map +1 -1
  52. package/dist/bin/slack.d.ts +8 -1
  53. package/dist/bin/slack.d.ts.map +1 -1
  54. package/dist/bin/slack.js +26 -5
  55. package/dist/bin/slack.js.map +1 -1
  56. package/dist/bin/test-report.d.ts.map +1 -1
  57. package/dist/bin/test-report.js +6 -8
  58. package/dist/bin/test-report.js.map +1 -1
  59. package/dist/bin/types.d.ts +8 -0
  60. package/dist/bin/types.d.ts.map +1 -1
  61. package/dist/test/bundle/cli-bundle.test.d.ts +2 -0
  62. package/dist/test/bundle/cli-bundle.test.d.ts.map +1 -0
  63. package/dist/test/bundle/cli-bundle.test.js +169 -0
  64. package/dist/test/bundle/cli-bundle.test.js.map +1 -0
  65. package/dist/test/e2e/cli-bundle.e2e.test.d.ts +2 -0
  66. package/dist/test/e2e/cli-bundle.e2e.test.d.ts.map +1 -0
  67. package/dist/test/e2e/cli-bundle.e2e.test.js +172 -0
  68. package/dist/test/e2e/cli-bundle.e2e.test.js.map +1 -0
  69. package/dist/test/unit/bin/affected-actors.test.d.ts +2 -0
  70. package/dist/test/unit/bin/affected-actors.test.d.ts.map +1 -0
  71. package/dist/test/unit/bin/affected-actors.test.js +160 -0
  72. package/dist/test/unit/bin/affected-actors.test.js.map +1 -0
  73. package/dist/test/unit/bin/build-from-local.test.js +53 -1
  74. package/dist/test/unit/bin/build-from-local.test.js.map +1 -1
  75. package/dist/test/unit/bin/build.test.js +113 -3
  76. package/dist/test/unit/bin/build.test.js.map +1 -1
  77. package/dist/test/unit/bin/diff-changes.test.js +78 -49
  78. package/dist/test/unit/bin/diff-changes.test.js.map +1 -1
  79. package/dist/test/unit/bin/git.test.js +50 -53
  80. package/dist/test/unit/bin/git.test.js.map +1 -1
  81. package/dist/test/unit/bin/slack.test.d.ts +2 -0
  82. package/dist/test/unit/bin/slack.test.d.ts.map +1 -0
  83. package/dist/test/unit/bin/slack.test.js +54 -0
  84. package/dist/test/unit/bin/slack.test.js.map +1 -0
  85. package/dist/test/unit/should-built-and-test.test.js +23 -17
  86. package/dist/test/unit/should-built-and-test.test.js.map +1 -1
  87. package/dist/tsconfig.tsbuildinfo +1 -1
  88. package/package.json +13 -3
  89. package/test/bundle/cli-bundle.test.ts +199 -0
  90. package/test/e2e/cli-bundle.e2e.test.ts +237 -0
  91. package/test/unit/bin/affected-actors.test.ts +190 -0
  92. package/test/unit/bin/build-from-local.test.ts +55 -0
  93. package/test/unit/bin/build.test.ts +135 -3
  94. package/test/unit/bin/diff-changes.test.ts +87 -49
  95. package/test/unit/bin/git.test.ts +56 -53
  96. package/test/unit/bin/slack.test.ts +65 -0
  97. package/test/unit/should-built-and-test.test.ts +27 -17
  98. package/.github/scripts/check-package-version-bump.mjs +0 -74
  99. package/.github/workflows-package-version +0 -1
@@ -61,6 +61,16 @@ const amazonActor = actorConfig('junglee/amazon-crawler', 'actors/junglee_Amazon
61
61
 
62
62
  const commits = [{ sha: 'Commit1', author: '', date: '', message: '' }];
63
63
 
64
+ // What a PR (functional changes only) or a release (isLatest, all changes) builds, in config order
65
+ const changedActorsFor = async ({
66
+ isLatest = false,
67
+ ...options
68
+ }: Parameters<typeof getChangedActors>[0] & { isLatest?: boolean }): Promise<ActorConfig[]> => {
69
+ const { functional, cosmetic } = await getChangedActors(options);
70
+ const changed = isLatest ? [...functional, ...cosmetic] : functional;
71
+ return options.actorConfigs.filter((config) => changed.includes(config));
72
+ };
73
+
64
74
  describe('getChangedActors', () => {
65
75
  beforeEach(() => {
66
76
  vi.restoreAllMocks();
@@ -68,11 +78,11 @@ describe('getChangedActors', () => {
68
78
  });
69
79
 
70
80
  it('returns empty array when no files changed', async () => {
71
- expect(await getChangedActors({ filepathsChanged: [], actorConfigs, commits })).toEqual([]);
81
+ expect(await changedActorsFor({ filepathsChanged: [], actorConfigs, commits })).toEqual([]);
72
82
  });
73
83
 
74
84
  it('returns empty array when only ignored top-level files changed', async () => {
75
- const result = await getChangedActors({
85
+ const result = await changedActorsFor({
76
86
  filepathsChanged: ['.gitignore', 'README.md', '.husky/pre-commit', '.vscode/settings.json'],
77
87
  actorConfigs,
78
88
  commits,
@@ -81,7 +91,7 @@ describe('getChangedActors', () => {
81
91
  });
82
92
 
83
93
  it('does not ignore source files whose names contain an ignored file name', async () => {
84
- const result = await getChangedActors({
94
+ const result = await changedActorsFor({
85
95
  filepathsChanged: ['actors/foo_bar/src/.gitignore-helper.ts'],
86
96
  actorConfigs,
87
97
  commits,
@@ -90,7 +100,7 @@ describe('getChangedActors', () => {
90
100
  });
91
101
 
92
102
  it('returns the actor when a functional file in its folder changes', async () => {
93
- const result = await getChangedActors({
103
+ const result = await changedActorsFor({
94
104
  filepathsChanged: ['actors/foo_bar/src/main.ts'],
95
105
  actorConfigs,
96
106
  commits,
@@ -99,7 +109,7 @@ describe('getChangedActors', () => {
99
109
  });
100
110
 
101
111
  it('returns actor when isLatest and README in actor folder changed (cosmetic)', async () => {
102
- const result = await getChangedActors({
112
+ const result = await changedActorsFor({
103
113
  filepathsChanged: ['actors/foo_bar/README.md'],
104
114
  actorConfigs,
105
115
  commits,
@@ -109,7 +119,7 @@ describe('getChangedActors', () => {
109
119
  });
110
120
 
111
121
  it('does not return actor when not isLatest and only README changed (cosmetic)', async () => {
112
- const result = await getChangedActors({
122
+ const result = await changedActorsFor({
113
123
  filepathsChanged: ['actors/foo_bar/README.md'],
114
124
  actorConfigs,
115
125
  commits,
@@ -120,7 +130,7 @@ describe('getChangedActors', () => {
120
130
 
121
131
  it('returns actor when isLatest and JSON file has only cosmetic changes', async () => {
122
132
  vi.spyOn(DiffJsonSchema, 'isCosmeticOnlyJsonSchemaChange').mockResolvedValue(true);
123
- const result = await getChangedActors({
133
+ const result = await changedActorsFor({
124
134
  filepathsChanged: ['actors/foo_bar/.actor/actor.json'],
125
135
  actorConfigs,
126
136
  commits,
@@ -131,7 +141,7 @@ describe('getChangedActors', () => {
131
141
 
132
142
  it('does not return actor when not isLatest and JSON file has only cosmetic changes', async () => {
133
143
  vi.spyOn(DiffJsonSchema, 'isCosmeticOnlyJsonSchemaChange').mockResolvedValue(true);
134
- const result = await getChangedActors({
144
+ const result = await changedActorsFor({
135
145
  filepathsChanged: ['actors/foo_bar/.actor/actor.json'],
136
146
  actorConfigs,
137
147
  commits,
@@ -141,7 +151,7 @@ describe('getChangedActors', () => {
141
151
  });
142
152
 
143
153
  it('returns actor when JSON file has functional changes', async () => {
144
- const result = await getChangedActors({
154
+ const result = await changedActorsFor({
145
155
  filepathsChanged: ['actors/foo_bar/.actor/actor.json'],
146
156
  actorConfigs,
147
157
  commits,
@@ -151,7 +161,7 @@ describe('getChangedActors', () => {
151
161
 
152
162
  it('JSON file in actor folder but outside .actor/ is functional, not checked for cosmetic', async () => {
153
163
  vi.spyOn(DiffJsonSchema, 'isCosmeticOnlyJsonSchemaChange').mockResolvedValue(true);
154
- const result = await getChangedActors({
164
+ const result = await changedActorsFor({
155
165
  filepathsChanged: ['actors/foo_bar/package.json'],
156
166
  actorConfigs,
157
167
  commits,
@@ -163,7 +173,7 @@ describe('getChangedActors', () => {
163
173
 
164
174
  it('JSON file under .actor/ inside actor folder is checked for cosmetic changes', async () => {
165
175
  vi.spyOn(DiffJsonSchema, 'isCosmeticOnlyJsonSchemaChange').mockResolvedValue(true);
166
- const result = await getChangedActors({
176
+ const result = await changedActorsFor({
167
177
  filepathsChanged: ['actors/foo_bar/.actor/input_schema.json'],
168
178
  actorConfigs,
169
179
  commits,
@@ -177,7 +187,7 @@ describe('getChangedActors', () => {
177
187
  });
178
188
 
179
189
  it('does not trigger narrow-context actor when shared file changes', async () => {
180
- const result = await getChangedActors({
190
+ const result = await changedActorsFor({
181
191
  filepathsChanged: ['shared/utils.ts'],
182
192
  actorConfigs,
183
193
  commits,
@@ -187,7 +197,7 @@ describe('getChangedActors', () => {
187
197
  });
188
198
 
189
199
  it('root-level changelog outside any actor folder is cosmetic for every actor when isLatest', async () => {
190
- const result = await getChangedActors({
200
+ const result = await changedActorsFor({
191
201
  filepathsChanged: ['CHANGELOG.md'],
192
202
  actorConfigs,
193
203
  commits,
@@ -198,7 +208,7 @@ describe('getChangedActors', () => {
198
208
  });
199
209
 
200
210
  it('root-level changelog is not cosmetic-triggered when not isLatest', async () => {
201
- const result = await getChangedActors({
211
+ const result = await changedActorsFor({
202
212
  filepathsChanged: ['CHANGELOG.md'],
203
213
  actorConfigs,
204
214
  commits,
@@ -208,7 +218,7 @@ describe('getChangedActors', () => {
208
218
  });
209
219
 
210
220
  it('treats any changelog filename as cosmetic regardless of configured paths', async () => {
211
- const result = await getChangedActors({
221
+ const result = await changedActorsFor({
212
222
  filepathsChanged: ['docs/CHANGELOG.md'],
213
223
  actorConfigs: [miniActor, standaloneActor],
214
224
  commits,
@@ -222,7 +232,7 @@ describe('getChangedActors', () => {
222
232
  ...standaloneActor,
223
233
  actorJson: { ...standaloneActor.actorJson, readme: new RelativeFile('docs/guide.md') },
224
234
  };
225
- const result = await getChangedActors({
235
+ const result = await changedActorsFor({
226
236
  filepathsChanged: ['docs/guide.md'],
227
237
  actorConfigs: [configuredActor],
228
238
  commits,
@@ -232,7 +242,7 @@ describe('getChangedActors', () => {
232
242
  });
233
243
 
234
244
  it('triggers narrow-context actor when its own folder changes', async () => {
235
- const result = await getChangedActors({
245
+ const result = await changedActorsFor({
236
246
  filepathsChanged: ['standalone-actors/standalone/src/main.ts'],
237
247
  actorConfigs,
238
248
  commits,
@@ -241,7 +251,7 @@ describe('getChangedActors', () => {
241
251
  });
242
252
 
243
253
  it('deduplicates actors when multiple files in same actor folder change', async () => {
244
- const result = await getChangedActors({
254
+ const result = await changedActorsFor({
245
255
  filepathsChanged: ['actors/foo_bar/src/main.ts', 'actors/foo_bar/package.json'],
246
256
  actorConfigs,
247
257
  commits,
@@ -251,7 +261,7 @@ describe('getChangedActors', () => {
251
261
  });
252
262
 
253
263
  it('handles mixed changes: returns both broad and narrow-context actors', async () => {
254
- const result = await getChangedActors({
264
+ const result = await changedActorsFor({
255
265
  filepathsChanged: ['actors/foo_bar/src/main.ts', 'standalone-actors/standalone/Dockerfile'],
256
266
  actorConfigs,
257
267
  commits,
@@ -262,7 +272,7 @@ describe('getChangedActors', () => {
262
272
 
263
273
  it('matches folder where folder name differs from actor name', async () => {
264
274
  const ownerlessActor = actorConfig('myteam/shopify-scraper', 'actors/shopify', 'APIFY_TOKEN_MYTEAM', '', ['']);
265
- const result = await getChangedActors({
275
+ const result = await changedActorsFor({
266
276
  filepathsChanged: ['actors/shopify/src/main.ts'],
267
277
  actorConfigs: [ownerlessActor],
268
278
  commits,
@@ -272,7 +282,7 @@ describe('getChangedActors', () => {
272
282
 
273
283
  it('in single-actor repo, .actor/ changes trigger builds', async () => {
274
284
  const rootActor = actorConfig('myteam/my-actor', '', 'BUILDER_APIFY_TOKEN', '', ['']);
275
- const result = await getChangedActors({
285
+ const result = await changedActorsFor({
276
286
  filepathsChanged: ['.actor/actor.json'],
277
287
  actorConfigs: [rootActor],
278
288
  commits,
@@ -281,7 +291,7 @@ describe('getChangedActors', () => {
281
291
  });
282
292
 
283
293
  it('in multi-actor repo, .actor/ changes only trigger broad-context actors', async () => {
284
- const result = await getChangedActors({
294
+ const result = await changedActorsFor({
285
295
  filepathsChanged: ['.actor/actor.json'],
286
296
  actorConfigs,
287
297
  commits,
@@ -293,7 +303,7 @@ describe('getChangedActors', () => {
293
303
  const narrowActor = actorConfig('foo/bar', 'actors/foo_bar', 'APIFY_TOKEN_FOO', 'actors/foo_bar', [
294
304
  'actors/foo_bar',
295
305
  ]);
296
- const result = await getChangedActors({
306
+ const result = await changedActorsFor({
297
307
  filepathsChanged: ['Actors/FOO_BAR/Main.ts'],
298
308
  actorConfigs: [narrowActor],
299
309
  commits,
@@ -309,7 +319,7 @@ describe('getChangedActors', () => {
309
319
  'actors/override',
310
320
  ['actors/override', 'packages'],
311
321
  );
312
- const result = await getChangedActors({
322
+ const result = await changedActorsFor({
313
323
  filepathsChanged: ['packages/shared/utils.ts'],
314
324
  actorConfigs: [overrideActor],
315
325
  commits,
@@ -325,7 +335,7 @@ describe('getChangedActors', () => {
325
335
  'actors/override',
326
336
  ['actors/override', 'packages'],
327
337
  );
328
- const result = await getChangedActors({
338
+ const result = await changedActorsFor({
329
339
  filepathsChanged: ['other-dir/file.ts'],
330
340
  actorConfigs: [overrideActor],
331
341
  commits,
@@ -336,7 +346,7 @@ describe('getChangedActors', () => {
336
346
  it('broad-context actor skips files in sibling actor folders', async () => {
337
347
  const actorA = actorConfig('team/actor-a', 'actors/a', 'APIFY_TOKEN_TEAM', '', ['']);
338
348
  const actorB = actorConfig('team/actor-b', 'actors/b', 'APIFY_TOKEN_TEAM', '', ['']);
339
- const result = await getChangedActors({
349
+ const result = await changedActorsFor({
340
350
  filepathsChanged: ['actors/b/src/main.ts'],
341
351
  actorConfigs: [actorA, actorB],
342
352
  commits,
@@ -349,7 +359,7 @@ describe('getChangedActors', () => {
349
359
  const childActor = actorConfig('team/child', 'actors/child', 'APIFY_TOKEN_TEAM', 'actors/child', [
350
360
  'actors/child',
351
361
  ]);
352
- const result = await getChangedActors({
362
+ const result = await changedActorsFor({
353
363
  filepathsChanged: ['actors/child/src/main.ts'],
354
364
  actorConfigs: [rootActor, childActor],
355
365
  commits,
@@ -363,7 +373,7 @@ describe('getChangedActors', () => {
363
373
  const childActor = actorConfig('team/child', 'actors/child', 'APIFY_TOKEN_TEAM', 'actors/child', [
364
374
  'actors/child',
365
375
  ]);
366
- const result = await getChangedActors({
376
+ const result = await changedActorsFor({
367
377
  filepathsChanged: ['lib/shared-utils.ts'],
368
378
  actorConfigs: [rootActor, childActor],
369
379
  commits,
@@ -376,7 +386,7 @@ describe('getChangedActors', () => {
376
386
  vi.spyOn(Dockerignore, 'loadDockerIgnore').mockReturnValue(
377
387
  (filePath) => filePath === 'actors/foo_bar/node_modules/foo.js',
378
388
  );
379
- const result = await getChangedActors({
389
+ const result = await changedActorsFor({
380
390
  filepathsChanged: ['actors/foo_bar/node_modules/foo.js'],
381
391
  actorConfigs: [miniActor],
382
392
  commits,
@@ -386,7 +396,7 @@ describe('getChangedActors', () => {
386
396
 
387
397
  it('file not matched by .dockerignore is classified normally', async () => {
388
398
  vi.spyOn(Dockerignore, 'loadDockerIgnore').mockReturnValue((filePath) => filePath.includes('node_modules'));
389
- const result = await getChangedActors({
399
+ const result = await changedActorsFor({
390
400
  filepathsChanged: ['actors/foo_bar/src/main.ts'],
391
401
  actorConfigs: [miniActor],
392
402
  commits,
@@ -396,7 +406,7 @@ describe('getChangedActors', () => {
396
406
 
397
407
  it('test file listed in .dockerignore triggers a build when it matches the test files glob', async () => {
398
408
  vi.spyOn(Dockerignore, 'loadDockerIgnore').mockReturnValue((filePath) => filePath.startsWith('test/'));
399
- const result = await getChangedActors({
409
+ const result = await changedActorsFor({
400
410
  filepathsChanged: ['test/platform/daily/foo.test.ts'],
401
411
  actorConfigs: [miniActor],
402
412
  commits,
@@ -407,7 +417,7 @@ describe('getChangedActors', () => {
407
417
 
408
418
  it('test file listed in .dockerignore stays ignored without a test files glob', async () => {
409
419
  vi.spyOn(Dockerignore, 'loadDockerIgnore').mockReturnValue((filePath) => filePath.startsWith('test/'));
410
- const result = await getChangedActors({
420
+ const result = await changedActorsFor({
411
421
  filepathsChanged: ['test/platform/daily/foo.test.ts'],
412
422
  actorConfigs: [miniActor],
413
423
  commits,
@@ -417,7 +427,7 @@ describe('getChangedActors', () => {
417
427
 
418
428
  it('test files glob matches case-insensitively and does not bypass .dockerignore for other files', async () => {
419
429
  vi.spyOn(Dockerignore, 'loadDockerIgnore').mockReturnValue(() => true);
420
- const result = await getChangedActors({
430
+ const result = await changedActorsFor({
421
431
  filepathsChanged: ['Test/Platform/Foo.test.ts'],
422
432
  actorConfigs: [miniActor],
423
433
  commits,
@@ -425,7 +435,7 @@ describe('getChangedActors', () => {
425
435
  });
426
436
  expect(result).toEqual([miniActor]);
427
437
  expect(
428
- await getChangedActors({
438
+ await changedActorsFor({
429
439
  filepathsChanged: ['actors/foo_bar/node_modules/foo.js'],
430
440
  actorConfigs: [miniActor],
431
441
  commits,
@@ -436,7 +446,7 @@ describe('getChangedActors', () => {
436
446
 
437
447
  it('test file outside an actor context still does not trigger it', async () => {
438
448
  vi.spyOn(Dockerignore, 'loadDockerIgnore').mockReturnValue(() => true);
439
- const result = await getChangedActors({
449
+ const result = await changedActorsFor({
440
450
  filepathsChanged: ['test/platform/daily/foo.test.ts'],
441
451
  actorConfigs: [standaloneActor],
442
452
  commits,
@@ -451,7 +461,7 @@ describe('getChangedActors', () => {
451
461
  ...standaloneActor,
452
462
  contextPaths: [...standaloneActor.contextPaths, new RelativeDir('test')],
453
463
  };
454
- const result = await getChangedActors({
464
+ const result = await changedActorsFor({
455
465
  filepathsChanged: ['test/platform/standalone.test.ts'],
456
466
  actorConfigs: [actorWithTests],
457
467
  commits,
@@ -461,7 +471,7 @@ describe('getChangedActors', () => {
461
471
  });
462
472
 
463
473
  it('JSON file in context but outside actor folder is functional (not checked for cosmetic)', async () => {
464
- const result = await getChangedActors({
474
+ const result = await changedActorsFor({
465
475
  filepathsChanged: ['lib/config.json'],
466
476
  actorConfigs: [miniActor],
467
477
  commits,
@@ -470,7 +480,7 @@ describe('getChangedActors', () => {
470
480
  });
471
481
 
472
482
  it('README outside actor folder but inside context is ignored (not cosmetic)', async () => {
473
- const result = await getChangedActors({
483
+ const result = await changedActorsFor({
474
484
  filepathsChanged: ['docs/README.md'],
475
485
  actorConfigs: [miniActor],
476
486
  commits,
@@ -480,7 +490,7 @@ describe('getChangedActors', () => {
480
490
  });
481
491
 
482
492
  it('README inside actor folder is cosmetic', async () => {
483
- const result = await getChangedActors({
493
+ const result = await changedActorsFor({
484
494
  filepathsChanged: ['actors/foo_bar/README.md'],
485
495
  actorConfigs: [miniActor],
486
496
  commits,
@@ -490,7 +500,7 @@ describe('getChangedActors', () => {
490
500
  });
491
501
 
492
502
  it('ignores a standalone actor top-level dev file relative to its context', async () => {
493
- const result = await getChangedActors({
503
+ const result = await changedActorsFor({
494
504
  filepathsChanged: ['standalone-actors/standalone/.eslintrc'],
495
505
  actorConfigs,
496
506
  commits,
@@ -499,7 +509,7 @@ describe('getChangedActors', () => {
499
509
  });
500
510
 
501
511
  it('does not special-case code/ and shared/ prefixes anymore — must be declared via overrideActorContext', async () => {
502
- const result = await getChangedActors({
512
+ const result = await changedActorsFor({
503
513
  filepathsChanged: ['code/some/code.ts'],
504
514
  actorConfigs: [miniActor],
505
515
  commits,
@@ -508,7 +518,7 @@ describe('getChangedActors', () => {
508
518
  });
509
519
 
510
520
  it('ignores code/.eslintrc when "code" is declared via overrideActorContext', async () => {
511
- const result = await getChangedActors({
521
+ const result = await changedActorsFor({
512
522
  filepathsChanged: ['code/.eslintrc'],
513
523
  actorConfigs: [amazonActor],
514
524
  commits,
@@ -517,7 +527,7 @@ describe('getChangedActors', () => {
517
527
  });
518
528
 
519
529
  it('shared/Dockerfile declared via overrideActorContext is functional', async () => {
520
- const result = await getChangedActors({
530
+ const result = await changedActorsFor({
521
531
  filepathsChanged: ['shared/Dockerfile'],
522
532
  actorConfigs: [amazonActor],
523
533
  commits,
@@ -526,7 +536,7 @@ describe('getChangedActors', () => {
526
536
  });
527
537
 
528
538
  it('code/README.md declared via overrideActorContext is ignored (outside actor folder)', async () => {
529
- const result = await getChangedActors({
539
+ const result = await changedActorsFor({
530
540
  filepathsChanged: ['code/README.md'],
531
541
  actorConfigs: [amazonActor],
532
542
  commits,
@@ -553,7 +563,8 @@ describe('getChangedActors logging', () => {
553
563
  expect(logger.info).toHaveBeenCalledWith(
554
564
  '[DIFF]: Changes specific to actor foo/bar: actors/foo_bar/src/main.ts',
555
565
  );
556
- expect(logger.info).toHaveBeenCalledWith('[DIFF]: Actors to be built and tested: foo/bar');
566
+ expect(logger.info).toHaveBeenCalledWith('[DIFF]: Actors with functional changes: foo/bar');
567
+ expect(logger.info).toHaveBeenCalledWith('[DIFF]: Actors with only cosmetic changes: none');
557
568
  });
558
569
 
559
570
  it('logs a single "shared" group when two actors are triggered by the exact same file', async () => {
@@ -570,7 +581,7 @@ describe('getChangedActors logging', () => {
570
581
  '[DIFF]: Shared changes for actors team/actor-a, team/actor-b: shared/shared.ts',
571
582
  );
572
583
  expect(logger.info).not.toHaveBeenCalledWith(expect.stringContaining('Changes specific to actor'));
573
- expect(logger.info).toHaveBeenCalledWith('[DIFF]: Actors to be built and tested: team/actor-a, team/actor-b');
584
+ expect(logger.info).toHaveBeenCalledWith('[DIFF]: Actors with functional changes: team/actor-a, team/actor-b');
574
585
  });
575
586
 
576
587
  it('logs shared and specific groups in descending-size order for partial overlap across actors', async () => {
@@ -589,7 +600,8 @@ describe('getChangedActors logging', () => {
589
600
  '[DIFF]: Shared changes for actors team/actor-a, team/actor-b: shared.ts',
590
601
  '[DIFF]: Changes specific to actor team/actor-a: actors/a/a-only.ts',
591
602
  '[DIFF]: Changes specific to actor team/actor-b: actors/b/b-only.ts',
592
- '[DIFF]: Actors to be built and tested: team/actor-a, team/actor-b',
603
+ '[DIFF]: Actors with functional changes: team/actor-a, team/actor-b',
604
+ '[DIFF]: Actors with only cosmetic changes: none',
593
605
  ]);
594
606
  });
595
607
 
@@ -609,7 +621,7 @@ describe('getChangedActors logging', () => {
609
621
  });
610
622
 
611
623
  it('still warns when the same push also rebuilds an Actor', async () => {
612
- const result = await getChangedActors({
624
+ const result = await changedActorsFor({
613
625
  filepathsChanged: ['test/platform/standalone.test.ts', 'standalone-actors/standalone/src/main.ts'],
614
626
  actorConfigs: [standaloneActor],
615
627
  commits,
@@ -640,6 +652,32 @@ describe('getChangedActors logging', () => {
640
652
 
641
653
  expect(logger.info).not.toHaveBeenCalledWith(expect.stringContaining('Shared changes'));
642
654
  expect(logger.info).not.toHaveBeenCalledWith(expect.stringContaining('Changes specific to'));
643
- expect(logger.info).toHaveBeenCalledWith('[DIFF]: No relevant files changed, skipping builds and tests');
655
+ expect(logger.info).toHaveBeenCalledWith('[DIFF]: Actors with functional changes: none');
656
+ expect(logger.info).toHaveBeenCalledWith('[DIFF]: Actors with only cosmetic changes: none');
657
+ });
658
+ });
659
+
660
+ describe('getChangedActors split by impact', () => {
661
+ beforeEach(() => {
662
+ vi.spyOn(DiffJsonSchema, 'isCosmeticOnlyJsonSchemaChange').mockResolvedValue(false);
663
+ });
664
+
665
+ it('lists an Actor as functional when it has any functional change, and as cosmetic only otherwise', async () => {
666
+ const result = await getChangedActors({
667
+ filepathsChanged: [
668
+ 'actors/foo_bar/README.md',
669
+ 'standalone-actors/standalone/README.md',
670
+ 'actors/foo_bar/src/main.ts',
671
+ ],
672
+ actorConfigs,
673
+ commits,
674
+ });
675
+ expect(result).toStrictEqual({ functional: [miniActor], cosmetic: [standaloneActor] });
676
+ });
677
+
678
+ it('returns empty lists when nothing relevant changed', async () => {
679
+ expect(
680
+ await getChangedActors({ filepathsChanged: ['.vscode/settings.json'], actorConfigs, commits }),
681
+ ).toStrictEqual({ functional: [], cosmetic: [] });
644
682
  });
645
683
  });
@@ -4,13 +4,16 @@ import { beforeEach, describe, expect, it, vi } from 'vitest';
4
4
  import {
5
5
  getBranchOnlyChangedFiles,
6
6
  getChangedFiles,
7
+ getChangedFilesBetween,
7
8
  getCommits,
9
+ getCommitsBetween,
8
10
  getCurrentBranch,
9
- getReleaseChanges,
10
11
  getRepoName,
11
12
  hasMergeFromTarget,
12
13
  normalizeRepoUrl,
13
14
  parseBaseCommit,
15
+ resolveBranchName,
16
+ resolveCommitSha,
14
17
  resolveReleaseBaseCommit,
15
18
  resolveRepoUrl,
16
19
  } from '../../../bin/git.js';
@@ -474,71 +477,71 @@ describe('resolveReleaseBaseCommit', () => {
474
477
  it('should throw on an invalid SHA', async () => {
475
478
  await expect(resolveReleaseBaseCommit('not-a-sha')).rejects.toThrow('Invalid base commit SHA');
476
479
  });
477
- });
478
-
479
- describe('getReleaseChanges', () => {
480
- const baseSha = 'b'.repeat(40);
481
- const headSha = 'd'.repeat(40);
482
- const mergedBranchCommit = `${'1'.repeat(40)}»¦«Dev<dev@example.com>»¦«Date1»¦«feat: branch change`;
483
- const mergeCommit = `${headSha}»¦«Dev<dev@example.com>»¦«Date2»¦«Merge pull request #1`;
484
-
485
- let gitRunSpy: MockInstance;
486
480
 
487
- beforeEach(() => {
488
- gitRunSpy = vi.spyOn(Utils, 'runGitCommand').mockImplementation(async (args: string[]) => {
489
- if (args[0] === 'rev-parse') return headSha;
490
- if (args[0] === 'log') return `${mergeCommit}\n${mergedBranchCommit}`;
491
- if (args.includes('--name-only')) return 'actors/foo/src/main.ts\nCHANGELOG.md';
492
- if (args[0] === 'diff')
493
- return 'diff --git a/CHANGELOG.md b/CHANGELOG.md\n--- a/CHANGELOG.md\n+++ b/CHANGELOG.md\n@@ -1 +1,2 @@\n+- Added foo\n # Changelog';
481
+ it('checks ancestry against a custom head', async () => {
482
+ const gitCommandSpy = vi.spyOn(Utils, 'runGitCommand').mockImplementation(async (args: string[]) => {
483
+ if (args[0] === 'rev-parse') return baseSha;
484
+ if (args[0] === 'merge-base') return 'c'.repeat(40);
494
485
  return '';
495
486
  });
487
+ await expect(resolveReleaseBaseCommit(baseSha, 'origin/release')).rejects.toThrow(
488
+ 'is not an ancestor of origin/release',
489
+ );
490
+ expect(gitCommandSpy).toHaveBeenCalledWith(['merge-base', baseSha, 'origin/release'], {
491
+ operation: 'check whether the release base commit is an ancestor of origin/release',
492
+ });
496
493
  });
494
+ });
497
495
 
498
- it('should diff the range directly and return the commits oldest first', async () => {
499
- const changes = await getReleaseChanges(baseSha);
496
+ describe('getCommitsBetween', () => {
497
+ it('lists the commits after fromRef up to toRef, oldest first', async () => {
498
+ const older = `${'1'.repeat(40)}»¦«Dev<dev@example.com>»¦«Date1»¦«first`;
499
+ const newer = `${'2'.repeat(40)}»¦«Dev<dev@example.com>»¦«Date2»¦«second`;
500
+ const spy = vi.spyOn(Utils, 'runGitCommand').mockResolvedValue(`${newer}\n${older}`);
500
501
 
501
- expect(changes).toStrictEqual({
502
- commits: [
503
- { sha: '1'.repeat(40), author: 'Dev<dev@example.com>', date: 'Date1', message: 'feat: branch change' },
504
- { sha: headSha, author: 'Dev<dev@example.com>', date: 'Date2', message: 'Merge pull request #1' },
505
- ],
506
- changedFiles: ['actors/foo/src/main.ts', 'CHANGELOG.md'],
507
- changelog: '- Added foo',
508
- });
509
- // Not from the parent of the oldest commit, which may predate the base commit for merge commits
510
- expect(gitRunSpy).toHaveBeenCalledWith(['rev-parse', 'HEAD'], {
511
- operation: 'identify the current release commit',
512
- });
513
- expect(gitRunSpy).toHaveBeenCalledWith(['diff', '--name-only', baseSha, 'HEAD'], {
514
- operation: 'list files changed since the last release',
515
- });
516
- expect(gitRunSpy).toHaveBeenCalledWith(
517
- ['log', '--pretty=format:%H»¦«%aN<%aE>»¦«%aD»¦«%s', `${baseSha}..HEAD`],
518
- { operation: 'list commits reachable from the source ref but absent from the target ref' },
519
- );
520
- expect(gitRunSpy).toHaveBeenCalledWith(['diff', baseSha, 'HEAD', '--', 'CHANGELOG.md'], {
521
- operation: 'read changelog changes since the last release',
502
+ const commits = await getCommitsBetween('base-sha', 'head-sha');
503
+
504
+ expect(commits.map((c) => c.message)).toStrictEqual(['first', 'second']);
505
+ expect(spy).toHaveBeenCalledWith(['log', '--pretty=format:%H»¦«%aN<%aE>»¦«%aD»¦«%s', 'base-sha..head-sha'], {
506
+ operation: 'list commits reachable from the source ref but absent from the target ref',
522
507
  });
523
508
  });
509
+ });
510
+
511
+ describe('getChangedFilesBetween', () => {
512
+ it('diffs the two endpoints directly', async () => {
513
+ const spy = vi.spyOn(Utils, 'runGitCommand').mockResolvedValue('src/main.ts\nCHANGELOG.md');
524
514
 
525
- it('should return null when HEAD is the base commit', async () => {
526
- await expect(getReleaseChanges(headSha)).resolves.toBeNull();
527
- expect(gitRunSpy).toHaveBeenCalledOnce();
528
- expect(gitRunSpy).toHaveBeenCalledWith(['rev-parse', 'HEAD'], {
529
- operation: 'identify the current release commit',
515
+ await expect(getChangedFilesBetween('base-sha', 'head-sha')).resolves.toStrictEqual([
516
+ 'src/main.ts',
517
+ 'CHANGELOG.md',
518
+ ]);
519
+ expect(spy).toHaveBeenCalledWith(['diff', '--name-only', 'base-sha..head-sha'], {
520
+ operation: 'get the list of changed files between commits',
530
521
  });
531
522
  });
523
+ });
532
524
 
533
- it('skips the changelog diff when CHANGELOG.md did not change', async () => {
534
- gitRunSpy.mockImplementation(async (args: string[]) => {
535
- if (args[0] === 'rev-parse') return headSha;
536
- if (args[0] === 'log') return `${mergeCommit}\n${mergedBranchCommit}`;
537
- if (args.includes('--name-only')) return 'actors/foo/src/main.ts';
538
- return '';
525
+ describe('resolveCommitSha', () => {
526
+ it('resolves a ref to the commit it points to', async () => {
527
+ const sha = 'a'.repeat(40);
528
+ const spy = vi.spyOn(Utils, 'runGitCommand').mockResolvedValue(sha);
529
+ await expect(resolveCommitSha('origin/feature')).resolves.toBe(sha);
530
+ expect(spy).toHaveBeenCalledWith(['rev-parse', '--verify', 'origin/feature^{commit}'], {
531
+ operation: 'resolve origin/feature to a commit',
539
532
  });
533
+ });
534
+ });
540
535
 
541
- await expect(getReleaseChanges(baseSha)).resolves.toMatchObject({ changelog: null });
542
- expect(gitRunSpy).not.toHaveBeenCalledWith(['diff', baseSha, 'HEAD', '--', 'CHANGELOG.md']);
536
+ describe('resolveBranchName', () => {
537
+ it.each([
538
+ ['a local branch', 'refs/heads/feature/foo', 'feature/foo'],
539
+ ['a remote branch, dropping the remote', 'refs/remotes/origin/feature/foo', 'feature/foo'],
540
+ ['a tag', 'refs/tags/v1.0.0', null],
541
+ ['a detached HEAD', 'HEAD', null],
542
+ ['a SHA', '', null],
543
+ ])('handles %s', async (_, fullName, expected) => {
544
+ vi.spyOn(Utils, 'runGitCommand').mockResolvedValue(fullName);
545
+ await expect(resolveBranchName('some-ref')).resolves.toBe(expected);
543
546
  });
544
547
  });