apify-test-tools 0.10.1-beta.3 → 0.10.1-beta.4

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 (140) hide show
  1. package/.github/workflows/public_platform-tests-claude-investigate-and-fix.yaml +7 -2
  2. package/CHANGELOG.md +6 -0
  3. package/CONTRIBUTING.md +9 -0
  4. package/bin/build-from-local.ts +8 -7
  5. package/bin/build.ts +22 -21
  6. package/bin/diff-changes.ts +11 -10
  7. package/bin/diff-json-schema.ts +14 -12
  8. package/bin/git.ts +90 -43
  9. package/bin/logger.ts +23 -0
  10. package/bin/main.ts +49 -35
  11. package/bin/output.ts +6 -0
  12. package/bin/slack.ts +8 -7
  13. package/bin/test-report.ts +22 -21
  14. package/bin/utils/config/actor-json.ts +84 -0
  15. package/bin/utils/config/load-config.ts +46 -46
  16. package/bin/utils/config/parser.ts +33 -28
  17. package/bin/utils/config/structures/base.ts +7 -0
  18. package/bin/utils/json-file.ts +11 -14
  19. package/bin/utils/path/repo-relative.ts +156 -0
  20. package/bin/utils/path/schema.ts +18 -0
  21. package/bin/utils.ts +61 -34
  22. package/dist/bin/build-from-local.d.ts +1 -1
  23. package/dist/bin/build-from-local.d.ts.map +1 -1
  24. package/dist/bin/build-from-local.js +8 -7
  25. package/dist/bin/build-from-local.js.map +1 -1
  26. package/dist/bin/build.d.ts.map +1 -1
  27. package/dist/bin/build.js +22 -21
  28. package/dist/bin/build.js.map +1 -1
  29. package/dist/bin/diff-changes.d.ts +1 -1
  30. package/dist/bin/diff-changes.d.ts.map +1 -1
  31. package/dist/bin/diff-changes.js +9 -8
  32. package/dist/bin/diff-changes.js.map +1 -1
  33. package/dist/bin/diff-json-schema.d.ts +1 -1
  34. package/dist/bin/diff-json-schema.d.ts.map +1 -1
  35. package/dist/bin/diff-json-schema.js +13 -12
  36. package/dist/bin/diff-json-schema.js.map +1 -1
  37. package/dist/bin/git.d.ts +10 -10
  38. package/dist/bin/git.d.ts.map +1 -1
  39. package/dist/bin/git.js +78 -37
  40. package/dist/bin/git.js.map +1 -1
  41. package/dist/bin/logger.d.ts +11 -0
  42. package/dist/bin/logger.d.ts.map +1 -0
  43. package/dist/bin/logger.js +20 -0
  44. package/dist/bin/logger.js.map +1 -0
  45. package/dist/bin/main.d.ts +1 -1
  46. package/dist/bin/main.js +48 -35
  47. package/dist/bin/main.js.map +1 -1
  48. package/dist/bin/output.d.ts +3 -0
  49. package/dist/bin/output.d.ts.map +1 -0
  50. package/dist/bin/output.js +6 -0
  51. package/dist/bin/output.js.map +1 -0
  52. package/dist/bin/slack.d.ts.map +1 -1
  53. package/dist/bin/slack.js +8 -7
  54. package/dist/bin/slack.js.map +1 -1
  55. package/dist/bin/test-report.d.ts.map +1 -1
  56. package/dist/bin/test-report.js +22 -21
  57. package/dist/bin/test-report.js.map +1 -1
  58. package/dist/bin/utils/config/actor-json.d.ts +14 -0
  59. package/dist/bin/utils/config/actor-json.d.ts.map +1 -0
  60. package/dist/bin/utils/config/actor-json.js +60 -0
  61. package/dist/bin/utils/config/actor-json.js.map +1 -0
  62. package/dist/bin/utils/config/load-config.d.ts +12 -2
  63. package/dist/bin/utils/config/load-config.d.ts.map +1 -1
  64. package/dist/bin/utils/config/load-config.js +32 -31
  65. package/dist/bin/utils/config/load-config.js.map +1 -1
  66. package/dist/bin/utils/config/parser.d.ts +4 -12
  67. package/dist/bin/utils/config/parser.d.ts.map +1 -1
  68. package/dist/bin/utils/config/parser.js +24 -23
  69. package/dist/bin/utils/config/parser.js.map +1 -1
  70. package/dist/bin/utils/config/structures/base.d.ts +5 -0
  71. package/dist/bin/utils/config/structures/base.d.ts.map +1 -1
  72. package/dist/bin/utils/config/structures/base.js.map +1 -1
  73. package/dist/bin/utils/json-file.d.ts +3 -2
  74. package/dist/bin/utils/json-file.d.ts.map +1 -1
  75. package/dist/bin/utils/json-file.js +8 -13
  76. package/dist/bin/utils/json-file.js.map +1 -1
  77. package/dist/bin/utils/path/repo-relative.d.ts +35 -0
  78. package/dist/bin/utils/path/repo-relative.d.ts.map +1 -0
  79. package/dist/bin/utils/path/repo-relative.js +137 -0
  80. package/dist/bin/utils/path/repo-relative.js.map +1 -0
  81. package/dist/bin/utils/path/schema.d.ts +7 -0
  82. package/dist/bin/utils/path/schema.d.ts.map +1 -0
  83. package/dist/bin/utils/path/schema.js +16 -0
  84. package/dist/bin/utils/path/schema.js.map +1 -0
  85. package/dist/bin/utils.d.ts +14 -3
  86. package/dist/bin/utils.d.ts.map +1 -1
  87. package/dist/bin/utils.js +52 -29
  88. package/dist/bin/utils.js.map +1 -1
  89. package/dist/test/unit/bin/build-from-local.test.js +3 -39
  90. package/dist/test/unit/bin/build-from-local.test.js.map +1 -1
  91. package/dist/test/unit/bin/diff-changes.test.js +97 -96
  92. package/dist/test/unit/bin/diff-changes.test.js.map +1 -1
  93. package/dist/test/unit/bin/diff.test.js +52 -48
  94. package/dist/test/unit/bin/diff.test.js.map +1 -1
  95. package/dist/test/unit/bin/git-command.test.d.ts +2 -0
  96. package/dist/test/unit/bin/git-command.test.d.ts.map +1 -0
  97. package/dist/test/unit/bin/git-command.test.js +54 -0
  98. package/dist/test/unit/bin/git-command.test.js.map +1 -0
  99. package/dist/test/unit/bin/git.test.js +211 -92
  100. package/dist/test/unit/bin/git.test.js.map +1 -1
  101. package/dist/test/unit/bin/logger.test.d.ts +2 -0
  102. package/dist/test/unit/bin/logger.test.d.ts.map +1 -0
  103. package/dist/test/unit/bin/logger.test.js +44 -0
  104. package/dist/test/unit/bin/logger.test.js.map +1 -0
  105. package/dist/test/unit/bin/logging.test.d.ts +2 -0
  106. package/dist/test/unit/bin/logging.test.d.ts.map +1 -0
  107. package/dist/test/unit/bin/logging.test.js +43 -0
  108. package/dist/test/unit/bin/logging.test.js.map +1 -0
  109. package/dist/test/unit/bin/utils/config/load-config.test.js +47 -25
  110. package/dist/test/unit/bin/utils/config/load-config.test.js.map +1 -1
  111. package/dist/test/unit/bin/utils/config/parser.test.js +64 -19
  112. package/dist/test/unit/bin/utils/config/parser.test.js.map +1 -1
  113. package/dist/test/unit/bin/utils/files.test.js +16 -27
  114. package/dist/test/unit/bin/utils/files.test.js.map +1 -1
  115. package/dist/test/unit/bin/utils/path/repo-relative.test.d.ts +2 -0
  116. package/dist/test/unit/bin/utils/path/repo-relative.test.d.ts.map +1 -0
  117. package/dist/test/unit/bin/utils/path/repo-relative.test.js +196 -0
  118. package/dist/test/unit/bin/utils/path/repo-relative.test.js.map +1 -0
  119. package/dist/test/unit/bin/utils/path/schema.test.d.ts +2 -0
  120. package/dist/test/unit/bin/utils/path/schema.test.d.ts.map +1 -0
  121. package/dist/test/unit/bin/utils/path/schema.test.js +52 -0
  122. package/dist/test/unit/bin/utils/path/schema.test.js.map +1 -0
  123. package/dist/test/unit/should-built-and-test.test.js +40 -38
  124. package/dist/test/unit/should-built-and-test.test.js.map +1 -1
  125. package/dist/tsconfig.tsbuildinfo +1 -1
  126. package/eslint.config.mjs +9 -1
  127. package/package.json +4 -4
  128. package/test/unit/bin/build-from-local.test.ts +3 -53
  129. package/test/unit/bin/diff-changes.test.ts +97 -96
  130. package/test/unit/bin/diff.test.ts +53 -48
  131. package/test/unit/bin/git-command.test.ts +71 -0
  132. package/test/unit/bin/git.test.ts +246 -89
  133. package/test/unit/bin/logger.test.ts +55 -0
  134. package/test/unit/bin/logging.test.ts +52 -0
  135. package/test/unit/bin/utils/config/load-config.test.ts +63 -26
  136. package/test/unit/bin/utils/config/parser.test.ts +81 -27
  137. package/test/unit/bin/utils/files.test.ts +19 -33
  138. package/test/unit/bin/utils/path/repo-relative.test.ts +250 -0
  139. package/test/unit/bin/utils/path/schema.test.ts +65 -0
  140. package/test/unit/should-built-and-test.test.ts +40 -38
