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

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 (187) hide show
  1. package/.gitattributes +2 -0
  2. package/.github/actions/checkout-restore-dependencies/action.yaml +64 -27
  3. package/.github/cli/apify-test-tools.mjs +360 -0
  4. package/.github/scripts/check-major-tag-refs.mjs +1 -1
  5. package/.github/scripts/mask-config-secrets.sh +17 -0
  6. package/.github/scripts/run-with-apify-tokens.mjs +37 -12
  7. package/.github/workflows/_check_code.yaml +25 -0
  8. package/.github/workflows/_move_major_tag.yaml +3 -81
  9. package/.github/workflows/_tests.yaml +34 -0
  10. package/.github/workflows/_update_release_metadata.yaml +0 -12
  11. package/.github/workflows/manual_move_major_tag.yaml +3 -3
  12. package/.github/workflows/manual_release_stable.yaml +3 -13
  13. package/.github/workflows/on_master.yaml +9 -5
  14. package/.github/workflows/on_pull_request.yaml +3 -48
  15. package/.github/workflows/public_platform-tests-claude-investigate-and-fix.yaml +13 -2
  16. package/.github/workflows/public_platform-tests.yaml +24 -1
  17. package/.github/workflows/public_pr-build-test.yaml +61 -25
  18. package/.github/workflows/public_push-build-latest.yaml +41 -8
  19. package/.husky/pre-commit +5 -0
  20. package/.prettierignore +3 -0
  21. package/CHANGELOG.md +16 -0
  22. package/CONTRIBUTING.md +105 -21
  23. package/README.md +218 -600
  24. package/bin/affected-actors.ts +198 -0
  25. package/bin/build-from-local.ts +65 -13
  26. package/bin/build.ts +123 -41
  27. package/bin/diff-changes.ts +32 -23
  28. package/bin/diff-json-schema.ts +5 -6
  29. package/bin/git.ts +69 -99
  30. package/bin/main.ts +96 -135
  31. package/bin/slack.ts +43 -10
  32. package/bin/stdin-input.ts +65 -0
  33. package/bin/test-report.ts +6 -9
  34. package/bin/types.ts +15 -0
  35. package/dist/bin/affected-actors.d.ts +46 -0
  36. package/dist/bin/affected-actors.d.ts.map +1 -0
  37. package/dist/bin/affected-actors.js +135 -0
  38. package/dist/bin/affected-actors.js.map +1 -0
  39. package/dist/bin/build-from-local.d.ts +12 -3
  40. package/dist/bin/build-from-local.d.ts.map +1 -1
  41. package/dist/bin/build-from-local.js +34 -12
  42. package/dist/bin/build-from-local.js.map +1 -1
  43. package/dist/bin/build.d.ts +30 -7
  44. package/dist/bin/build.d.ts.map +1 -1
  45. package/dist/bin/build.js +85 -26
  46. package/dist/bin/build.js.map +1 -1
  47. package/dist/bin/diff-changes.d.ts +11 -4
  48. package/dist/bin/diff-changes.d.ts.map +1 -1
  49. package/dist/bin/diff-changes.js +19 -19
  50. package/dist/bin/diff-changes.js.map +1 -1
  51. package/dist/bin/diff-json-schema.d.ts +2 -2
  52. package/dist/bin/diff-json-schema.d.ts.map +1 -1
  53. package/dist/bin/diff-json-schema.js +1 -4
  54. package/dist/bin/diff-json-schema.js.map +1 -1
  55. package/dist/bin/git.d.ts +23 -21
  56. package/dist/bin/git.d.ts.map +1 -1
  57. package/dist/bin/git.js +60 -83
  58. package/dist/bin/git.js.map +1 -1
  59. package/dist/bin/main.js +82 -105
  60. package/dist/bin/main.js.map +1 -1
  61. package/dist/bin/slack.d.ts +14 -4
  62. package/dist/bin/slack.d.ts.map +1 -1
  63. package/dist/bin/slack.js +39 -6
  64. package/dist/bin/slack.js.map +1 -1
  65. package/dist/bin/stdin-input.d.ts +17 -0
  66. package/dist/bin/stdin-input.d.ts.map +1 -0
  67. package/dist/bin/stdin-input.js +54 -0
  68. package/dist/bin/stdin-input.js.map +1 -0
  69. package/dist/bin/test-report.d.ts.map +1 -1
  70. package/dist/bin/test-report.js +6 -8
  71. package/dist/bin/test-report.js.map +1 -1
  72. package/dist/bin/types.d.ts +13 -0
  73. package/dist/bin/types.d.ts.map +1 -1
  74. package/dist/lib/extend-expect.d.ts.map +1 -1
  75. package/dist/lib/extend-expect.js +37 -218
  76. package/dist/lib/extend-expect.js.map +1 -1
  77. package/dist/lib/lib.d.ts +1 -2
  78. package/dist/lib/lib.d.ts.map +1 -1
  79. package/dist/lib/lib.js +15 -22
  80. package/dist/lib/lib.js.map +1 -1
  81. package/dist/lib/run-checks.d.ts +14 -0
  82. package/dist/lib/run-checks.d.ts.map +1 -0
  83. package/dist/lib/run-checks.js +82 -0
  84. package/dist/lib/run-checks.js.map +1 -0
  85. package/dist/lib/run-test-result.d.ts +3 -6
  86. package/dist/lib/run-test-result.d.ts.map +1 -1
  87. package/dist/lib/run-test-result.js +41 -54
  88. package/dist/lib/run-test-result.js.map +1 -1
  89. package/dist/test/bundle/cli-bundle.test.d.ts +2 -0
  90. package/dist/test/bundle/cli-bundle.test.d.ts.map +1 -0
  91. package/dist/test/bundle/cli-bundle.test.js +190 -0
  92. package/dist/test/bundle/cli-bundle.test.js.map +1 -0
  93. package/dist/test/e2e/cli-bundle.e2e.test.d.ts +2 -0
  94. package/dist/test/e2e/cli-bundle.e2e.test.d.ts.map +1 -0
  95. package/dist/test/e2e/cli-bundle.e2e.test.js +138 -0
  96. package/dist/test/e2e/cli-bundle.e2e.test.js.map +1 -0
  97. package/dist/test/e2e/git-build-release.e2e.test.d.ts +2 -0
  98. package/dist/test/e2e/git-build-release.e2e.test.d.ts.map +1 -0
  99. package/dist/test/e2e/git-build-release.e2e.test.js +87 -0
  100. package/dist/test/e2e/git-build-release.e2e.test.js.map +1 -0
  101. package/dist/test/e2e/helpers.d.ts +37 -0
  102. package/dist/test/e2e/helpers.d.ts.map +1 -0
  103. package/dist/test/e2e/helpers.js +57 -0
  104. package/dist/test/e2e/helpers.js.map +1 -0
  105. package/dist/test/unit/bin/affected-actors-cli.test.d.ts +2 -0
  106. package/dist/test/unit/bin/affected-actors-cli.test.d.ts.map +1 -0
  107. package/dist/test/unit/bin/affected-actors-cli.test.js +167 -0
  108. package/dist/test/unit/bin/affected-actors-cli.test.js.map +1 -0
  109. package/dist/test/unit/bin/affected-actors.test.d.ts +2 -0
  110. package/dist/test/unit/bin/affected-actors.test.d.ts.map +1 -0
  111. package/dist/test/unit/bin/affected-actors.test.js +141 -0
  112. package/dist/test/unit/bin/affected-actors.test.js.map +1 -0
  113. package/dist/test/unit/bin/build-cli.test.d.ts +2 -0
  114. package/dist/test/unit/bin/build-cli.test.d.ts.map +1 -0
  115. package/dist/test/unit/bin/build-cli.test.js +44 -0
  116. package/dist/test/unit/bin/build-cli.test.js.map +1 -0
  117. package/dist/test/unit/bin/build-from-local.test.js +65 -1
  118. package/dist/test/unit/bin/build-from-local.test.js.map +1 -1
  119. package/dist/test/unit/bin/build.test.js +223 -3
  120. package/dist/test/unit/bin/build.test.js.map +1 -1
  121. package/dist/test/unit/bin/compiled-cli.d.ts +6 -0
  122. package/dist/test/unit/bin/compiled-cli.d.ts.map +1 -0
  123. package/dist/test/unit/bin/compiled-cli.js +16 -0
  124. package/dist/test/unit/bin/compiled-cli.js.map +1 -0
  125. package/dist/test/unit/bin/diff-changes.test.js +130 -101
  126. package/dist/test/unit/bin/diff-changes.test.js.map +1 -1
  127. package/dist/test/unit/bin/diff.test.js +25 -28
  128. package/dist/test/unit/bin/diff.test.js.map +1 -1
  129. package/dist/test/unit/bin/git.test.js +69 -152
  130. package/dist/test/unit/bin/git.test.js.map +1 -1
  131. package/dist/test/unit/bin/slack.test.d.ts +2 -0
  132. package/dist/test/unit/bin/slack.test.d.ts.map +1 -0
  133. package/dist/test/unit/bin/slack.test.js +71 -0
  134. package/dist/test/unit/bin/slack.test.js.map +1 -0
  135. package/dist/test/unit/bin/stdin-input.test.d.ts +2 -0
  136. package/dist/test/unit/bin/stdin-input.test.d.ts.map +1 -0
  137. package/dist/test/unit/bin/stdin-input.test.js +50 -0
  138. package/dist/test/unit/bin/stdin-input.test.js.map +1 -0
  139. package/dist/test/unit/github-scripts/mask-config-secrets.test.d.ts +2 -0
  140. package/dist/test/unit/github-scripts/mask-config-secrets.test.d.ts.map +1 -0
  141. package/dist/test/unit/github-scripts/mask-config-secrets.test.js +50 -0
  142. package/dist/test/unit/github-scripts/mask-config-secrets.test.js.map +1 -0
  143. package/dist/test/unit/github-scripts/run-with-apify-tokens.test.d.ts +2 -0
  144. package/dist/test/unit/github-scripts/run-with-apify-tokens.test.d.ts.map +1 -0
  145. package/dist/test/unit/github-scripts/run-with-apify-tokens.test.js +102 -0
  146. package/dist/test/unit/github-scripts/run-with-apify-tokens.test.js.map +1 -0
  147. package/dist/test/unit/run-checks.test.d.ts +2 -0
  148. package/dist/test/unit/run-checks.test.d.ts.map +1 -0
  149. package/dist/test/unit/run-checks.test.js +62 -0
  150. package/dist/test/unit/run-checks.test.js.map +1 -0
  151. package/dist/test/unit/run-test-result.test.d.ts +2 -0
  152. package/dist/test/unit/run-test-result.test.d.ts.map +1 -0
  153. package/dist/test/unit/run-test-result.test.js +34 -0
  154. package/dist/test/unit/run-test-result.test.js.map +1 -0
  155. package/dist/test/unit/should-built-and-test.test.js +41 -38
  156. package/dist/test/unit/should-built-and-test.test.js.map +1 -1
  157. package/dist/tsconfig.tsbuildinfo +1 -1
  158. package/docs/cli.md +373 -0
  159. package/docs/lib.md +271 -0
  160. package/docs/workflows.md +367 -0
  161. package/lib/extend-expect.ts +48 -258
  162. package/lib/lib.ts +24 -28
  163. package/lib/run-checks.ts +101 -0
  164. package/lib/run-test-result.ts +45 -55
  165. package/package.json +13 -3
  166. package/test/bundle/cli-bundle.test.ts +235 -0
  167. package/test/e2e/cli-bundle.e2e.test.ts +207 -0
  168. package/test/e2e/git-build-release.e2e.test.ts +113 -0
  169. package/test/e2e/helpers.ts +82 -0
  170. package/test/unit/bin/affected-actors-cli.test.ts +201 -0
  171. package/test/unit/bin/affected-actors.test.ts +182 -0
  172. package/test/unit/bin/build-cli.test.ts +55 -0
  173. package/test/unit/bin/build-from-local.test.ts +71 -0
  174. package/test/unit/bin/build.test.ts +279 -3
  175. package/test/unit/bin/compiled-cli.ts +17 -0
  176. package/test/unit/bin/diff-changes.test.ts +139 -101
  177. package/test/unit/bin/diff.test.ts +25 -28
  178. package/test/unit/bin/git.test.ts +85 -176
  179. package/test/unit/bin/slack.test.ts +84 -0
  180. package/test/unit/bin/stdin-input.test.ts +61 -0
  181. package/test/unit/github-scripts/mask-config-secrets.test.ts +61 -0
  182. package/test/unit/github-scripts/run-with-apify-tokens.test.ts +115 -0
  183. package/test/unit/run-checks.test.ts +76 -0
  184. package/test/unit/run-test-result.test.ts +44 -0
  185. package/test/unit/should-built-and-test.test.ts +46 -39
  186. package/.github/scripts/check-package-version-bump.mjs +0 -74
  187. package/.github/workflows-package-version +0 -1
