apify-test-tools 0.8.6 → 0.9.0-beta.0
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.
- package/README.md +83 -13
- package/bin/build-from-local.ts +35 -34
- package/bin/build.ts +55 -59
- package/bin/diff-changes.ts +166 -125
- package/bin/dockerignore.ts +50 -0
- package/bin/main.ts +7 -7
- package/bin/path-utils.ts +13 -0
- package/bin/test-report.ts +5 -5
- package/bin/types.ts +17 -5
- package/bin/utils.ts +124 -95
- package/dist/bin/build-from-local.d.ts +2 -2
- package/dist/bin/build-from-local.d.ts.map +1 -1
- package/dist/bin/build-from-local.js +33 -28
- package/dist/bin/build-from-local.js.map +1 -1
- package/dist/bin/build.d.ts +5 -8
- package/dist/bin/build.d.ts.map +1 -1
- package/dist/bin/build.js +49 -56
- package/dist/bin/build.js.map +1 -1
- package/dist/bin/diff-changes.d.ts +0 -6
- package/dist/bin/diff-changes.d.ts.map +1 -1
- package/dist/bin/diff-changes.js +129 -89
- package/dist/bin/diff-changes.js.map +1 -1
- package/dist/bin/dockerignore.d.ts +17 -0
- package/dist/bin/dockerignore.d.ts.map +1 -0
- package/dist/bin/dockerignore.js +41 -0
- package/dist/bin/dockerignore.js.map +1 -0
- package/dist/bin/main.js +7 -7
- package/dist/bin/main.js.map +1 -1
- package/dist/bin/path-utils.d.ts +4 -0
- package/dist/bin/path-utils.d.ts.map +1 -0
- package/dist/bin/path-utils.js +9 -0
- package/dist/bin/path-utils.js.map +1 -0
- package/dist/bin/test-report.js +3 -3
- package/dist/bin/test-report.js.map +1 -1
- package/dist/bin/types.d.ts +15 -4
- package/dist/bin/types.d.ts.map +1 -1
- package/dist/bin/utils.d.ts +2 -19
- package/dist/bin/utils.d.ts.map +1 -1
- package/dist/bin/utils.js +82 -81
- package/dist/bin/utils.js.map +1 -1
- package/dist/lib/lib.d.ts +10 -4
- package/dist/lib/lib.d.ts.map +1 -1
- package/dist/lib/lib.js +28 -22
- package/dist/lib/lib.js.map +1 -1
- package/dist/lib/types.d.ts +2 -2
- package/dist/lib/types.d.ts.map +1 -1
- package/dist/lib/utils.d.ts +1 -1
- package/dist/lib/utils.d.ts.map +1 -1
- package/dist/lib/utils.js +4 -4
- package/dist/lib/utils.js.map +1 -1
- package/dist/test/unit/bin/build-from-local.test.js +55 -86
- package/dist/test/unit/bin/build-from-local.test.js.map +1 -1
- package/dist/test/unit/bin/diff-changes.test.js +358 -36
- package/dist/test/unit/bin/diff-changes.test.js.map +1 -1
- package/dist/test/unit/bin/dockerignore.test.d.ts +2 -0
- package/dist/test/unit/bin/dockerignore.test.d.ts.map +1 -0
- package/dist/test/unit/bin/dockerignore.test.js +102 -0
- package/dist/test/unit/bin/dockerignore.test.js.map +1 -0
- package/dist/test/unit/bin/path-utils.test.d.ts +2 -0
- package/dist/test/unit/bin/path-utils.test.d.ts.map +1 -0
- package/dist/test/unit/bin/path-utils.test.js +14 -0
- package/dist/test/unit/bin/path-utils.test.js.map +1 -0
- package/dist/test/unit/bin/utils.test.d.ts +2 -0
- package/dist/test/unit/bin/utils.test.d.ts.map +1 -0
- package/dist/test/unit/bin/utils.test.js +292 -0
- package/dist/test/unit/bin/utils.test.js.map +1 -0
- package/dist/test/unit/should-built-and-test.test.js +83 -32
- package/dist/test/unit/should-built-and-test.test.js.map +1 -1
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/lib/lib.ts +28 -22
- package/lib/types.ts +2 -2
- package/lib/utils.ts +4 -4
- package/package.json +62 -61
- package/test/unit/bin/build-from-local.test.ts +62 -121
- package/test/unit/bin/diff-changes.test.ts +397 -41
- package/test/unit/bin/dockerignore.test.ts +123 -0
- package/test/unit/bin/path-utils.test.ts +17 -0
- package/test/unit/bin/utils.test.ts +342 -0
- package/test/unit/should-built-and-test.test.ts +89 -32
|
@@ -1,47 +1,35 @@
|
|
|
1
1
|
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
|
2
2
|
|
|
3
|
-
import { getChangedActors
|
|
3
|
+
import { getChangedActors } from '../../../bin/diff-changes.js';
|
|
4
4
|
import * as DiffJsonSchema from '../../../bin/diff-json-schema.js';
|
|
5
|
+
import * as Dockerignore from '../../../bin/dockerignore.js';
|
|
5
6
|
import type { ActorConfig } from '../../../bin/types.js';
|
|
6
7
|
|
|
7
|
-
const miniActor: ActorConfig = {
|
|
8
|
+
const miniActor: ActorConfig = {
|
|
9
|
+
actorFullName: 'foo/bar',
|
|
10
|
+
folder: 'actors/foo_bar',
|
|
11
|
+
tokenEnvVar: 'APIFY_TOKEN_FOO',
|
|
12
|
+
dockerContextDir: '',
|
|
13
|
+
contextPaths: [''],
|
|
14
|
+
};
|
|
8
15
|
const standaloneActor: ActorConfig = {
|
|
9
|
-
|
|
16
|
+
actorFullName: 'owner/standalone',
|
|
10
17
|
folder: 'standalone-actors/standalone',
|
|
11
|
-
|
|
18
|
+
tokenEnvVar: 'APIFY_TOKEN_OWNER',
|
|
19
|
+
dockerContextDir: 'standalone-actors/standalone',
|
|
20
|
+
contextPaths: ['standalone-actors/standalone'],
|
|
12
21
|
};
|
|
13
22
|
const actorConfigs = [miniActor, standaloneActor];
|
|
23
|
+
const amazonActor: ActorConfig = {
|
|
24
|
+
actorFullName: 'junglee/amazon-crawler',
|
|
25
|
+
folder: 'actors/junglee_Amazon-crawler',
|
|
26
|
+
tokenEnvVar: 'APIFY_TOKEN_JUNGLEE',
|
|
27
|
+
dockerContextDir: '',
|
|
28
|
+
contextPaths: ['actors/junglee_Amazon-crawler', 'code', 'shared'],
|
|
29
|
+
};
|
|
14
30
|
|
|
15
31
|
const commits = [{ sha: 'Commit1', author: '', date: '', message: '' }];
|
|
16
32
|
|
|
17
|
-
describe('maybeParseActorFolder', () => {
|
|
18
|
-
it('returns actorName for actors/ path', () => {
|
|
19
|
-
expect(maybeParseActorFolder('actors/foo_bar/actor.json')).toEqual({
|
|
20
|
-
isActorFolder: true,
|
|
21
|
-
actorName: 'foo/bar',
|
|
22
|
-
});
|
|
23
|
-
});
|
|
24
|
-
|
|
25
|
-
it('returns actorName for standalone-actors/ path', () => {
|
|
26
|
-
expect(maybeParseActorFolder('standalone-actors/my_actor/main.ts')).toEqual({
|
|
27
|
-
isActorFolder: true,
|
|
28
|
-
actorName: 'my/actor',
|
|
29
|
-
});
|
|
30
|
-
});
|
|
31
|
-
|
|
32
|
-
it('returns false for top-level file', () => {
|
|
33
|
-
expect(maybeParseActorFolder('package.json')).toEqual({ isActorFolder: false });
|
|
34
|
-
});
|
|
35
|
-
|
|
36
|
-
it('returns false for path with no file inside actor folder', () => {
|
|
37
|
-
expect(maybeParseActorFolder('actors/foo_bar')).toEqual({ isActorFolder: false });
|
|
38
|
-
});
|
|
39
|
-
|
|
40
|
-
it('returns false for unrelated folder', () => {
|
|
41
|
-
expect(maybeParseActorFolder('src/utils.ts')).toEqual({ isActorFolder: false });
|
|
42
|
-
});
|
|
43
|
-
});
|
|
44
|
-
|
|
45
33
|
describe('getChangedActors', () => {
|
|
46
34
|
beforeEach(() => {
|
|
47
35
|
vi.spyOn(DiffJsonSchema, 'isCosmeticOnlyJsonSchemaChange').mockReturnValue(false);
|
|
@@ -92,7 +80,7 @@ describe('getChangedActors', () => {
|
|
|
92
80
|
it('returns actor when isLatest and JSON file has only cosmetic changes', () => {
|
|
93
81
|
vi.spyOn(DiffJsonSchema, 'isCosmeticOnlyJsonSchemaChange').mockReturnValue(true);
|
|
94
82
|
const result = getChangedActors({
|
|
95
|
-
filepathsChanged: ['actors/foo_bar/actor.json'],
|
|
83
|
+
filepathsChanged: ['actors/foo_bar/.actor/actor.json'],
|
|
96
84
|
actorConfigs,
|
|
97
85
|
commits,
|
|
98
86
|
isLatest: true,
|
|
@@ -103,7 +91,7 @@ describe('getChangedActors', () => {
|
|
|
103
91
|
it('does not return actor when not isLatest and JSON file has only cosmetic changes', () => {
|
|
104
92
|
vi.spyOn(DiffJsonSchema, 'isCosmeticOnlyJsonSchemaChange').mockReturnValue(true);
|
|
105
93
|
const result = getChangedActors({
|
|
106
|
-
filepathsChanged: ['actors/foo_bar/actor.json'],
|
|
94
|
+
filepathsChanged: ['actors/foo_bar/.actor/actor.json'],
|
|
107
95
|
actorConfigs,
|
|
108
96
|
commits,
|
|
109
97
|
isLatest: false,
|
|
@@ -114,14 +102,37 @@ describe('getChangedActors', () => {
|
|
|
114
102
|
it('returns actor when JSON file has functional changes', () => {
|
|
115
103
|
vi.spyOn(DiffJsonSchema, 'isCosmeticOnlyJsonSchemaChange').mockReturnValue(false);
|
|
116
104
|
const result = getChangedActors({
|
|
117
|
-
filepathsChanged: ['actors/foo_bar/actor.json'],
|
|
105
|
+
filepathsChanged: ['actors/foo_bar/.actor/actor.json'],
|
|
106
|
+
actorConfigs,
|
|
107
|
+
commits,
|
|
108
|
+
});
|
|
109
|
+
expect(result).toEqual([miniActor]);
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
it('JSON file in actor folder but outside .actor/ is functional, not checked for cosmetic', () => {
|
|
113
|
+
vi.spyOn(DiffJsonSchema, 'isCosmeticOnlyJsonSchemaChange').mockReturnValue(true);
|
|
114
|
+
const result = getChangedActors({
|
|
115
|
+
filepathsChanged: ['actors/foo_bar/package.json'],
|
|
118
116
|
actorConfigs,
|
|
119
117
|
commits,
|
|
118
|
+
isLatest: false,
|
|
119
|
+
});
|
|
120
|
+
expect(result).toEqual([miniActor]);
|
|
121
|
+
expect(DiffJsonSchema.isCosmeticOnlyJsonSchemaChange).not.toHaveBeenCalled();
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
it('JSON file under .actor/ inside actor folder is checked for cosmetic changes', () => {
|
|
125
|
+
vi.spyOn(DiffJsonSchema, 'isCosmeticOnlyJsonSchemaChange').mockReturnValue(true);
|
|
126
|
+
const result = getChangedActors({
|
|
127
|
+
filepathsChanged: ['actors/foo_bar/.actor/input_schema.json'],
|
|
128
|
+
actorConfigs,
|
|
129
|
+
commits,
|
|
130
|
+
isLatest: true,
|
|
120
131
|
});
|
|
121
132
|
expect(result).toEqual([miniActor]);
|
|
122
133
|
});
|
|
123
134
|
|
|
124
|
-
it('
|
|
135
|
+
it('does not trigger narrow-context actor when shared file changes', () => {
|
|
125
136
|
const result = getChangedActors({
|
|
126
137
|
filepathsChanged: ['shared/utils.ts'],
|
|
127
138
|
actorConfigs,
|
|
@@ -131,18 +142,28 @@ describe('getChangedActors', () => {
|
|
|
131
142
|
expect(result).not.toContainEqual(standaloneActor);
|
|
132
143
|
});
|
|
133
144
|
|
|
134
|
-
it('
|
|
145
|
+
it('root-level changelog outside any actor folder is cosmetic for every actor when isLatest', () => {
|
|
135
146
|
const result = getChangedActors({
|
|
136
147
|
filepathsChanged: ['CHANGELOG.md'],
|
|
137
148
|
actorConfigs,
|
|
138
149
|
commits,
|
|
139
150
|
isLatest: true,
|
|
140
151
|
});
|
|
141
|
-
expect(result).
|
|
142
|
-
expect(result).
|
|
152
|
+
expect(result).toEqual(expect.arrayContaining([miniActor, standaloneActor]));
|
|
153
|
+
expect(result).toHaveLength(2);
|
|
143
154
|
});
|
|
144
155
|
|
|
145
|
-
it('
|
|
156
|
+
it('root-level changelog is not cosmetic-triggered when not isLatest', () => {
|
|
157
|
+
const result = getChangedActors({
|
|
158
|
+
filepathsChanged: ['CHANGELOG.md'],
|
|
159
|
+
actorConfigs,
|
|
160
|
+
commits,
|
|
161
|
+
isLatest: false,
|
|
162
|
+
});
|
|
163
|
+
expect(result).toEqual([]);
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
it('triggers narrow-context actor when its own folder changes', () => {
|
|
146
167
|
const result = getChangedActors({
|
|
147
168
|
filepathsChanged: ['standalone-actors/standalone/src/main.ts'],
|
|
148
169
|
actorConfigs,
|
|
@@ -161,7 +182,7 @@ describe('getChangedActors', () => {
|
|
|
161
182
|
expect(result).toContainEqual(miniActor);
|
|
162
183
|
});
|
|
163
184
|
|
|
164
|
-
it('handles mixed changes: returns both
|
|
185
|
+
it('handles mixed changes: returns both broad and narrow-context actors', () => {
|
|
165
186
|
const result = getChangedActors({
|
|
166
187
|
filepathsChanged: ['actors/foo_bar/src/main.ts', 'standalone-actors/standalone/Dockerfile'],
|
|
167
188
|
actorConfigs,
|
|
@@ -171,6 +192,47 @@ describe('getChangedActors', () => {
|
|
|
171
192
|
expect(result).toContainEqual(standaloneActor);
|
|
172
193
|
});
|
|
173
194
|
|
|
195
|
+
it('matches folder where folder name differs from actor name', () => {
|
|
196
|
+
const ownerlessActor: ActorConfig = {
|
|
197
|
+
actorFullName: 'myteam/shopify-scraper',
|
|
198
|
+
folder: 'actors/shopify',
|
|
199
|
+
tokenEnvVar: 'APIFY_TOKEN_MYTEAM',
|
|
200
|
+
dockerContextDir: '',
|
|
201
|
+
contextPaths: [''],
|
|
202
|
+
};
|
|
203
|
+
const result = getChangedActors({
|
|
204
|
+
filepathsChanged: ['actors/shopify/src/main.ts'],
|
|
205
|
+
actorConfigs: [ownerlessActor],
|
|
206
|
+
commits,
|
|
207
|
+
});
|
|
208
|
+
expect(result).toEqual([ownerlessActor]);
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
it('in single-actor repo, .actor/ changes trigger builds', () => {
|
|
212
|
+
const rootActor: ActorConfig = {
|
|
213
|
+
actorFullName: 'myteam/my-actor',
|
|
214
|
+
folder: '',
|
|
215
|
+
tokenEnvVar: 'BUILDER_APIFY_TOKEN',
|
|
216
|
+
dockerContextDir: '',
|
|
217
|
+
contextPaths: [''],
|
|
218
|
+
};
|
|
219
|
+
const result = getChangedActors({
|
|
220
|
+
filepathsChanged: ['.actor/actor.json'],
|
|
221
|
+
actorConfigs: [rootActor],
|
|
222
|
+
commits,
|
|
223
|
+
});
|
|
224
|
+
expect(result).toEqual([rootActor]);
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
it('in multi-actor repo, .actor/ changes only trigger broad-context actors', () => {
|
|
228
|
+
const result = getChangedActors({
|
|
229
|
+
filepathsChanged: ['.actor/actor.json'],
|
|
230
|
+
actorConfigs,
|
|
231
|
+
commits,
|
|
232
|
+
});
|
|
233
|
+
expect(result).toEqual([miniActor]);
|
|
234
|
+
});
|
|
235
|
+
|
|
174
236
|
it('file paths are matched case-insensitively', () => {
|
|
175
237
|
const result = getChangedActors({
|
|
176
238
|
filepathsChanged: ['Actors/FOO_BAR/Main.ts'],
|
|
@@ -179,4 +241,298 @@ describe('getChangedActors', () => {
|
|
|
179
241
|
});
|
|
180
242
|
expect(result).toEqual([miniActor]);
|
|
181
243
|
});
|
|
244
|
+
|
|
245
|
+
it('triggers actor with contextPaths override when file matches an override path', () => {
|
|
246
|
+
const overrideActor: ActorConfig = {
|
|
247
|
+
actorFullName: 'team/override-actor',
|
|
248
|
+
folder: 'actors/override',
|
|
249
|
+
tokenEnvVar: 'APIFY_TOKEN_TEAM',
|
|
250
|
+
dockerContextDir: 'actors/override',
|
|
251
|
+
contextPaths: ['actors/override', 'packages'],
|
|
252
|
+
};
|
|
253
|
+
const result = getChangedActors({
|
|
254
|
+
filepathsChanged: ['packages/shared/utils.ts'],
|
|
255
|
+
actorConfigs: [overrideActor],
|
|
256
|
+
commits,
|
|
257
|
+
});
|
|
258
|
+
expect(result).toEqual([overrideActor]);
|
|
259
|
+
});
|
|
260
|
+
|
|
261
|
+
it('does not trigger actor with contextPaths override when file is outside all override paths', () => {
|
|
262
|
+
const overrideActor: ActorConfig = {
|
|
263
|
+
actorFullName: 'team/override-actor',
|
|
264
|
+
folder: 'actors/override',
|
|
265
|
+
tokenEnvVar: 'APIFY_TOKEN_TEAM',
|
|
266
|
+
dockerContextDir: 'actors/override',
|
|
267
|
+
contextPaths: ['actors/override', 'packages'],
|
|
268
|
+
};
|
|
269
|
+
const result = getChangedActors({
|
|
270
|
+
filepathsChanged: ['other-dir/file.ts'],
|
|
271
|
+
actorConfigs: [overrideActor],
|
|
272
|
+
commits,
|
|
273
|
+
});
|
|
274
|
+
expect(result).toEqual([]);
|
|
275
|
+
});
|
|
276
|
+
|
|
277
|
+
it('broad-context actor skips files in sibling actor folders', () => {
|
|
278
|
+
const actorA: ActorConfig = {
|
|
279
|
+
actorFullName: 'team/actor-a',
|
|
280
|
+
folder: 'actors/a',
|
|
281
|
+
tokenEnvVar: 'APIFY_TOKEN_TEAM',
|
|
282
|
+
dockerContextDir: '',
|
|
283
|
+
contextPaths: [''],
|
|
284
|
+
};
|
|
285
|
+
const actorB: ActorConfig = {
|
|
286
|
+
actorFullName: 'team/actor-b',
|
|
287
|
+
folder: 'actors/b',
|
|
288
|
+
tokenEnvVar: 'APIFY_TOKEN_TEAM',
|
|
289
|
+
dockerContextDir: '',
|
|
290
|
+
contextPaths: [''],
|
|
291
|
+
};
|
|
292
|
+
const result = getChangedActors({
|
|
293
|
+
filepathsChanged: ['actors/b/src/main.ts'],
|
|
294
|
+
actorConfigs: [actorA, actorB],
|
|
295
|
+
commits,
|
|
296
|
+
});
|
|
297
|
+
expect(result).toEqual([actorB]);
|
|
298
|
+
});
|
|
299
|
+
|
|
300
|
+
it('root actor (folder="") is excluded from sibling actor folder files', () => {
|
|
301
|
+
const rootActor: ActorConfig = {
|
|
302
|
+
actorFullName: 'team/root',
|
|
303
|
+
folder: '',
|
|
304
|
+
tokenEnvVar: 'APIFY_TOKEN_TEAM',
|
|
305
|
+
dockerContextDir: '',
|
|
306
|
+
contextPaths: [''],
|
|
307
|
+
};
|
|
308
|
+
const childActor: ActorConfig = {
|
|
309
|
+
actorFullName: 'team/child',
|
|
310
|
+
folder: 'actors/child',
|
|
311
|
+
tokenEnvVar: 'APIFY_TOKEN_TEAM',
|
|
312
|
+
dockerContextDir: 'actors/child',
|
|
313
|
+
contextPaths: ['actors/child'],
|
|
314
|
+
};
|
|
315
|
+
const result = getChangedActors({
|
|
316
|
+
filepathsChanged: ['actors/child/src/main.ts'],
|
|
317
|
+
actorConfigs: [rootActor, childActor],
|
|
318
|
+
commits,
|
|
319
|
+
});
|
|
320
|
+
expect(result).not.toContainEqual(rootActor);
|
|
321
|
+
expect(result).toContainEqual(childActor);
|
|
322
|
+
});
|
|
323
|
+
|
|
324
|
+
it('root actor (folder="") sees files outside any actor folder', () => {
|
|
325
|
+
const rootActor: ActorConfig = {
|
|
326
|
+
actorFullName: 'team/root',
|
|
327
|
+
folder: '',
|
|
328
|
+
tokenEnvVar: 'APIFY_TOKEN_TEAM',
|
|
329
|
+
dockerContextDir: '',
|
|
330
|
+
contextPaths: [''],
|
|
331
|
+
};
|
|
332
|
+
const childActor: ActorConfig = {
|
|
333
|
+
actorFullName: 'team/child',
|
|
334
|
+
folder: 'actors/child',
|
|
335
|
+
tokenEnvVar: 'APIFY_TOKEN_TEAM',
|
|
336
|
+
dockerContextDir: 'actors/child',
|
|
337
|
+
contextPaths: ['actors/child'],
|
|
338
|
+
};
|
|
339
|
+
const result = getChangedActors({
|
|
340
|
+
filepathsChanged: ['lib/shared-utils.ts'],
|
|
341
|
+
actorConfigs: [rootActor, childActor],
|
|
342
|
+
commits,
|
|
343
|
+
});
|
|
344
|
+
expect(result).toContainEqual(rootActor);
|
|
345
|
+
expect(result).not.toContainEqual(childActor);
|
|
346
|
+
});
|
|
347
|
+
|
|
348
|
+
it('file matched by .dockerignore is treated as ignored', () => {
|
|
349
|
+
vi.spyOn(Dockerignore, 'loadDockerIgnore').mockReturnValue(
|
|
350
|
+
(filePath) => filePath === 'actors/foo_bar/node_modules/foo.js',
|
|
351
|
+
);
|
|
352
|
+
const result = getChangedActors({
|
|
353
|
+
filepathsChanged: ['actors/foo_bar/node_modules/foo.js'],
|
|
354
|
+
actorConfigs: [miniActor],
|
|
355
|
+
commits,
|
|
356
|
+
});
|
|
357
|
+
expect(result).toEqual([]);
|
|
358
|
+
});
|
|
359
|
+
|
|
360
|
+
it('file not matched by .dockerignore is classified normally', () => {
|
|
361
|
+
vi.spyOn(Dockerignore, 'loadDockerIgnore').mockReturnValue((filePath) => filePath.includes('node_modules'));
|
|
362
|
+
const result = getChangedActors({
|
|
363
|
+
filepathsChanged: ['actors/foo_bar/src/main.ts'],
|
|
364
|
+
actorConfigs: [miniActor],
|
|
365
|
+
commits,
|
|
366
|
+
});
|
|
367
|
+
expect(result).toEqual([miniActor]);
|
|
368
|
+
});
|
|
369
|
+
|
|
370
|
+
it('JSON file in context but outside actor folder is functional (not checked for cosmetic)', () => {
|
|
371
|
+
vi.spyOn(DiffJsonSchema, 'isCosmeticOnlyJsonSchemaChange').mockReturnValue(true);
|
|
372
|
+
const result = getChangedActors({
|
|
373
|
+
filepathsChanged: ['lib/config.json'],
|
|
374
|
+
actorConfigs: [miniActor],
|
|
375
|
+
commits,
|
|
376
|
+
});
|
|
377
|
+
expect(result).toEqual([miniActor]);
|
|
378
|
+
});
|
|
379
|
+
|
|
380
|
+
it('README outside actor folder but inside context is ignored (not cosmetic)', () => {
|
|
381
|
+
const result = getChangedActors({
|
|
382
|
+
filepathsChanged: ['docs/README.md'],
|
|
383
|
+
actorConfigs: [miniActor],
|
|
384
|
+
commits,
|
|
385
|
+
isLatest: true,
|
|
386
|
+
});
|
|
387
|
+
expect(result).toEqual([]);
|
|
388
|
+
});
|
|
389
|
+
|
|
390
|
+
it('README inside actor folder is cosmetic', () => {
|
|
391
|
+
const result = getChangedActors({
|
|
392
|
+
filepathsChanged: ['actors/foo_bar/README.md'],
|
|
393
|
+
actorConfigs: [miniActor],
|
|
394
|
+
commits,
|
|
395
|
+
isLatest: true,
|
|
396
|
+
});
|
|
397
|
+
expect(result).toEqual([miniActor]);
|
|
398
|
+
});
|
|
399
|
+
|
|
400
|
+
it('hoists a standalone actor own top-level dev file relative to its context before checking the ignore list', () => {
|
|
401
|
+
const result = getChangedActors({
|
|
402
|
+
filepathsChanged: ['standalone-actors/standalone/.eslintrc'],
|
|
403
|
+
actorConfigs,
|
|
404
|
+
commits,
|
|
405
|
+
});
|
|
406
|
+
expect(result).toEqual([]);
|
|
407
|
+
});
|
|
408
|
+
|
|
409
|
+
it('does not special-case code/ and shared/ prefixes anymore — must be declared via overrideActorContext', () => {
|
|
410
|
+
const result = getChangedActors({
|
|
411
|
+
filepathsChanged: ['code/.eslintrc'],
|
|
412
|
+
actorConfigs: [miniActor],
|
|
413
|
+
commits,
|
|
414
|
+
});
|
|
415
|
+
expect(result).toEqual([miniActor]);
|
|
416
|
+
});
|
|
417
|
+
|
|
418
|
+
it('ignores code/.eslintrc when "code" is declared via overrideActorContext', () => {
|
|
419
|
+
const result = getChangedActors({
|
|
420
|
+
filepathsChanged: ['code/.eslintrc'],
|
|
421
|
+
actorConfigs: [amazonActor],
|
|
422
|
+
commits,
|
|
423
|
+
});
|
|
424
|
+
expect(result).toEqual([]);
|
|
425
|
+
});
|
|
426
|
+
|
|
427
|
+
it('shared/Dockerfile declared via overrideActorContext is functional', () => {
|
|
428
|
+
const result = getChangedActors({
|
|
429
|
+
filepathsChanged: ['shared/Dockerfile'],
|
|
430
|
+
actorConfigs: [amazonActor],
|
|
431
|
+
commits,
|
|
432
|
+
});
|
|
433
|
+
expect(result).toEqual([amazonActor]);
|
|
434
|
+
});
|
|
435
|
+
|
|
436
|
+
it('code/README.md declared via overrideActorContext is ignored (outside actor folder)', () => {
|
|
437
|
+
const result = getChangedActors({
|
|
438
|
+
filepathsChanged: ['code/README.md'],
|
|
439
|
+
actorConfigs: [amazonActor],
|
|
440
|
+
commits,
|
|
441
|
+
isLatest: true,
|
|
442
|
+
});
|
|
443
|
+
expect(result).toEqual([]);
|
|
444
|
+
});
|
|
445
|
+
});
|
|
446
|
+
|
|
447
|
+
describe('getChangedActors logging', () => {
|
|
448
|
+
beforeEach(() => {
|
|
449
|
+
vi.spyOn(DiffJsonSchema, 'isCosmeticOnlyJsonSchemaChange').mockReturnValue(false);
|
|
450
|
+
vi.spyOn(console, 'error').mockImplementation(() => undefined);
|
|
451
|
+
});
|
|
452
|
+
|
|
453
|
+
it('logs a single "specific" group for a single actor with one functional file', () => {
|
|
454
|
+
getChangedActors({
|
|
455
|
+
filepathsChanged: ['actors/foo_bar/src/main.ts'],
|
|
456
|
+
actorConfigs: [miniActor],
|
|
457
|
+
commits,
|
|
458
|
+
});
|
|
459
|
+
|
|
460
|
+
expect(console.error).toHaveBeenCalledWith(
|
|
461
|
+
'[DIFF]: Changes specific to actor foo/bar: actors/foo_bar/src/main.ts',
|
|
462
|
+
);
|
|
463
|
+
expect(console.error).toHaveBeenCalledWith('[DIFF]: Actors to be built and tested: foo/bar');
|
|
464
|
+
});
|
|
465
|
+
|
|
466
|
+
it('logs a single "shared" group when two actors are triggered by the exact same file', () => {
|
|
467
|
+
const actorA: ActorConfig = {
|
|
468
|
+
actorFullName: 'team/actor-a',
|
|
469
|
+
folder: 'actors/a',
|
|
470
|
+
tokenEnvVar: 'APIFY_TOKEN_TEAM',
|
|
471
|
+
dockerContextDir: '',
|
|
472
|
+
contextPaths: ['', 'shared'],
|
|
473
|
+
};
|
|
474
|
+
const actorB: ActorConfig = {
|
|
475
|
+
actorFullName: 'team/actor-b',
|
|
476
|
+
folder: 'actors/b',
|
|
477
|
+
tokenEnvVar: 'APIFY_TOKEN_TEAM',
|
|
478
|
+
dockerContextDir: '',
|
|
479
|
+
contextPaths: ['', 'shared'],
|
|
480
|
+
};
|
|
481
|
+
|
|
482
|
+
getChangedActors({
|
|
483
|
+
filepathsChanged: ['shared/shared.ts'],
|
|
484
|
+
actorConfigs: [actorA, actorB],
|
|
485
|
+
commits,
|
|
486
|
+
});
|
|
487
|
+
|
|
488
|
+
expect(console.error).toHaveBeenCalledWith(
|
|
489
|
+
'[DIFF]: Shared changes for actors team/actor-a, team/actor-b: shared/shared.ts',
|
|
490
|
+
);
|
|
491
|
+
expect(console.error).not.toHaveBeenCalledWith(expect.stringContaining('Changes specific to actor'));
|
|
492
|
+
expect(console.error).toHaveBeenCalledWith('[DIFF]: Actors to be built and tested: team/actor-a, team/actor-b');
|
|
493
|
+
});
|
|
494
|
+
|
|
495
|
+
it('logs shared and specific groups in descending-size order for partial overlap across actors', () => {
|
|
496
|
+
const actorA: ActorConfig = {
|
|
497
|
+
actorFullName: 'team/actor-a',
|
|
498
|
+
folder: 'actors/a',
|
|
499
|
+
tokenEnvVar: 'APIFY_TOKEN_TEAM',
|
|
500
|
+
dockerContextDir: '',
|
|
501
|
+
contextPaths: [''],
|
|
502
|
+
};
|
|
503
|
+
const actorB: ActorConfig = {
|
|
504
|
+
actorFullName: 'team/actor-b',
|
|
505
|
+
folder: 'actors/b',
|
|
506
|
+
tokenEnvVar: 'APIFY_TOKEN_TEAM',
|
|
507
|
+
dockerContextDir: '',
|
|
508
|
+
contextPaths: [''],
|
|
509
|
+
};
|
|
510
|
+
|
|
511
|
+
getChangedActors({
|
|
512
|
+
filepathsChanged: ['shared.ts', 'actors/a/a-only.ts', 'actors/b/b-only.ts'],
|
|
513
|
+
actorConfigs: [actorA, actorB],
|
|
514
|
+
commits,
|
|
515
|
+
});
|
|
516
|
+
|
|
517
|
+
const errorCalls = (console.error as unknown as ReturnType<typeof vi.fn>).mock.calls.map((call) => call[0]);
|
|
518
|
+
|
|
519
|
+
expect(errorCalls).toEqual([
|
|
520
|
+
'[DIFF]: Shared changes for actors team/actor-a, team/actor-b: shared.ts',
|
|
521
|
+
'[DIFF]: Changes specific to actor team/actor-a: actors/a/a-only.ts',
|
|
522
|
+
'[DIFF]: Changes specific to actor team/actor-b: actors/b/b-only.ts',
|
|
523
|
+
'[DIFF]: Actors to be built and tested: team/actor-a, team/actor-b',
|
|
524
|
+
]);
|
|
525
|
+
});
|
|
526
|
+
|
|
527
|
+
it('logs no group lines when zero actors changed', () => {
|
|
528
|
+
getChangedActors({
|
|
529
|
+
filepathsChanged: ['.gitignore', 'README.md'],
|
|
530
|
+
actorConfigs,
|
|
531
|
+
commits,
|
|
532
|
+
});
|
|
533
|
+
|
|
534
|
+
expect(console.error).not.toHaveBeenCalledWith(expect.stringContaining('Shared changes'));
|
|
535
|
+
expect(console.error).not.toHaveBeenCalledWith(expect.stringContaining('Changes specific to'));
|
|
536
|
+
expect(console.error).toHaveBeenCalledWith('[DIFF]: No relevant files changed, skipping builds and tests');
|
|
537
|
+
});
|
|
182
538
|
});
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
|
|
3
|
+
import { afterEach, describe, expect, it, vi } from 'vitest';
|
|
4
|
+
|
|
5
|
+
import { buildDockerIgnoreMatcher, loadDockerIgnore } from '../../../bin/dockerignore.js';
|
|
6
|
+
|
|
7
|
+
const { fsMock } = vi.hoisted(() => ({
|
|
8
|
+
fsMock: {
|
|
9
|
+
readFileSync: vi.fn(),
|
|
10
|
+
},
|
|
11
|
+
}));
|
|
12
|
+
|
|
13
|
+
vi.mock('node:fs', () => ({ default: fsMock }));
|
|
14
|
+
|
|
15
|
+
afterEach(() => vi.restoreAllMocks());
|
|
16
|
+
|
|
17
|
+
describe('loadDockerIgnore', () => {
|
|
18
|
+
it('returns no-op matcher when .dockerignore is absent', () => {
|
|
19
|
+
fsMock.readFileSync.mockImplementation(() => {
|
|
20
|
+
throw new Error('ENOENT');
|
|
21
|
+
});
|
|
22
|
+
const matcher = loadDockerIgnore('actors/my-actor');
|
|
23
|
+
expect(matcher('actors/my-actor/src/main.ts')).toBe(false);
|
|
24
|
+
expect(matcher('actors/my-actor/node_modules/foo.js')).toBe(false);
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
it('matches files listed in .dockerignore', () => {
|
|
28
|
+
fsMock.readFileSync.mockReturnValue('node_modules\n*.log\n');
|
|
29
|
+
const matcher = loadDockerIgnore('');
|
|
30
|
+
expect(matcher('node_modules/foo/bar.js')).toBe(true);
|
|
31
|
+
expect(matcher('debug.log')).toBe(true);
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
it('does not match files not in .dockerignore', () => {
|
|
35
|
+
fsMock.readFileSync.mockReturnValue('node_modules\n');
|
|
36
|
+
const matcher = loadDockerIgnore('');
|
|
37
|
+
expect(matcher('src/main.ts')).toBe(false);
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
it.each(['dist', 'dist/'])('handles directory pattern "%s"', (pattern) => {
|
|
41
|
+
fsMock.readFileSync.mockReturnValue(`${pattern}\n`);
|
|
42
|
+
const matcher = loadDockerIgnore('');
|
|
43
|
+
expect(matcher('dist/bundle.js')).toBe(true);
|
|
44
|
+
expect(matcher('src/dist-utils.ts')).toBe(false);
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
it('handles negation patterns', () => {
|
|
48
|
+
fsMock.readFileSync.mockReturnValue('*.log\n!important.log\n');
|
|
49
|
+
const matcher = loadDockerIgnore('');
|
|
50
|
+
expect(matcher('debug.log')).toBe(true);
|
|
51
|
+
expect(matcher('important.log')).toBe(false);
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
it('strips dockerContextDir prefix before matching', () => {
|
|
55
|
+
fsMock.readFileSync.mockReturnValue('node_modules\n');
|
|
56
|
+
const matcher = loadDockerIgnore('actors/shopify');
|
|
57
|
+
expect(matcher('actors/shopify/node_modules/foo.js')).toBe(true);
|
|
58
|
+
expect(matcher('actors/shopify/src/main.ts')).toBe(false);
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
it('returns false for files outside dockerContextDir', () => {
|
|
62
|
+
fsMock.readFileSync.mockReturnValue('*\n');
|
|
63
|
+
const matcher = loadDockerIgnore('actors/shopify');
|
|
64
|
+
expect(matcher('other-actor/src/main.ts')).toBe(false);
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
it('reads .dockerignore from the resolved, absolute dockerContextDir root', () => {
|
|
68
|
+
fsMock.readFileSync.mockReturnValue('');
|
|
69
|
+
loadDockerIgnore('actors/shopify');
|
|
70
|
+
expect(fsMock.readFileSync).toHaveBeenCalledWith(
|
|
71
|
+
path.join(path.resolve('actors/shopify'), '.dockerignore'),
|
|
72
|
+
'utf-8',
|
|
73
|
+
);
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
it('reads .dockerignore from the resolved repo root when dockerContextDir is empty', () => {
|
|
77
|
+
fsMock.readFileSync.mockReturnValue('');
|
|
78
|
+
loadDockerIgnore('');
|
|
79
|
+
expect(fsMock.readFileSync).toHaveBeenCalledWith(path.join(path.resolve(''), '.dockerignore'), 'utf-8');
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
it('handles comments and blank lines', () => {
|
|
83
|
+
fsMock.readFileSync.mockReturnValue('# this is a comment\n\nnode_modules\n');
|
|
84
|
+
const matcher = loadDockerIgnore('');
|
|
85
|
+
expect(matcher('node_modules/foo.js')).toBe(true);
|
|
86
|
+
expect(matcher('src/main.ts')).toBe(false);
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
it('normalizes a leading "./" in patterns, which Docker treats as a no-op', () => {
|
|
90
|
+
// The common real-world .dockerignore style: every pattern prefixed with "./",
|
|
91
|
+
// including a negation re-including one of the otherwise-matched files.
|
|
92
|
+
fsMock.readFileSync.mockReturnValue('./node_modules\n./*.log\n!./keep.log\n');
|
|
93
|
+
const matcher = loadDockerIgnore('');
|
|
94
|
+
expect(matcher('node_modules/foo.js')).toBe(true);
|
|
95
|
+
expect(matcher('debug.log')).toBe(true);
|
|
96
|
+
expect(matcher('keep.log')).toBe(false);
|
|
97
|
+
expect(matcher('main.js')).toBe(false);
|
|
98
|
+
});
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
describe('buildDockerIgnoreMatcher', () => {
|
|
102
|
+
it('treats paths as already relative to absoluteRootDir when hoistFrom is left at its default', () => {
|
|
103
|
+
fsMock.readFileSync.mockReturnValue('test/\n*.log\n');
|
|
104
|
+
const matcher = buildDockerIgnoreMatcher('/repo/actors/shopify');
|
|
105
|
+
expect(matcher('test/fixture.json')).toBe(true);
|
|
106
|
+
expect(matcher('debug.log')).toBe(true);
|
|
107
|
+
expect(matcher('main.js')).toBe(false);
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
it('reads .dockerignore from the given absoluteRootDir', () => {
|
|
111
|
+
fsMock.readFileSync.mockReturnValue('');
|
|
112
|
+
buildDockerIgnoreMatcher('/repo/actors/shopify');
|
|
113
|
+
expect(fsMock.readFileSync).toHaveBeenCalledWith('/repo/actors/shopify/.dockerignore', 'utf-8');
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
it('returns no-op matcher when .dockerignore is absent', () => {
|
|
117
|
+
fsMock.readFileSync.mockImplementation(() => {
|
|
118
|
+
throw new Error('ENOENT');
|
|
119
|
+
});
|
|
120
|
+
const matcher = buildDockerIgnoreMatcher('/repo/actors/shopify');
|
|
121
|
+
expect(matcher('main.js')).toBe(false);
|
|
122
|
+
});
|
|
123
|
+
});
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
|
|
3
|
+
import { findContainingScope, hoistPath, isPathWithinScope } from '../../../bin/path-utils.js';
|
|
4
|
+
|
|
5
|
+
describe('path-utils', () => {
|
|
6
|
+
it('isPathWithinScope matches a scope entry that names an exact file, not just a directory prefix', () => {
|
|
7
|
+
expect(isPathWithinScope('shared/utils.ts', 'shared/utils.ts')).toBe(true);
|
|
8
|
+
});
|
|
9
|
+
|
|
10
|
+
it('findContainingScope picks the exact-file entry out of a list of scope paths', () => {
|
|
11
|
+
expect(findContainingScope('shared/utils.ts', ['actors/foo', 'shared/utils.ts'])).toBe('shared/utils.ts');
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
it('hoistPath resolves an exact-file scope match to an empty string, not an out-of-bounds slice', () => {
|
|
15
|
+
expect(hoistPath('shared/utils.ts', 'shared/utils.ts')).toBe('');
|
|
16
|
+
});
|
|
17
|
+
});
|