package/bin/logger.ts ADDED
@@ -0,0 +1,23 @@
1
+ import process from 'node:process';
2
+ import { formatWithOptions } from 'node:util';
3
+
4
+ export const logLevels = ['debug', 'info', 'warn', 'error', 'silent'] as const;
5
+ export type LogLevel = (typeof logLevels)[number];
6
+
7
+ let currentLevel: LogLevel = 'info';
8
+
9
+ const write = (level: Exclude<LogLevel, 'silent'>, args: unknown[]) => {
10
+ if (logLevels.indexOf(level) < logLevels.indexOf(currentLevel)) return;
11
+ process.stderr.write(`${formatWithOptions({ colors: false }, ...args)}\n`);
12
+ };
13
+
14
+ /** All diagnostics go to stderr, leaving stdout available for machine-readable results. */
15
+ export const logger = {
16
+ setLevel(level: LogLevel): void {
17
+ currentLevel = level;
18
+ },
19
+ debug: (...args: unknown[]): void => write('debug', args),
20
+ info: (...args: unknown[]): void => write('info', args),
21
+ warn: (...args: unknown[]): void => write('warn', args),
22
+ error: (...args: unknown[]): void => write('error', args),
23
+ };
package/bin/main.ts CHANGED
@@ -1,4 +1,4 @@
1
- #!/usr/bin/env node
1
+ #!/usr/bin/env -S node --no-warnings
2
2
 