@@ -0,0 +1,61 @@
1
+ import { describe, expect, it } from 'vitest';
2
+
3
+ import { parseBuildInput, parseReleaseChanges } from '../../../bin/stdin-input.js';
4
+
5
+ describe('parseBuildInput', () => {
6
+ it('reads the Actors', () => {
7
+ expect(parseBuildInput('{"actors":[{"actorFullName":"owner/a"}]}')).toStrictEqual({
8
+ actors: [{ actorFullName: 'owner/a' }],
9
+ });
10
+ });
11
+
12
+ it('ignores the other fields, so whole get-affected-actors fields can be passed along', () => {
13
+ const input = { head: 'f'.repeat(40), ref: 'feature', actors: [{ actorFullName: 'owner/a', extra: 1 }] };
14
+ expect(parseBuildInput(JSON.stringify(input))).toStrictEqual({ actors: [{ actorFullName: 'owner/a' }] });
15
+ });
16
+
17
+ it('accepts an empty list, which builds nothing', () => {
18
+ expect(parseBuildInput('{"actors":[]}')).toStrictEqual({ actors: [] });
19
+ });
20
+
21
+ it('rejects empty input, which is what a failed command earlier in a pipe leaves behind', () => {
22
+ expect(() => parseBuildInput(' \n')).toThrow('Expected the Actors to build as JSON on stdin, got nothing');
23
+ });
24
+
25
+ it('rejects input that is not JSON', () => {
26
+ expect(() => parseBuildInput('owner/a')).toThrow('not valid JSON');
27
+ });
28
+
29
+ it('rejects JSON that is not a list of Actors, e.g. a whole get-affected-actors document', () => {
30
+ expect(() => parseBuildInput('{"functionallyChangedActors":[{"actorFullName":"owner/a"}]}')).toThrow(
31
+ 'is not the Actors to build',
32
+ );
33
+ });
34
+ });
35
+
36
+ describe('parseReleaseChanges', () => {
37
+ const commit = { sha: '1'.repeat(40), author: 'Dev<dev@example.com>', date: 'Date', message: 'feat: foo' };
38
+
39
+ it('reads what changed from a get-affected-actors document, ignoring the rest', () => {
40
+ const document = {
41
+ version: 1,
42
+ ref: 'master',
43
+ head: '1'.repeat(40),
44
+ commits: [commit],
45
+ changedFiles: ['src/main.ts'],
46
+ changelog: '- Added foo',
47
+ functionallyChangedActors: [{ actorFullName: 'owner/a' }],
48
+ cosmeticallyChangedActors: [],
49
+ };
50
+ expect(parseReleaseChanges(JSON.stringify(document))).toStrictEqual({
51
+ commits: [commit],
52
+ changedFiles: ['src/main.ts'],
53
+ changelog: '- Added foo',
54
+ });
55
+ });
56
+
57
+ it('rejects empty input and input that is not a get-affected-actors document', () => {
58
+ expect(() => parseReleaseChanges('')).toThrow('got nothing');
59
+ expect(() => parseReleaseChanges('{"actors":[]}')).toThrow('not the output of get-affected-actors');
60
+ });
61
+ });
@@ -0,0 +1,61 @@
1
+ import { spawnSync } from 'node:child_process';
2
+ import process from 'node:process';
3
+ import { fileURLToPath } from 'node:url';
4
+
5
+ import { describe, expect, it } from 'vitest';
6
+
7
+ const SCRIPT = fileURLToPath(new URL('../../../.github/scripts/mask-config-secrets.sh', import.meta.url));
8
+
9
+ const run = (configSecrets?: string) =>
10
+ spawnSync('bash', [SCRIPT], {
11
+ encoding: 'utf8',
12
+ env: { PATH: process.env.PATH, ...(configSecrets === undefined ? {} : { CONFIG_SECRETS: configSecrets }) },
13
+ });
14
+
15
+ const masks = (stdout: string) => stdout.split('\n').filter((line) => line !== '');
16
+
17
+ describe('mask-config-secrets.sh', () => {
18
+ it('registers every value as a mask', () => {
19
+ const { status, stdout, stderr } = run(JSON.stringify({ TOKEN_ONE: 'one', TOKEN_TWO: 'two' }));
20
+ expect(status, stderr).toBe(0);
21
+ expect(masks(stdout)).toStrictEqual(['::add-mask::one', '::add-mask::two']);
22
+ });
23
+
24
+ it('registers a multi-line value line by line, skipping empty lines', () => {
25
+ const { status, stdout, stderr } = run(
26
+ JSON.stringify({ DEPLOY_KEY: '-----BEGIN KEY-----\nabc\n\n-----END KEY-----\n' }),
27
+ );
28
+ expect(status, stderr).toBe(0);
29
+ expect(masks(stdout)).toStrictEqual([
30
+ '::add-mask::-----BEGIN KEY-----',
31
+ '::add-mask::abc',
32
+ '::add-mask::-----END KEY-----',
33
+ ]);
34
+ });
35
+
36
+ it('skips values that are not strings', () => {
37
+ const { status, stdout, stderr } = run(JSON.stringify({ TOKEN_ONE: 'one', COUNT: 1 }));
38
+ expect(status, stderr).toBe(0);
39
+ expect(masks(stdout)).toStrictEqual(['::add-mask::one']);
40
+ });
41
+
42
+ it.each([
43
+ ['is unset', undefined],
44
+ ['is empty', ''],
45
+ ])('does nothing when CONFIG_SECRETS %s', (_, configSecrets) => {
46
+ const { status, stdout, stderr } = run(configSecrets);
47
+ expect(status, stderr).toBe(0);
48
+ expect(stdout).toBe('');
49
+ });
50
+
51
+ it.each([
52
+ ['is not JSON', 'apify_api_xyzzy'],
53
+ ['is a JSON string', '"apify_api_xyzzy"'],
54
+ ['is a JSON array', '["apify_api_xyzzy"]'],
55
+ ])('fails without printing the value when the secret %s', (_, configSecrets) => {
56
+ const { status, stdout, stderr } = run(configSecrets);
57
+ expect(status).toBe(1);
58
+ expect(stdout).toContain('::error::The CONFIG_SECRETS secret must be a JSON object.');
59
+ expect(stdout + stderr).not.toContain('xyzzy');
60
+ });
61
+ });
@@ -0,0 +1,115 @@
1
+ import { spawnSync } from 'node:child_process';
2
+ import { mkdtemp, rm, writeFile } from 'node:fs/promises';
3
+ import os from 'node:os';
4
+ import path from 'node:path';
5
+ import process from 'node:process';
6
+ import { fileURLToPath } from 'node:url';
7
+
8
+ import { afterAll, beforeAll, describe, expect, it } from 'vitest';
9
+
10
+ const SCRIPT = fileURLToPath(new URL('../../../.github/scripts/run-with-apify-tokens.mjs', import.meta.url));
11
+
12
+ // Three Actors, each with its own token, so a test can leave any of them out.
13
+ const CONFIG = {
14
+ actors: [
15
+ { actorFullName: 'team/one', tokenEnvVar: 'TOKEN_ONE' },
16
+ { actorFullName: 'team/two', tokenEnvVar: 'TOKEN_TWO' },
17
+ { actorFullName: 'team/three', tokenEnvVar: 'TOKEN_THREE' },
18
+ ],
19
+ };
20
+
21
+ // The command the wrapper runs prints what it was handed, so a test sees exactly what got through.
22
+ const PRINT_ENV = `console.log(JSON.stringify({
23
+ TOKEN_ONE: process.env.TOKEN_ONE,
24
+ TOKEN_TWO: process.env.TOKEN_TWO,
25
+ TOKEN_THREE: process.env.TOKEN_THREE,
26
+ NPM_TOKEN: process.env.NPM_TOKEN,
27
+ WORKFLOW_SECRETS: process.env.WORKFLOW_SECRETS,
28
+ }))`;
29
+
30
+ let cwd: string;
31
+
32
+ // `workflowSecrets` is what the step's `toJSON(secrets)` holds inside the reusable workflow.
33
+ const run = (workflowSecrets: Record<string, string>) => {
34
+ const result = spawnSync(process.execPath, [SCRIPT, process.execPath, '-e', PRINT_ENV], {
35
+ cwd,
36
+ encoding: 'utf8',
37
+ env: { PATH: process.env.PATH, WORKFLOW_SECRETS: JSON.stringify(workflowSecrets) },
38
+ });
39
+ return { ...result, env: result.status === 0 ? JSON.parse(result.stdout) : undefined };
40
+ };
41
+
42
+ describe('run-with-apify-tokens.mjs', () => {
43
+ beforeAll(async () => {
44
+ cwd = await mkdtemp(path.join(os.tmpdir(), 'run-with-apify-tokens-'));
45
+ await writeFile(path.join(cwd, 'apify-test-tools.config.json'), JSON.stringify(CONFIG));
46
+ });
47
+
48
+ afterAll(async () => {
49
+ await rm(cwd, { recursive: true, force: true });
50
+ });
51
+
52
+ it('passes on inherited secrets, as `secrets: inherit` sends them', () => {
53
+ const { status, stderr, env } = run({
54
+ TOKEN_ONE: 'one',
55
+ TOKEN_TWO: 'two',
56
+ TOKEN_THREE: 'three',
57
+ NPM_TOKEN: 'npm',
58
+ });
59
+ expect(status, stderr).toBe(0);
60
+ expect(env).toStrictEqual({ TOKEN_ONE: 'one', TOKEN_TWO: 'two', TOKEN_THREE: 'three' });
61
+ });
62
+
63
+ it('unpacks the tokens from the CONFIG_SECRETS secret', () => {
64
+ const { status, stderr, env } = run({
65
+ CONFIG_SECRETS: JSON.stringify({ TOKEN_ONE: 'one', TOKEN_TWO: 'two', TOKEN_THREE: 'three' }),
66
+ NPM_TOKEN: 'npm',
67
+ });
68
+ expect(status, stderr).toBe(0);
69
+ expect(env).toStrictEqual({ TOKEN_ONE: 'one', TOKEN_TWO: 'two', TOKEN_THREE: 'three' });
70
+ });
71
+
72
+ it('passes on only the config names when CONFIG_SECRETS holds every secret of the caller', () => {
73
+ const { status, stderr, env } = run({
74
+ CONFIG_SECRETS: JSON.stringify({
75
+ TOKEN_ONE: 'one',
76
+ TOKEN_TWO: 'two',
77
+ TOKEN_THREE: 'three',
78
+ NPM_TOKEN: 'npm',
79
+ github_token: 'gh',
80
+ }),
81
+ });
82
+ expect(status, stderr).toBe(0);
83
+ expect(env).toStrictEqual({ TOKEN_ONE: 'one', TOKEN_TWO: 'two', TOKEN_THREE: 'three' });
84
+ });
85
+
86
+ it('prefers CONFIG_SECRETS over an inherited secret of the same name', () => {
87
+ const { status, stderr, env } = run({
88
+ CONFIG_SECRETS: JSON.stringify({ TOKEN_ONE: 'from-config-secrets' }),
89
+ TOKEN_ONE: 'inherited',
90
+ TOKEN_TWO: 'inherited',
91
+ });
92
+ expect(status, stderr).toBe(0);
93
+ expect(env).toStrictEqual({ TOKEN_ONE: 'from-config-secrets', TOKEN_TWO: 'inherited' });
94
+ });
95
+
96
+ it('warns about a token the config declares but nothing passes, and runs anyway', () => {
97
+ const { status, stderr, env } = run({
98
+ CONFIG_SECRETS: JSON.stringify({ TOKEN_ONE: 'one', TOKEN_TWO: 'two' }),
99
+ });
100
+ expect(status, stderr).toBe(0);
101
+ expect(stderr).toContain('declares TOKEN_THREE, but no such secret was passed');
102
+ expect(env).toStrictEqual({ TOKEN_ONE: 'one', TOKEN_TWO: 'two' });
103
+ });
104
+
105
+ it.each([
106
+ ['is not JSON', 'apify_api_xyzzy', 'The CONFIG_SECRETS secret is not valid JSON.'],
107
+ ['is a JSON string', '"apify_api_xyzzy"', 'The CONFIG_SECRETS secret must be a JSON object.'],
108
+ ['is a JSON array', '["apify_api_xyzzy"]', 'The CONFIG_SECRETS secret must be a JSON object.'],
109
+ ])('fails without printing the value when CONFIG_SECRETS %s', (_, packed, message) => {
110
+ const { status, stdout, stderr } = run({ CONFIG_SECRETS: packed });
111
+ expect(status).toBe(1);
112
+ expect(stderr).toContain(message);
113
+ expect(stdout + stderr).not.toContain('xyzzy');
114
+ });
115
+ });
@@ -0,0 +1,76 @@
1
+ import { describe, expect, it } from 'vitest';
2
+
3
+ import { checkRun } from '../../lib/run-checks.js';
4
+ import type { RunTestResult } from '../../lib/run-test-result.js';
5
+
6
+ const fakeRun = (overrides: { status?: string; log?: string; stats?: object; chargedEventCounts?: object } = {}) =>
7
+ ({
8
+ status: overrides.status ?? 'SUCCEEDED',
9
+ getRunInfo: async () => ({
10
+ stats: { durationMillis: 5_000 },
11
+ chargedEventCounts: overrides.chargedEventCounts,
12
+ }),
13
+ getDataset: async () => ({ items: new Array(10) }),
14
+ getStatistics: async () => ({
15
+ requestsFailed: 0,
16
+ requestsRetries: 1,
17
+ requestRetryHistogram: [5, 1],
18
+ ...overrides.stats,
19
+ }),
20
+ getLog: async () => overrides.log ?? 'all good',
21
+ }) as unknown as RunTestResult;
22
+
23
+ const failures = async (run: RunTestResult, options: Parameters<typeof checkRun>[1]) =>
24
+ (await checkRun(run, options)).filter(({ pass }) => !pass).map(({ name }) => name);
25
+
26
+ describe('checkRun()', () => {
27
+ it('passes a run matching the defaults', async () => {
28
+ expect(await failures(fakeRun(), { datasetItemCount: 10 })).toEqual([]);
29
+ });
30
+
31
+ it('reports every failing check at once', async () => {
32
+ const run = fakeRun({ status: 'FAILED', log: 'TypeError: x', stats: { requestsFailed: 2 } });
33
+ expect(await failures(run, { datasetItemCount: { min: 11 } })).toEqual([
34
+ 'status',
35
+ 'datasetItemCount',
36
+ 'failedRequests',
37
+ 'forbiddenLogs',
38
+ ]);
39
+ });
40
+
41
+ it('disables a check with null', async () => {
42
+ const run = fakeRun({ stats: { requestsFailed: 2 } });
43
+ expect(await failures(run, { datasetItemCount: 10, failedRequests: null })).toEqual([]);
44
+ });
45
+
46
+ it('treats explicitly undefined options as disabled checks', async () => {
47
+ const options = { datasetItemCount: 10, duration: undefined, maxRetriesPerRequest: undefined };
48
+ expect(await failures(fakeRun(), options)).toEqual([]);
49
+ });
50
+
51
+ it('checks the longest retry chain only when asked to', async () => {
52
+ expect(await failures(fakeRun(), { datasetItemCount: 10, maxRetriesPerRequest: 0 })).toEqual([
53
+ 'maxRetriesPerRequest',
54
+ ]);
55
+ expect(await failures(fakeRun(), { datasetItemCount: 10, maxRetriesPerRequest: 1 })).toEqual([]);
56
+ });
57
+
58
+ describe('chargedEventCounts', () => {
59
+ const run = fakeRun({ chargedEventCounts: { start: 1, item: 7 } });
60
+
61
+ it('passes matching counts and intervals', async () => {
62
+ const options = { datasetItemCount: 10, chargedEventCounts: { start: 1, item: { min: 5 } } };
63
+ expect(await failures(run, options)).toEqual([]);
64
+ });
65
+
66
+ it('expects unlisted charged events to not happen', async () => {
67
+ const options = { datasetItemCount: 10, chargedEventCounts: { start: 1 } as Record<string, number> };
68
+ expect(await failures(run, options)).toEqual(['chargedEventCounts.item']);
69
+ });
70
+
71
+ it('fails an expected event that was never charged', async () => {
72
+ const options = { datasetItemCount: 10, chargedEventCounts: { start: 1, item: 7, extra: { min: 0 } } };
73
+ expect(await failures(run, options)).toEqual(['chargedEventCounts.extra']);
74
+ });
75
+ });
76
+ });
@@ -0,0 +1,44 @@
1
+ import type { ActorRun, ApifyClient } from 'apify-client';
2
+ import { describe, expect, it, vi } from 'vitest';
3
+
4
+ import { RunTestResult } from '../../lib/run-test-result.js';
5
+
6
+ const run = { id: 'run', status: 'SUCCEEDED', defaultDatasetId: 'ds', defaultKeyValueStoreId: 'kvs' } as ActorRun;
7
+
8
+ describe('RunTestResult', () => {
9
+ it('fetches the log once, even when empty or requested concurrently', async () => {
10
+ const get = vi.fn(async () => '');
11
+ const client = { run: () => ({ log: () => ({ get }) }) } as unknown as ApifyClient;
12
+ const result = new RunTestResult(client, run);
13
+
14
+ await Promise.all([result.getLog(), result.getLog()]);
15
+ await result.getLog();
16
+
17
+ expect(get).toHaveBeenCalledTimes(1);
18
+ });
19
+
20
+ it('retries after a failed fetch', async () => {
21
+ const get = vi.fn().mockRejectedValueOnce(new Error('boom')).mockResolvedValueOnce('log');
22
+ const client = { run: () => ({ log: () => ({ get }) }) } as unknown as ApifyClient;
23
+ const result = new RunTestResult(client, run);
24
+
25
+ await expect(result.getLog()).rejects.toThrow('boom');
26
+ expect(await result.getLog()).toBe('log');
27
+ });
28
+
29
+ it('pages through the dataset instead of listing it in one response', async () => {
30
+ const total = 25_000;
31
+ const listItems = vi.fn(async ({ offset, limit }: { offset: number; limit: number }) => ({
32
+ total,
33
+ items: Array.from({ length: Math.min(limit, total - offset) }, (_, i) => offset + i),
34
+ }));
35
+ const client = { dataset: () => ({ listItems }) } as unknown as ApifyClient;
36
+ const result = new RunTestResult(client, run);
37
+
38
+ const { items } = await result.getDataset<number>();
39
+
40
+ expect(items).toHaveLength(total);
41
+ expect(items[24_999]).toBe(24_999);
42
+ expect(listItems).toHaveBeenCalledTimes(25);
43
+ });
44
+ });
@@ -5,7 +5,7 @@ import path from 'node:path';
5
5
  import { afterAll, describe, expect, test } from 'vitest';
