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.
- package/.github/workflows/public_platform-tests-claude-investigate-and-fix.yaml +7 -2
- package/CHANGELOG.md +6 -0
- package/CONTRIBUTING.md +9 -0
- package/bin/build-from-local.ts +8 -7
- package/bin/build.ts +22 -21
- package/bin/diff-changes.ts +11 -10
- package/bin/diff-json-schema.ts +14 -12
- package/bin/git.ts +90 -43
- package/bin/logger.ts +23 -0
- package/bin/main.ts +49 -35
- package/bin/output.ts +6 -0
- package/bin/slack.ts +8 -7
- package/bin/test-report.ts +22 -21
- package/bin/utils/config/actor-json.ts +84 -0
- package/bin/utils/config/load-config.ts +46 -46
- package/bin/utils/config/parser.ts +33 -28
- package/bin/utils/config/structures/base.ts +7 -0
- package/bin/utils/json-file.ts +11 -14
- package/bin/utils/path/repo-relative.ts +156 -0
- package/bin/utils/path/schema.ts +18 -0
- package/bin/utils.ts +61 -34
- package/dist/bin/build-from-local.d.ts +1 -1
- package/dist/bin/build-from-local.d.ts.map +1 -1
- package/dist/bin/build-from-local.js +8 -7
- package/dist/bin/build-from-local.js.map +1 -1
- package/dist/bin/build.d.ts.map +1 -1
- package/dist/bin/build.js +22 -21
- package/dist/bin/build.js.map +1 -1
- package/dist/bin/diff-changes.d.ts +1 -1
- package/dist/bin/diff-changes.d.ts.map +1 -1
- package/dist/bin/diff-changes.js +9 -8
- package/dist/bin/diff-changes.js.map +1 -1
- package/dist/bin/diff-json-schema.d.ts +1 -1
- package/dist/bin/diff-json-schema.d.ts.map +1 -1
- package/dist/bin/diff-json-schema.js +13 -12
- package/dist/bin/diff-json-schema.js.map +1 -1
- package/dist/bin/git.d.ts +10 -10
- package/dist/bin/git.d.ts.map +1 -1
- package/dist/bin/git.js +78 -37
- package/dist/bin/git.js.map +1 -1
- package/dist/bin/logger.d.ts +11 -0
- package/dist/bin/logger.d.ts.map +1 -0
- package/dist/bin/logger.js +20 -0
- package/dist/bin/logger.js.map +1 -0
- package/dist/bin/main.d.ts +1 -1
- package/dist/bin/main.js +48 -35
- package/dist/bin/main.js.map +1 -1
- package/dist/bin/output.d.ts +3 -0
- package/dist/bin/output.d.ts.map +1 -0
- package/dist/bin/output.js +6 -0
- package/dist/bin/output.js.map +1 -0
- package/dist/bin/slack.d.ts.map +1 -1
- package/dist/bin/slack.js +8 -7
- package/dist/bin/slack.js.map +1 -1
- package/dist/bin/test-report.d.ts.map +1 -1
- package/dist/bin/test-report.js +22 -21
- package/dist/bin/test-report.js.map +1 -1
- package/dist/bin/utils/config/actor-json.d.ts +14 -0
- package/dist/bin/utils/config/actor-json.d.ts.map +1 -0
- package/dist/bin/utils/config/actor-json.js +60 -0
- package/dist/bin/utils/config/actor-json.js.map +1 -0
- package/dist/bin/utils/config/load-config.d.ts +12 -2
- package/dist/bin/utils/config/load-config.d.ts.map +1 -1
- package/dist/bin/utils/config/load-config.js +32 -31
- package/dist/bin/utils/config/load-config.js.map +1 -1
- package/dist/bin/utils/config/parser.d.ts +4 -12
- package/dist/bin/utils/config/parser.d.ts.map +1 -1
- package/dist/bin/utils/config/parser.js +24 -23
- package/dist/bin/utils/config/parser.js.map +1 -1
- package/dist/bin/utils/config/structures/base.d.ts +5 -0
- package/dist/bin/utils/config/structures/base.d.ts.map +1 -1
- package/dist/bin/utils/config/structures/base.js.map +1 -1
- package/dist/bin/utils/json-file.d.ts +3 -2
- package/dist/bin/utils/json-file.d.ts.map +1 -1
- package/dist/bin/utils/json-file.js +8 -13
- package/dist/bin/utils/json-file.js.map +1 -1
- package/dist/bin/utils/path/repo-relative.d.ts +35 -0
- package/dist/bin/utils/path/repo-relative.d.ts.map +1 -0
- package/dist/bin/utils/path/repo-relative.js +137 -0
- package/dist/bin/utils/path/repo-relative.js.map +1 -0
- package/dist/bin/utils/path/schema.d.ts +7 -0
- package/dist/bin/utils/path/schema.d.ts.map +1 -0
- package/dist/bin/utils/path/schema.js +16 -0
- package/dist/bin/utils/path/schema.js.map +1 -0
- package/dist/bin/utils.d.ts +14 -3
- package/dist/bin/utils.d.ts.map +1 -1
- package/dist/bin/utils.js +52 -29
- package/dist/bin/utils.js.map +1 -1
- package/dist/test/unit/bin/build-from-local.test.js +3 -39
- package/dist/test/unit/bin/build-from-local.test.js.map +1 -1
- package/dist/test/unit/bin/diff-changes.test.js +97 -96
- package/dist/test/unit/bin/diff-changes.test.js.map +1 -1
- package/dist/test/unit/bin/diff.test.js +52 -48
- package/dist/test/unit/bin/diff.test.js.map +1 -1
- package/dist/test/unit/bin/git-command.test.d.ts +2 -0
- package/dist/test/unit/bin/git-command.test.d.ts.map +1 -0
- package/dist/test/unit/bin/git-command.test.js +54 -0
- package/dist/test/unit/bin/git-command.test.js.map +1 -0
- package/dist/test/unit/bin/git.test.js +211 -92
- package/dist/test/unit/bin/git.test.js.map +1 -1
- package/dist/test/unit/bin/logger.test.d.ts +2 -0
- package/dist/test/unit/bin/logger.test.d.ts.map +1 -0
- package/dist/test/unit/bin/logger.test.js +44 -0
- package/dist/test/unit/bin/logger.test.js.map +1 -0
- package/dist/test/unit/bin/logging.test.d.ts +2 -0
- package/dist/test/unit/bin/logging.test.d.ts.map +1 -0
- package/dist/test/unit/bin/logging.test.js +43 -0
- package/dist/test/unit/bin/logging.test.js.map +1 -0
- package/dist/test/unit/bin/utils/config/load-config.test.js +47 -25
- package/dist/test/unit/bin/utils/config/load-config.test.js.map +1 -1
- package/dist/test/unit/bin/utils/config/parser.test.js +64 -19
- package/dist/test/unit/bin/utils/config/parser.test.js.map +1 -1
- package/dist/test/unit/bin/utils/files.test.js +16 -27
- package/dist/test/unit/bin/utils/files.test.js.map +1 -1
- package/dist/test/unit/bin/utils/path/repo-relative.test.d.ts +2 -0
- package/dist/test/unit/bin/utils/path/repo-relative.test.d.ts.map +1 -0
- package/dist/test/unit/bin/utils/path/repo-relative.test.js +196 -0
- package/dist/test/unit/bin/utils/path/repo-relative.test.js.map +1 -0
- package/dist/test/unit/bin/utils/path/schema.test.d.ts +2 -0
- package/dist/test/unit/bin/utils/path/schema.test.d.ts.map +1 -0
- package/dist/test/unit/bin/utils/path/schema.test.js +52 -0
- package/dist/test/unit/bin/utils/path/schema.test.js.map +1 -0
- package/dist/test/unit/should-built-and-test.test.js +40 -38
- package/dist/test/unit/should-built-and-test.test.js.map +1 -1
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/eslint.config.mjs +9 -1
- package/package.json +4 -4
- package/test/unit/bin/build-from-local.test.ts +3 -53
- package/test/unit/bin/diff-changes.test.ts +97 -96
- package/test/unit/bin/diff.test.ts +53 -48
- package/test/unit/bin/git-command.test.ts +71 -0
- package/test/unit/bin/git.test.ts +246 -89
- package/test/unit/bin/logger.test.ts +55 -0
- package/test/unit/bin/logging.test.ts +52 -0
- package/test/unit/bin/utils/config/load-config.test.ts +63 -26
- package/test/unit/bin/utils/config/parser.test.ts +81 -27
- package/test/unit/bin/utils/files.test.ts +19 -33
- package/test/unit/bin/utils/path/repo-relative.test.ts +250 -0
- package/test/unit/bin/utils/path/schema.test.ts +65 -0
- 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
|
-
|
|
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
|
-
|
|
104
|
+
logger.info('[MERGE-FROM-TARGET-OPTIMIZATION]: Branch itself has no functional changes, skipping tests');
|
|
92
105
|
return [];
|
|
93
106
|
}
|
|
94
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
282
|
+
logger.error(`[ERROR]: ${err.message}`);
|
|
269
283
|
} else {
|
|
270
284
|
// Argument-parsing/validation failure — keep yargs' usage output.
|
|
271
|
-
|
|
272
|
-
|
|
285
|
+
logger.error(yargsInstance.help());
|
|
286
|
+
logger.error(`\n${msg}`);
|
|
273
287
|
}
|
|
274
288
|
process.exit(1);
|
|
275
289
|
})
|
package/bin/output.ts
ADDED
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
|
-
|
|
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
|
-
|
|
42
|
-
|
|
43
|
-
|
|
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
|
-
|
|
64
|
-
|
|
65
|
-
|
|
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,
|
package/bin/test-report.ts
CHANGED
|
@@ -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
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
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
|
-
|
|
51
|
-
|
|
51
|
+
logger.info(`${i + 1}) ${fullName} ... ${aResult.meta.runLink}`);
|
|
52
|
+
logger.info();
|
|
52
53
|
}
|
|
53
54
|
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
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
|
-
|
|
70
|
-
|
|
70
|
+
logger.info(`${i + 1}) ${fullName} ... ${meta.runLink}`);
|
|
71
|
+
logger.info();
|
|
71
72
|
}
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
73
|
+
logger.info();
|
|
74
|
+
logger.info(`PASSED: ${passed.length}, FAILED: ${failed.length}`);
|
|
75
|
+
logger.info();
|
|
75
76
|
|
|
76
77
|
if (!reportSlackChannel) {
|
|
77
|
-
|
|
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
|
-
|
|
98
|
-
|
|
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 {
|
|
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 ->
|
|
14
|
+
// 2. plain object -> ValidatedActorConfig[] (parseConfigFile — picks a strategy, validates
|
|
16
15
|
// and normalizes; see ./parser.ts)
|
|
17
|
-
// 3.
|
|
16
|
+
// 3. ValidatedActorConfig -> LoadedActorConfig (loadActorConfig — reads .actor/actor.json)
|
|
18
17
|
|
|
19
18
|
// #region utils
|
|
20
19
|
|
|
21
|
-
const findOverlappingContextPaths = (contextPaths:
|
|
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
|
-
//
|
|
36
|
-
|
|
37
|
-
|
|
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
|
-
|
|
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:
|
|
82
|
+
export const loadActorConfig = (entry: ValidatedActorConfig): LoadedActorConfig => {
|
|
70
83
|
const { folder } = entry;
|
|
71
|
-
const
|
|
72
|
-
|
|
73
|
-
const
|
|
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) =>
|
|
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 "${
|
|
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
|
-
|
|
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);
|