3
3
  import process from 'node:process';
4
4
 
@@ -20,11 +20,18 @@ import {
20
20
  resolveReleaseBaseCommit,
21
21
  resolveRepoUrl,
22
22
  } from './git.js';
23
+ import { logger, logLevels } from './logger.js';
24
+ import { writeJson } from './output.js';
23
25
  import { notifyToSlack } from './slack.js';
24
26
  import { reportTestResults } from './test-report.js';
25
27
  import type { Config } from './types.js';
26
28
  import { readConfigFile } from './utils/config/load-config.js';
27
29
 
30
+ /**
31
+ * Middlewares to be run before every command execution
32
+ */
33
+ const middlewares = [(args: { logLevel: (typeof logLevels)[number] }) => logger.setLevel(args.logLevel)];
34
+
28
35
  /*
29
36
  * Option groups shared across commands. Each is a plain object passed to `.options()`, so a command's
30
37
  * builder composes them by chaining (`y.options(gitRangeOptions).options(actorSelectionOptions)`)
@@ -64,6 +71,12 @@ const buildOptions = {
64
71
 
65
72
  /** Global flags, available to every command. */
66
73
  const globalOptions = {
74
+ 'log-level': {
75
+ type: 'string',
76
+ choices: logLevels,
77
+ default: 'info',
78
+ describe: 'Minimum diagnostic level written to stderr; silent disables logging',
79
+ },
67
80
  'dry-run': { type: 'boolean', default: false },
68
81
  } as const satisfies Record<string, Options>;
69
82
 