6
6
 
7
7
  import { getChangedActors } from '../../bin/diff-changes.js';
8
- import type { ActorConfig, Commit } from '../../bin/types.js';
8
+ import type { ActorConfig } from '../../bin/types.js';
9
9
  import { ExistingDir, ExistingFile, RelativeDir, RelativeFile } from '../../bin/utils/path/repo-relative.js';
10
10
 
11
11
  const originalCwd = process.cwd();
@@ -41,6 +41,16 @@ const actorConfig = (
41
41
  };
42
42
  };
43
43
 
44
+ // What a PR (functional changes only) or a release (isLatest, all changes) builds, in config order
45
+ const changedActorsFor = async ({
46
+ isLatest = false,
47
+ ...options
48
+ }: Parameters<typeof getChangedActors>[0] & { isLatest?: boolean }): Promise<ActorConfig[]> => {
49
+ const { functional, cosmetic } = await getChangedActors(options);
50
+ const changed = isLatest ? [...functional, ...cosmetic] : functional;
51
+ return options.actorConfigs.filter((config) => changed.includes(config));
52
+ };
53
+
44
54
  describe('Should build and test parser', () => {
45
55
  // From https://github.com/apify-store/testing-repo-for-github-actions
46
56
  const ACTOR_CONFIGS: ActorConfig[] = [
@@ -62,19 +72,16 @@ describe('Should build and test parser', () => {
62
72
  ),
63
73
  ];
64
74
 
65
- const commits: Commit[] = [
66
- { sha: 'Commit1', author: '', date: '', message: '' },
67
- { sha: 'Commit3', author: '', date: '', message: '' },
68
- ];
75
+ const range = { fromRef: 'Commit0', toRef: 'Commit3' };
69
76
 
70
77
  test('Ignores dev-only readme', async () => {
71
78
  const FILES = ['README.md', 'code/README.md', 'shared/README.md'];
72
79
 
73
- const actorsChanged = await getChangedActors({
80
+ const actorsChanged = await changedActorsFor({
74
81
  actorConfigs: ACTOR_CONFIGS,
75
82
  isLatest: false,
76
83
  filepathsChanged: FILES,
77
- commits,
84
+ range,
78
85
  });
79
86
 
80
87
  expect(actorsChanged).toEqual([]);
@@ -83,11 +90,11 @@ describe('Should build and test parser', () => {
83
90
  test('Ignores other ignored files and folders', async () => {
84
91
  const FILES = ['.vscode/settings.json', '.gitignore', '.husky/pre-commit', '.eslintrc', '.editorconfig'];
85
92
 
86
- const actorsChanged = await getChangedActors({
93
+ const actorsChanged = await changedActorsFor({
87
94
  actorConfigs: ACTOR_CONFIGS,
88
95
  isLatest: false,
89
96
  filepathsChanged: FILES,
90
- commits,
97
+ range,
91
98
  });
92
99
 
93
100
  expect(actorsChanged).toEqual([]);
@@ -96,11 +103,11 @@ describe('Should build and test parser', () => {
96
103
  test('.actor/ changes trigger builds for broad-context actors', async () => {
97
104
  const FILES = ['.actor/actor.json'];
98
105
 
99
- const actorsChanged = await getChangedActors({
106
+ const actorsChanged = await changedActorsFor({
100
107
  actorConfigs: ACTOR_CONFIGS,
101
108
  isLatest: false,
102
109
  filepathsChanged: FILES,
103
- commits,
110
+ range,
104
111
  });
105
112
 
106
113
  expect(actorsChanged).toEqual(ACTOR_CONFIGS.slice(0, 2));
@@ -109,19 +116,19 @@ describe('Should build and test parser', () => {
109
116
  test('Configured changelog is cosmetic for every actor, only on latest', async () => {
110
117
  const FILES = ['CHANGELOG.md'];
111
118
 
112
- const actorsChangedNotLatest = await getChangedActors({
119
+ const actorsChangedNotLatest = await changedActorsFor({
113
120
  actorConfigs: ACTOR_CONFIGS,
114
121
  isLatest: false,
115
122
  filepathsChanged: FILES,
116
- commits,
123
+ range,
117
124
  });
118
125
  expect(actorsChangedNotLatest).toEqual([]);
119
126
 
120
- const actorsChangedLatest = await getChangedActors({
127
+ const actorsChangedLatest = await changedActorsFor({
121
128
  actorConfigs: ACTOR_CONFIGS,
122
129
  isLatest: true,
123
130
  filepathsChanged: FILES,
124
- commits,
131
+ range,
125
132
  });
126
133
  expect(actorsChangedLatest).toEqual(ACTOR_CONFIGS);
127
134
  });
@@ -129,11 +136,11 @@ describe('Should build and test parser', () => {
129
136
  test('A changelog nested inside one actor own folder is excluded for sibling actors', async () => {
130
137
  const FILES = ['actors/lukaskrivka_testing-github-integration-1/CHANGELOG.md'];
131
138
 
132
- const actorsChanged = await getChangedActors({
139
+ const actorsChanged = await changedActorsFor({
133
140
  actorConfigs: ACTOR_CONFIGS,
134
141
  isLatest: true,
135
142
  filepathsChanged: FILES,
136
- commits,
143
+ range,
137
144
  });
138
145
 
139
146
  expect(actorsChanged).toEqual([ACTOR_CONFIGS[0]]);
@@ -142,11 +149,11 @@ describe('Should build and test parser', () => {
142
149
  test('Code updated, tests broad-context actors', async () => {
143
150
  const FILES = ['code/src/main.ts', 'package.json'];
144
151
 
145
- const actorsChanged = await getChangedActors({
152
+ const actorsChanged = await changedActorsFor({
146
153
  actorConfigs: ACTOR_CONFIGS,
147
154
  isLatest: false,
148
155
  filepathsChanged: FILES,
149
- commits,
156
+ range,
150
157
  });
151
158
 
152
159
  expect(actorsChanged).toEqual(ACTOR_CONFIGS.slice(0, 2));
@@ -158,11 +165,11 @@ describe('Should build and test parser', () => {
158
165
  'standalone-actors/lukaskrivka_test-standalone/Dockerfile',
159
166
  ];
160
167
 
161
- const actorsChanged = await getChangedActors({
168
+ const actorsChanged = await changedActorsFor({
162
169
  actorConfigs: ACTOR_CONFIGS,
163
170
  isLatest: false,
164
171
  filepathsChanged: FILES,
165
- commits,
172
+ range,
166
173
  });
167
174
 
168
175
  expect(actorsChanged).toEqual([ACTOR_CONFIGS[0], ACTOR_CONFIGS[2]]);
@@ -171,11 +178,11 @@ describe('Should build and test parser', () => {
171
178
  test('src/main.ts updated', async () => {
172
179
  const FILES = ['src/main.ts'];
173
180
 
174
- const actorsChanged = await getChangedActors({
181
+ const actorsChanged = await changedActorsFor({
175
182
  actorConfigs: ACTOR_CONFIGS,
176
183
  isLatest: true,
177
184
  filepathsChanged: FILES,
178
- commits,
185
+ range,
179
186
  });
180
187
 
181
188
  expect(actorsChanged).toEqual(ACTOR_CONFIGS.slice(0, 2));
@@ -188,11 +195,11 @@ describe('Should build and test parser', () => {
188
195
  'standalone-actors/lukaskrivka_test-standalone/Dockerfile',
189
196
  ];
190
197
 
191
- const actorsChanged = await getChangedActors({
198
+ const actorsChanged = await changedActorsFor({
192
199
  actorConfigs: ACTOR_CONFIGS,
193
200
  isLatest: false,
194
201
  filepathsChanged: FILES,
195
- commits,
202
+ range,
196
203
  });
197
204
 
198
205
  expect(actorsChanged).toEqual(ACTOR_CONFIGS);
@@ -204,11 +211,11 @@ describe('Should build and test parser', () => {
204
211
  'standalone-actors/lukaskrivka_test-standalone/README.md',
205
212
  ];
206
213
 
207
- const actorsChanged = await getChangedActors({
214
+ const actorsChanged = await changedActorsFor({
208
215
  actorConfigs: ACTOR_CONFIGS,
209
216
  isLatest: false,
210
217
  filepathsChanged: FILES,
211
- commits,
218
+ range,
212
219
  });
213
220
 
214
221
  expect(actorsChanged).toEqual([]);
@@ -217,11 +224,11 @@ describe('Should build and test parser', () => {
217
224
  test('Actor JSON changes in PR context trigger tests', async () => {
218
225
  const FILES = ['actors/lukaskrivka_testing-github-integration-1/.actor/actor.json'];
219
226
 
220
- const actorsChanged = await getChangedActors({
227
+ const actorsChanged = await changedActorsFor({
221
228
  actorConfigs: ACTOR_CONFIGS,
222
229
  isLatest: false,
223
230
  filepathsChanged: FILES,
224
- commits,
231
+ range,
225
232
  });
226
233
 
227
234
  expect(actorsChanged).toEqual([ACTOR_CONFIGS[0]]);
@@ -230,11 +237,11 @@ describe('Should build and test parser', () => {
230
237
  test('Actor JSON changes in latest context trigger builds', async () => {
231
238
  const FILES = ['actors/lukaskrivka_testing-github-integration-1/.actor/actor.json'];
232
239
 
233
- const actorsChanged = await getChangedActors({
240
+ const actorsChanged = await changedActorsFor({
234
241
  actorConfigs: ACTOR_CONFIGS,
235
242
  isLatest: true,
236
243
  filepathsChanged: FILES,
237
- commits,
244
+ range,
238
245
  });
239
246
 
240
247
  expect(actorsChanged).toEqual([ACTOR_CONFIGS[0]]);
@@ -243,11 +250,11 @@ describe('Should build and test parser', () => {
243
250
  test('JSON file with functional changes triggers tests', async () => {
244
251
  const FILES = ['actors/lukaskrivka_testing-github-integration-1/.actor/actor.json'];
245
252
 
246
- const actorsChanged = await getChangedActors({
253
+ const actorsChanged = await changedActorsFor({
247
254
  actorConfigs: ACTOR_CONFIGS,
248
255
  isLatest: false,
249
256
  filepathsChanged: FILES,
250
- commits,
257
+ range,
251
258
  });
252
259
 
253
260
  expect(actorsChanged).toEqual([ACTOR_CONFIGS[0]]);
@@ -259,11 +266,11 @@ describe('Should build and test parser', () => {
259
266
  'actors/lukaskrivka_testing-github-integration-2/.actor/input_schema.json',
260
267
  ];
261
268
 
262
- const actorsChanged = await getChangedActors({
269
+ const actorsChanged = await changedActorsFor({
263
270
  actorConfigs: ACTOR_CONFIGS,
264
271
  isLatest: false,
265
272
  filepathsChanged: FILES,
266
- commits,
273
+ range,
267
274
  });
268
275
 
269
276
  expect(actorsChanged).toEqual(ACTOR_CONFIGS.slice(0, 2));
@@ -272,11 +279,11 @@ describe('Should build and test parser', () => {
272
279
  test('Narrow-context actor JSON change in PR context triggers tests', async () => {
273
280
  const FILES = ['standalone-actors/lukaskrivka_test-standalone/.actor/actor.json'];
274
281
 
275
- const actorsChanged = await getChangedActors({
282
+ const actorsChanged = await changedActorsFor({
276
283
  actorConfigs: ACTOR_CONFIGS,
277
284
  isLatest: false,
278
285
  filepathsChanged: FILES,
279
- commits,
286
+ range,
280
287
  });
281
288
 
282
289
  expect(actorsChanged).toEqual([ACTOR_CONFIGS[2]]);
@@ -331,11 +338,11 @@ describe('Should build and test parser', () => {
331
338
  ),
332
339
  ];
333
340
 
334
- const actorsChanged = await getChangedActors({
341
+ const actorsChanged = await changedActorsFor({
335
342
  actorConfigs: ACTOR_CONFIGS_GOOGLE_MAPS,
336
343
  isLatest: false,
337
344
  filepathsChanged: FILES,
338
- commits,
345
+ range,
339
346
  });
340
347
 
341
348
  expect(actorsChanged).toEqual(ACTOR_CONFIGS_GOOGLE_MAPS.slice(0, 8));