@@ -75,53 +88,54 @@ const resolveChangedActors = async (config: Config, { isLatest }: { isLatest: bo
75
88
  // since the branch has no functional changes of its own, there is nothing new to validate.
76
89
  // Exception: if the branch has any functional changes alongside the merge, we must re-test — even
77
90
  // individually validated changes can have novel interactions when combined.
78
- if (hasMergeFromTarget(config.sourceBranch, config.targetBranch)) {
79
- console.error(
91
+ if (await hasMergeFromTarget(config.sourceBranch, config.targetBranch)) {
92
+ logger.info(
80
93
  '[MERGE-FROM-TARGET-OPTIMIZATION]: There is merge from target branch, checking if there are no functional changes in our own branch. If so, we can skip tests',
81
94
  );
82
- const branchOnlyFiles = getBranchOnlyChangedFiles(config.sourceBranch, config.targetBranch);
95
+ const branchOnlyFiles = await getBranchOnlyChangedFiles(config.sourceBranch, config.targetBranch);
83
96
  // Omit baseCommit to get full branch history. Validated functional commits can still interact with merged ones
84
- const allBranchCommits = getCommits({ ...config, baseCommit: undefined });
85
- const branchOnlyActorsChanged = getChangedActors({
97
+ const allBranchCommits = await getCommits({ ...config, baseCommit: undefined });
98
+ const branchOnlyActorsChanged = await getChangedActors({
86
99
  filepathsChanged: branchOnlyFiles,
87
100
  actorConfigs,
88
101
  commits: allBranchCommits,
89
102
  });
90
103
  if (branchOnlyActorsChanged.length === 0) {
91
- console.error('[MERGE-FROM-TARGET-OPTIMIZATION]: Branch itself has no functional changes, skipping tests');
104
+ logger.info('[MERGE-FROM-TARGET-OPTIMIZATION]: Branch itself has no functional changes, skipping tests');
92
105
  return [];
93
106
  }
94
- console.error(
107
+ logger.info(
95
108
  `[MERGE-FROM-TARGET-OPTIMIZATION]: Branch has ${branchOnlyActorsChanged.length} functional changes, cannot optimize, we continue with full check`,
96
109
  );
97
110
  }
98
111
 
99
112
  // If the optimization doesn't apply, we check all branch commits including merges for full coverage. We don't reuse the merge optimization results because here we can apply baseCommit and check merge commits (they might be functional or just cosmetic)
100
- const commits = getCommits(config);
101
- const changedFiles = getChangedFiles(commits);
113
+ const commits = await getCommits(config);
114
+ const changedFiles = await getChangedFiles(commits);
102
115
  return getChangedActors({ filepathsChanged: changedFiles, actorConfigs, isLatest, commits });
103
116
  };
104
117
 
105
118
  await yargs()
106
119
  .scriptName('public-actors-utils')
107
120
  .options(globalOptions)
121
+ .middleware(middlewares)
108
122
  .command(
109
123
  'get-commits',
110
124
  '',
111
125
  (y) => y.options(gitRangeOptions),
112
- (args) => {
113
- const commits = getCommits(args);
114
- console.log(JSON.stringify(commits));
126
+ async (args) => {
127
+ const commits = await getCommits(args);
128
+ writeJson(commits);
115
129
  },
116
130
  )
117
131
  .command(
118
132
  'get-latest-commit',
119
133
  '',
120
134
  (y) => y.options(gitRangeOptions),
121
- (args) => {
122
- const commits = getCommits(args);
135
+ async (args) => {
136
+ const commits = await getCommits(args);
123
137
  if (commits.length > 0) {
124
- console.log(JSON.stringify(commits[commits.length - 1]));
138
+ writeJson(commits[commits.length - 1]);
125
139
  }
126
140
  },
127
141
  )
@@ -129,10 +143,10 @@ await yargs()
129
143
  'get-changed-files',
130
144
  '',
131
145
  (y) => y.options(gitRangeOptions),
132
- (args) => {
133
- const commits = getCommits(args);
134
- const changedFiles = getChangedFiles(commits);
135
- console.log(JSON.stringify(changedFiles));
146
+ async (args) => {
147
+ const commits = await getCommits(args);
148
+ const changedFiles = await getChangedFiles(commits);
149
+ writeJson(changedFiles);
136
150
  },
137
151
  )
138
152
  .command(
@@ -141,7 +155,7 @@ await yargs()
141
155
  (y) => y.options(actorSelectionOptions),
142
156
  async ({ actors, ignore }) => {
143
157
  const actorConfigs = await readConfigFile({ actors, ignore });
144
- console.log(JSON.stringify(actorConfigs));
158
+ writeJson(actorConfigs);
145
159
  },
146
160
  )
147
161
  .command(
@@ -150,7 +164,7 @@ await yargs()
150
164
  (y) => y.options(gitRangeOptions).options(actorSelectionOptions),
151
165
  async (config) => {
152
166
  const actorsChanged = await resolveChangedActors(config, { isLatest: false });
153
- console.log(JSON.stringify(actorsChanged));
167
+ writeJson(actorsChanged);
154
168
  },
155
169
  )
156
170
  .command(
@@ -173,13 +187,13 @@ await yargs()
173
187
  async (config) => {
174
188
  const actorsChanged = await resolveChangedActors(config, { isLatest: false });
175
189
  const builds = await runBuilds({
176
- ...resolveRepoUrl(config.repoUrl),
190
+ ...(await resolveRepoUrl(config.repoUrl)),
177
191
  actorConfigs: actorsChanged,
178
192
  branch: config.sourceBranch.replace('origin/', ''),
179
193
  dryRun: config.dryRun,
180
194
  useDockerCache: config.useDockerCache,
181
195
  });
182
- console.log(JSON.stringify(builds));
196
+ writeJson(builds);
183
197
  },
184
198
  )
185
199
  .command(
@@ -196,19 +210,19 @@ await yargs()
196
210
  .option('report-slack-channel', { type: 'string' })
197
211
  .option('release-slack-channel', { type: 'string' }),
198
212
  async (args) => {
199
- const baseSha = resolveReleaseBaseCommit(args.baseCommit);
200
- const branch = getCurrentBranch();
201
- const changes = getReleaseChanges(baseSha);
213
+ const baseSha = await resolveReleaseBaseCommit(args.baseCommit);
214
+ const branch = await getCurrentBranch();
215
+ const changes = await getReleaseChanges(baseSha);
202
216
  if (!changes) {
203
- console.error(`HEAD is the base commit ${baseSha}, there is nothing new to release`);
217
+ logger.info(`HEAD is the base commit ${baseSha}, there is nothing new to release`);
204
218
  return;
205
219
  }
206
220
  const { commits, changedFiles, changelog } = changes;
207
- const { repoUrl, shouldVerifyRepoUrl } = resolveRepoUrl(args.repoUrl);
221
+ const { repoUrl, shouldVerifyRepoUrl } = await resolveRepoUrl(args.repoUrl);
208
222
 
209
223
  const isLatest = true;
210
224
  const actorConfigs = await readConfigFile(args);
211
- const actorsChanged = getChangedActors({
225
+ const actorsChanged = await getChangedActors({
212
226
  filepathsChanged: changedFiles,
213
227
  actorConfigs,
214
228
  isLatest,
@@ -224,7 +238,7 @@ await yargs()
224
238
  dryRun,
225
239
  useDockerCache: args.useDockerCache,
226
240
  });
227
- console.error(JSON.stringify(builds));
241
+ logger.info(JSON.stringify(builds));
228
242
 
229
243
  await notifyToSlack({
230
244
  changedFiles,
@@ -246,7 +260,7 @@ await yargs()
246
260
  async ({ actors, ignore, dryRun }) => {
247
261
  const actorConfigs = await readConfigFile({ actors, ignore });
248
262
  const builds = await runBuildsFromLocal({ actorConfigs, dryRun });
249
- console.log(JSON.stringify(builds));
263
+ writeJson(builds);
250
264
  },
251
265
  )
252
266
  .command(
@@ -265,11 +279,11 @@ await yargs()
265
279
  // or a missing config file) arrive here as `err`. A malformed selection must fail loudly
266
280
  // rather than silently operate on the wrong set of actors — print the message, no stack.
267
281
  if (err) {
268
- console.error(`[ERROR]: ${err.message}`);
282
+ logger.error(`[ERROR]: ${err.message}`);
269
283
  } else {
270
284
  // Argument-parsing/validation failure — keep yargs' usage output.
271
- console.error(yargsInstance.help());
272
- console.error(`\n${msg}`);
285
+ logger.error(yargsInstance.help());
286
+ logger.error(`\n${msg}`);
273
287
  }
274
288
  process.exit(1);
275
289
  })
package/bin/output.ts ADDED
@@ -0,0 +1,6 @@
1
+ import process from 'node:process';
2
+
3
+ /** CLI results always go to stdout, independently of the diagnostic log level. */
4
+ export const writeJson = (value: unknown): void => {
5
+ process.stdout.write(`${JSON.stringify(value)}\n`);
6
+ };
package/bin/slack.ts CHANGED
@@ -1,5 +1,6 @@
1
1
  import { WebClient } from '@slack/web-api';
2
2
 
3
+ import { logger } from './logger.js';
3
4
  import type { Commit } from './types.js';
4
5
  import { getEnvVar } from './utils.js';
5
6
 
@@ -30,7 +31,7 @@ export const notifyToSlack = async ({
30
31
  const slack = new WebClient(getEnvVar('SLACK_TOKEN_RELEASES_BOT'));
31
32
 
32
33
  if (!changelog) {
33
- console.warn('No new changelog entries found, did you forget to update it?');
34
+ logger.warn('No new changelog entries found, did you forget to update it?');
34
35
  }
35
36
 
36
37
  let shortMessage = `*${repository}* – New release (by ${displayName(author)}):\n\n`;
@@ -38,9 +39,9 @@ export const notifyToSlack = async ({
38
39
  // This one is just for broader public that only cares about public facing changes
39
40
  if (changelog && releaseSlackChannel) {
40
41
  shortMessage += `*Additions to the changelog*:\n\n${changelog}\n`;
41
- console.error(`=========================================`);
42
- console.error(`**Sending slack message to channel**: ${releaseSlackChannel}.\n\n${shortMessage}`);
43
- console.error(`=========================================`);
42
+ logger.info(`=========================================`);
43
+ logger.info(`**Sending slack message to channel**: ${releaseSlackChannel}.\n\n${shortMessage}`);
44
+ logger.info(`=========================================`);
44
45
  if (!dryRun) {
45
46
  await slack.chat.postMessage({
46
47
  channel: releaseSlackChannel,
@@ -60,9 +61,9 @@ export const notifyToSlack = async ({
60
61
 
61
62
  // This one is for devs and project managers that need to know more details
62
63
  if (reportSlackChannel) {
63
- console.error(`=========================================`);
64
- console.error(`Sending slack message to channel: ${reportSlackChannel}.\n\n${longMessage}`);
65
- console.error(`=========================================`);
64
+ logger.info(`=========================================`);
65
+ logger.info(`Sending slack message to channel: ${reportSlackChannel}.\n\n${longMessage}`);
66
+ logger.info(`=========================================`);
66
67
  if (!dryRun) {
67
68
  await slack.chat.postMessage({
68
69
  text: longMessage,
@@ -1,5 +1,6 @@
1
1
  import fs from 'node:fs/promises';
2
2
 
3
+ import { logger } from './logger.js';
3
4
  import { sendSlackMessage } from './slack.js';
4
5
  import { getEnvVar } from './utils.js';
5
6
 
@@ -38,23 +39,23 @@ export const reportTestResults = async ({
38
39
 
39
40
  const failedAssertions: { message: string; runLink: string; actorId: string }[] = [];
40
41
 
41
- console.error();
42
- console.error(`PASSED: ${passed.length}, FAILED: ${failed.length}`);
43
- console.error();
44
- console.error('**************************************************');
45
- console.error('* Successes *');
46
- console.error('**************************************************');
47
- console.error();
42
+ logger.info();
43
+ logger.info(`PASSED: ${passed.length}, FAILED: ${failed.length}`);
44
+ logger.info();
45
+ logger.info('**************************************************');
46
+ logger.info('* Successes *');
47
+ logger.info('**************************************************');
48
+ logger.info();
48
49
  for (const [i, aResult] of passed.entries()) {
49
50
  const { fullName } = aResult;
50
- console.error(`${i + 1}) ${fullName} ... ${aResult.meta.runLink}`);
51
- console.error();
51
+ logger.info(`${i + 1}) ${fullName} ... ${aResult.meta.runLink}`);
52
+ logger.info();
52
53
  }
53
54
 
54
- console.error('**************************************************');
55
- console.error('* Failures *');
56
- console.error('**************************************************');
57
- console.error();
55
+ logger.info('**************************************************');
56
+ logger.info('* Failures *');
57
+ logger.info('**************************************************');
58
+ logger.info();
58
59
  for (const [i, aResult] of failed.entries()) {
59
60
  const { failureMessages, fullName, meta } = aResult;
60
61
  if (failureMessages) {
@@ -66,15 +67,15 @@ export const reportTestResults = async ({
66
67
  })),
67
68
  );
68
69
  }
69
- console.error(`${i + 1}) ${fullName} ... ${meta.runLink}`);
70
- console.error();
70
+ logger.info(`${i + 1}) ${fullName} ... ${meta.runLink}`);
71
+ logger.info();
71
72
  }
72
- console.error();
73
- console.error(`PASSED: ${passed.length}, FAILED: ${failed.length}`);
74
- console.error();
73
+ logger.info();
74
+ logger.info(`PASSED: ${passed.length}, FAILED: ${failed.length}`);
75
+ logger.info();
75
76
 
76
77
  if (!reportSlackChannel) {
77
- console.error(
78
+ logger.info(
78
79
  `Skipping slack notification. If you want to enable it, add --report-slack-channel flag and make sure SLACK_TOKEN_TESTS_BOT env variable is set.`,
79
80
  );
80
81
  return;
@@ -94,8 +95,8 @@ export const reportTestResults = async ({
94
95
  .slice(1)
95
96
  .map(({ message, runLink, actorId }) => `• ${message} --- <${runLink}|${actorId}>`);
96
97
 
97
- console.error('SLACK:', slackMessage);
98
- console.error('\tblocks:', blocks.join('\n\t\t'));
98
+ logger.info('SLACK:', slackMessage);
99
+ logger.info('\tblocks:', blocks.join('\n\t\t'));
99
100
 
100
101
  if (!reportSlackChannel) {
101
102
  return;
@@ -0,0 +1,84 @@
1
+ import z from 'zod';
2
+
3
+ import { safeReadJsonObjectFile } from '../json-file.js';
4
+ import { ExistingFile, type RelativeDir, type RelativeFile } from '../path/repo-relative.js';
5
+ import type { ValidatedActorConfig } from './structures/base.js';
6
+
7
+ const inlineOrPath = z.union([z.string(), z.record(z.string(), z.unknown())]).nullish();
8
+
9
+ const ActorJsonFieldsSchema = z.object({
10
+ dockerContextDir: z.string().default('..'),
11
+ dockerfile: z.string().optional(),
12
+ readme: z.string().nullish(),
13
+ changelog: z.string().nullish(),
14
+ input: inlineOrPath,
15
+ inputSchema: inlineOrPath,
16
+ output: inlineOrPath,
17
+ outputSchema: inlineOrPath,
18
+ webServerSchema: inlineOrPath,
19
+ });
20
+
21
+ export interface ActorJsonPaths {
22
+ file: ExistingFile;
23
+ dockerContextDir: RelativeDir;
24
+ dockerfile?: RelativeFile;
25
+ readme?: RelativeFile;
26
+ changelog?: RelativeFile;
27
+ inputSchema?: RelativeFile;
28
+ outputSchema?: RelativeFile;
29
+ webServerSchema?: RelativeFile;
30
+ }
31
+
32
+ const resolveFile = (actorJsonFile: ExistingFile, field: string, value: unknown): RelativeFile | undefined => {
33
+ if (typeof value !== 'string') return undefined;
34
+ try {
35
+ return actorJsonFile.joinFile(value);
36
+ } catch (error) {
37
+ throw new Error(`Invalid "${field}" in "${actorJsonFile}": ${(error as Error).message}`);
38
+ }
39
+ };
40
+
41
+ export function readActorJson(config: ValidatedActorConfig): ActorJsonPaths {
42
+ const actorJsonPath = config.folder.joinFile('.actor/actor.json');
43
+ let actorJsonFile: ExistingFile;
44
+ try {
45
+ actorJsonFile = ExistingFile.initialize(actorJsonPath);
46
+ } catch {
47
+ throw new Error(
48
+ `Cannot read "${actorJsonPath}". Every actor entry must have a corresponding .actor/actor.json file.`,
49
+ );
50
+ }
51
+
52
+ const json = safeReadJsonObjectFile(actorJsonFile);
53
+ if (!json.success) throw new Error(`Cannot read "${actorJsonFile}". ${json.failure.message}`);
54
+
55
+ const parsed = ActorJsonFieldsSchema.safeParse(json.contents);
56
+ if (!parsed.success) throw new Error(`Invalid "${actorJsonFile}": ${z.prettifyError(parsed.error)}`);
57
+
58
+ const fields = parsed.data;
59
+ let dockerContextDir: RelativeDir;
60
+ try {
61
+ dockerContextDir = actorJsonFile.joinDir(fields.dockerContextDir);
62
+ } catch (error) {
63
+ throw new Error(`Invalid "dockerContextDir" in "${actorJsonFile}": ${(error as Error).message}`);
64
+ }
65
+
66
+ return {
67
+ file: actorJsonFile,
68
+ dockerContextDir,
69
+ dockerfile: resolveFile(actorJsonFile, 'dockerfile', fields.dockerfile),
70
+ readme: resolveFile(actorJsonFile, 'readme', fields.readme),
71
+ changelog: resolveFile(actorJsonFile, 'changelog', fields.changelog),
72
+ inputSchema: resolveFile(
73
+ actorJsonFile,
74
+ fields.input != null ? 'input' : 'inputSchema',
75
+ fields.input ?? fields.inputSchema,
76
+ ),
77
+ outputSchema: resolveFile(
78
+ actorJsonFile,
79
+ fields.output != null ? 'output' : 'outputSchema',
80
+ fields.output ?? fields.outputSchema,
81
+ ),
82
+ webServerSchema: resolveFile(actorJsonFile, 'webServerSchema', fields.webServerSchema),
83
+ };
84
+ }
@@ -1,30 +1,26 @@
1
- import path from 'node:path';
2
-
3
1
  import { selectActors } from '../../actor-filtering.js';
4
- import { isPathWithinScope } from '../../path-utils.js';
5
2
  import type { ActorConfig } from '../../types.js';
6
3
  import { safeReadJsonObjectFile } from '../json-file.js';
4
+ import { type ExistingDir, ExistingFile, type RelativeDir } from '../path/repo-relative.js';
5
+ import { type ActorJsonPaths, readActorJson } from './actor-json.js';
7
6
  import { parseConfigFile } from './parser.js';
8
- import type { ResolvedActorConfig } from './structures/base.js';
7
+ import type { ValidatedActorConfig } from './structures/base.js';
9
8
 
10
9
  export const CONFIG_FILE_NAME = 'apify-test-tools.config.json';
11
10
 
12
11
  // Reading the config happens in three stages, each one swappable on its own:
13
12
  //
14
13
  // 1. file -> plain object (safeReadJsonObjectFile)
15
- // 2. plain object -> ResolvedActorConfig[] (parseConfigFile — picks a strategy, validates
14
+ // 2. plain object -> ValidatedActorConfig[] (parseConfigFile — picks a strategy, validates
16
15
  // and normalizes; see ./parser.ts)
17
- // 3. ResolvedActorConfig -> ActorConfig (loadActorConfig — merges in .actor/actor.json)
16
+ // 3. ValidatedActorConfig -> LoadedActorConfig (loadActorConfig — reads .actor/actor.json)
18
17
 
19
18
  // #region utils
20
19
 
21
- const findOverlappingContextPaths = (contextPaths: string[]): [string, string] | undefined => {
20
+ const findOverlappingContextPaths = (contextPaths: RelativeDir[]): [RelativeDir, RelativeDir] | undefined => {
22
21
  for (let i = 0; i < contextPaths.length; i++) {
23
22
  for (let j = i + 1; j < contextPaths.length; j++) {
24
- if (
25
- isPathWithinScope(contextPaths[i], contextPaths[j]) ||
26
- isPathWithinScope(contextPaths[j], contextPaths[i])
27
- ) {
23
+ if (contextPaths[i].contains(contextPaths[j]) || contextPaths[j].contains(contextPaths[i])) {
28
24
  return [contextPaths[i], contextPaths[j]];
29
25
  }
30
26
  }
@@ -32,9 +28,17 @@ const findOverlappingContextPaths = (contextPaths: string[]): [string, string] |
32
28
  return undefined;
33
29
  };
34
30
 
35
- // The repo root is spelled "" internally but "." in a config file, so report it the way a reader
36
- // would have written it.
37
- const displayFolder = (folder: string): string => folder || '.';
31
+ // Downstream ActorConfig consumers still use "" for the repo root.
32
+ const legacyPath = (dir: RelativeDir): string => (dir.path === '.' ? '' : dir.path);
33
+
34
+ export interface LoadedActorConfig {
35
+ actorFullName: string;
36
+ folder: ExistingDir;
37
+ tokenEnvVar: string;
38
+ actorJson: ActorJsonPaths;
39
+ dockerContextDir: RelativeDir;
40
+ contextPaths: RelativeDir[];
41
+ }
38
42
 
39
43
  // #endregion
40
44
 
@@ -42,7 +46,16 @@ const displayFolder = (folder: string): string => folder || '.';
42
46
 
43
47
  /** Stage 1 — the config file as a plain object, or a failure phrased for whoever wrote it. */
44
48
  const readConfigFileContents = async (): Promise<Record<string, unknown>> => {
45
- const file = safeReadJsonObjectFile(CONFIG_FILE_NAME);
49
+ let configFile: ExistingFile;
50
+ try {
51
+ configFile = new ExistingFile(CONFIG_FILE_NAME);
52
+ } catch {
53
+ throw new Error(
54
+ `Config file "${CONFIG_FILE_NAME}" not found in the current directory. ` +
55
+ `Please create one with the required actor entries.`,
56
+ );
57
+ }
58
+ const file = safeReadJsonObjectFile(configFile);
46
59
  if (file.success) {
47
60
  return file.contents;
48
61
  }
@@ -66,57 +79,44 @@ const readConfigFileContents = async (): Promise<Record<string, unknown>> => {
66
79
  * Stage 3 — resolves one normalized entry against the repo, reading the actor's `.actor/actor.json`
67
80
  * for its `dockerContextDir` and deciding which paths count as the actor's context.
68
81
  */
69
- export const loadActorConfig = (entry: ResolvedActorConfig): ActorConfig => {
82
+ export const loadActorConfig = (entry: ValidatedActorConfig): LoadedActorConfig => {
70
83
  const { folder } = entry;
71
- const actorJsonPath = folder ? `${folder}/.actor/actor.json` : '.actor/actor.json';
72
-
73
- const actorJson = safeReadJsonObjectFile(actorJsonPath);
74
- if (!actorJson.success) {
75
- throw new Error(
76
- `Cannot read "${actorJsonPath}". Every actor entry in "${CONFIG_FILE_NAME}" ` +
77
- `must have a corresponding .actor/actor.json file.`,
78
- );
79
- }
80
-
81
- const actorDotDir = folder ? `${folder}/.actor` : '.actor';
82
- const rawDockerContextDir =
83
- typeof actorJson.contents.dockerContextDir === 'string' ? actorJson.contents.dockerContextDir : '..';
84
- const resolved = path.resolve(process.cwd(), actorDotDir, rawDockerContextDir);
85
- const dockerContextDir = path.relative(process.cwd(), resolved);
86
-
87
- if (dockerContextDir.startsWith('..')) {
88
- throw new Error(
89
- `"dockerContextDir" for folder "${displayFolder(folder)}" resolves outside the repository root. ` +
90
- `Resolved path: "${dockerContextDir}".`,
91
- );
92
- }
93
-
94
- const normalizedDockerContextDir = dockerContextDir === '.' ? '' : dockerContextDir;
95
- const contextPaths = [...(entry.overrideActorContext ?? [normalizedDockerContextDir])];
84
+ const actorJson = readActorJson(entry);
85
+ const { dockerContextDir } = actorJson;
86
+ const contextPaths = [...(entry.overrideActorContext ?? [dockerContextDir])];
96
87
 
97
88
  // The actor's own folder is always part of its context. When an explicit "overrideActorContext"
98
89
  // doesn't already cover it, add it automatically instead of failing the workflow.
99
- if (!contextPaths.some((contextPath) => isPathWithinScope(folder, contextPath))) {
90
+ if (!contextPaths.some((contextPath) => contextPath.contains(folder))) {
100
91
  contextPaths.push(folder);
101
92
  }
102
93
 
103
94
  const overlap = findOverlappingContextPaths(contextPaths);
104
95
  if (overlap) {
105
96
  throw new Error(
106
- `Invalid context paths for folder "${displayFolder(folder)}" in "${CONFIG_FILE_NAME}": ` +
97
+ `Invalid context paths for folder "${folder}" in "${CONFIG_FILE_NAME}": ` +
107
98
  `"${overlap[0]}" and "${overlap[1]}" overlap. Context paths must not be prefixes of one another.`,
108
99
  );
109
100
  }
110
101
 
111
102
  return {
112
103
  actorFullName: entry.actorFullName,
113
- folder,
104
+ folder: entry.folder,
114
105
  tokenEnvVar: entry.tokenEnvVar,
115
- dockerContextDir: normalizedDockerContextDir,
106
+ actorJson,
107
+ dockerContextDir,
116
108
  contextPaths,
117
109
  };
118
110
  };
119
111
 
112
+ const toLegacyActorConfig = (entry: LoadedActorConfig): ActorConfig => ({
113
+ actorFullName: entry.actorFullName,
114
+ folder: legacyPath(entry.folder),
115
+ tokenEnvVar: entry.tokenEnvVar,
116
+ dockerContextDir: legacyPath(entry.dockerContextDir),
117
+ contextPaths: entry.contextPaths.map(legacyPath),
118
+ });
119
+
120
120
  // #endregion
121
121
 
122
122
  export const readConfigFile = async (selection: { actors: string[]; ignore: string[] }): Promise<ActorConfig[]> => {
@@ -126,7 +126,7 @@ export const readConfigFile = async (selection: { actors: string[]; ignore: stri
126
126
  const actorConfigs: ActorConfig[] = [];
127
127
  for (const parsed of parsedConfigs) {
128
128
  // Sequential on purpose: the first actor with a problem should be the one reported.
129
- actorConfigs.push(loadActorConfig(parsed));
129
+ actorConfigs.push(toLegacyActorConfig(loadActorConfig(parsed)));
130
130
  }
131
131
 
132
132
  return selectActors(selection, actorConfigs);