apify-test-tools 0.10.2-beta.0 → 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.
- package/.gitattributes +2 -0
- package/.github/actions/checkout-restore-dependencies/action.yaml +64 -27
- package/.github/cli/apify-test-tools.mjs +360 -0
- package/.github/scripts/check-major-tag-refs.mjs +1 -1
- package/.github/scripts/mask-config-secrets.sh +17 -0
- package/.github/scripts/run-with-apify-tokens.mjs +37 -12
- package/.github/workflows/_check_code.yaml +25 -0
- package/.github/workflows/_move_major_tag.yaml +3 -81
- package/.github/workflows/_tests.yaml +34 -0
- package/.github/workflows/_update_release_metadata.yaml +0 -12
- package/.github/workflows/manual_move_major_tag.yaml +3 -3
- package/.github/workflows/manual_release_stable.yaml +3 -13
- package/.github/workflows/on_master.yaml +9 -5
- package/.github/workflows/on_pull_request.yaml +3 -48
- package/.github/workflows/public_platform-tests-claude-investigate-and-fix.yaml +13 -2
- package/.github/workflows/public_platform-tests.yaml +24 -1
- package/.github/workflows/public_pr-build-test.yaml +62 -49
- package/.github/workflows/public_push-build-latest.yaml +41 -8
- package/.husky/pre-commit +5 -0
- package/.prettierignore +3 -0
- package/CHANGELOG.md +17 -0
- package/CONTRIBUTING.md +105 -21
- package/README.md +218 -597
- package/bin/affected-actors.ts +198 -0
- package/bin/build-from-local.ts +65 -13
- package/bin/build.ts +123 -41
- package/bin/diff-changes.ts +55 -25
- package/bin/diff-json-schema.ts +5 -6
- package/bin/git.ts +69 -99
- package/bin/main.ts +96 -122
- package/bin/slack.ts +43 -10
- package/bin/stdin-input.ts +65 -0
- package/bin/test-report.ts +6 -9
- package/bin/types.ts +16 -0
- package/dist/bin/affected-actors.d.ts +46 -0
- package/dist/bin/affected-actors.d.ts.map +1 -0
- package/dist/bin/affected-actors.js +135 -0
- package/dist/bin/affected-actors.js.map +1 -0
- package/dist/bin/build-from-local.d.ts +12 -3
- package/dist/bin/build-from-local.d.ts.map +1 -1
- package/dist/bin/build-from-local.js +34 -12
- package/dist/bin/build-from-local.js.map +1 -1
- package/dist/bin/build.d.ts +30 -7
- package/dist/bin/build.d.ts.map +1 -1
- package/dist/bin/build.js +85 -26
- package/dist/bin/build.js.map +1 -1
- package/dist/bin/diff-changes.d.ts +12 -4
- package/dist/bin/diff-changes.d.ts.map +1 -1
- package/dist/bin/diff-changes.js +31 -20
- package/dist/bin/diff-changes.js.map +1 -1
- package/dist/bin/diff-json-schema.d.ts +2 -2
- package/dist/bin/diff-json-schema.d.ts.map +1 -1
- package/dist/bin/diff-json-schema.js +1 -4
- package/dist/bin/diff-json-schema.js.map +1 -1
- package/dist/bin/git.d.ts +23 -21
- package/dist/bin/git.d.ts.map +1 -1
- package/dist/bin/git.js +60 -83
- package/dist/bin/git.js.map +1 -1
- package/dist/bin/main.js +82 -93
- package/dist/bin/main.js.map +1 -1
- package/dist/bin/slack.d.ts +14 -4
- package/dist/bin/slack.d.ts.map +1 -1
- package/dist/bin/slack.js +39 -6
- package/dist/bin/slack.js.map +1 -1
- package/dist/bin/stdin-input.d.ts +17 -0
- package/dist/bin/stdin-input.d.ts.map +1 -0
- package/dist/bin/stdin-input.js +54 -0
- package/dist/bin/stdin-input.js.map +1 -0
- package/dist/bin/test-report.d.ts.map +1 -1
- package/dist/bin/test-report.js +6 -8
- package/dist/bin/test-report.js.map +1 -1
- package/dist/bin/types.d.ts +14 -0
- package/dist/bin/types.d.ts.map +1 -1
- package/dist/lib/extend-expect.d.ts.map +1 -1
- package/dist/lib/extend-expect.js +37 -218
- package/dist/lib/extend-expect.js.map +1 -1
- package/dist/lib/lib.d.ts +1 -2
- package/dist/lib/lib.d.ts.map +1 -1
- package/dist/lib/lib.js +15 -22
- package/dist/lib/lib.js.map +1 -1
- package/dist/lib/run-checks.d.ts +14 -0
- package/dist/lib/run-checks.d.ts.map +1 -0
- package/dist/lib/run-checks.js +82 -0
- package/dist/lib/run-checks.js.map +1 -0
- package/dist/lib/run-test-result.d.ts +3 -6
- package/dist/lib/run-test-result.d.ts.map +1 -1
- package/dist/lib/run-test-result.js +41 -54
- package/dist/lib/run-test-result.js.map +1 -1
- package/dist/test/bundle/cli-bundle.test.d.ts +2 -0
- package/dist/test/bundle/cli-bundle.test.d.ts.map +1 -0
- package/dist/test/bundle/cli-bundle.test.js +190 -0
- package/dist/test/bundle/cli-bundle.test.js.map +1 -0
- package/dist/test/e2e/cli-bundle.e2e.test.d.ts +2 -0
- package/dist/test/e2e/cli-bundle.e2e.test.d.ts.map +1 -0
- package/dist/test/e2e/cli-bundle.e2e.test.js +138 -0
- package/dist/test/e2e/cli-bundle.e2e.test.js.map +1 -0
- package/dist/test/e2e/git-build-release.e2e.test.d.ts +2 -0
- package/dist/test/e2e/git-build-release.e2e.test.d.ts.map +1 -0
- package/dist/test/e2e/git-build-release.e2e.test.js +87 -0
- package/dist/test/e2e/git-build-release.e2e.test.js.map +1 -0
- package/dist/test/e2e/helpers.d.ts +37 -0
- package/dist/test/e2e/helpers.d.ts.map +1 -0
- package/dist/test/e2e/helpers.js +57 -0
- package/dist/test/e2e/helpers.js.map +1 -0
- package/dist/test/unit/bin/affected-actors-cli.test.d.ts +2 -0
- package/dist/test/unit/bin/affected-actors-cli.test.d.ts.map +1 -0
- package/dist/test/unit/bin/affected-actors-cli.test.js +167 -0
- package/dist/test/unit/bin/affected-actors-cli.test.js.map +1 -0
- package/dist/test/unit/bin/affected-actors.test.d.ts +2 -0
- package/dist/test/unit/bin/affected-actors.test.d.ts.map +1 -0
- package/dist/test/unit/bin/affected-actors.test.js +141 -0
- package/dist/test/unit/bin/affected-actors.test.js.map +1 -0
- package/dist/test/unit/bin/build-cli.test.d.ts +2 -0
- package/dist/test/unit/bin/build-cli.test.d.ts.map +1 -0
- package/dist/test/unit/bin/build-cli.test.js +44 -0
- package/dist/test/unit/bin/build-cli.test.js.map +1 -0
- package/dist/test/unit/bin/build-from-local.test.js +65 -1
- package/dist/test/unit/bin/build-from-local.test.js.map +1 -1
- package/dist/test/unit/bin/build.test.js +223 -3
- package/dist/test/unit/bin/build.test.js.map +1 -1
- package/dist/test/unit/bin/compiled-cli.d.ts +6 -0
- package/dist/test/unit/bin/compiled-cli.d.ts.map +1 -0
- package/dist/test/unit/bin/compiled-cli.js +16 -0
- package/dist/test/unit/bin/compiled-cli.js.map +1 -0
- package/dist/test/unit/bin/diff-changes.test.js +205 -85
- package/dist/test/unit/bin/diff-changes.test.js.map +1 -1
- package/dist/test/unit/bin/diff.test.js +25 -28
- package/dist/test/unit/bin/diff.test.js.map +1 -1
- package/dist/test/unit/bin/git.test.js +69 -152
- package/dist/test/unit/bin/git.test.js.map +1 -1
- package/dist/test/unit/bin/slack.test.d.ts +2 -0
- package/dist/test/unit/bin/slack.test.d.ts.map +1 -0
- package/dist/test/unit/bin/slack.test.js +71 -0
- package/dist/test/unit/bin/slack.test.js.map +1 -0
- package/dist/test/unit/bin/stdin-input.test.d.ts +2 -0
- package/dist/test/unit/bin/stdin-input.test.d.ts.map +1 -0
- package/dist/test/unit/bin/stdin-input.test.js +50 -0
- package/dist/test/unit/bin/stdin-input.test.js.map +1 -0
- package/dist/test/unit/github-scripts/mask-config-secrets.test.d.ts +2 -0
- package/dist/test/unit/github-scripts/mask-config-secrets.test.d.ts.map +1 -0
- package/dist/test/unit/github-scripts/mask-config-secrets.test.js +50 -0
- package/dist/test/unit/github-scripts/mask-config-secrets.test.js.map +1 -0
- package/dist/test/unit/github-scripts/run-with-apify-tokens.test.d.ts +2 -0
- package/dist/test/unit/github-scripts/run-with-apify-tokens.test.d.ts.map +1 -0
- package/dist/test/unit/github-scripts/run-with-apify-tokens.test.js +102 -0
- package/dist/test/unit/github-scripts/run-with-apify-tokens.test.js.map +1 -0
- package/dist/test/unit/run-checks.test.d.ts +2 -0
- package/dist/test/unit/run-checks.test.d.ts.map +1 -0
- package/dist/test/unit/run-checks.test.js +62 -0
- package/dist/test/unit/run-checks.test.js.map +1 -0
- package/dist/test/unit/run-test-result.test.d.ts +2 -0
- package/dist/test/unit/run-test-result.test.d.ts.map +1 -0
- package/dist/test/unit/run-test-result.test.js +34 -0
- package/dist/test/unit/run-test-result.test.js.map +1 -0
- package/dist/test/unit/should-built-and-test.test.js +41 -38
- package/dist/test/unit/should-built-and-test.test.js.map +1 -1
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/docs/cli.md +373 -0
- package/docs/lib.md +271 -0
- package/docs/workflows.md +367 -0
- package/lib/extend-expect.ts +48 -258
- package/lib/lib.ts +24 -28
- package/lib/run-checks.ts +101 -0
- package/lib/run-test-result.ts +45 -55
- package/package.json +15 -3
- package/test/bundle/cli-bundle.test.ts +235 -0
- package/test/e2e/cli-bundle.e2e.test.ts +207 -0
- package/test/e2e/git-build-release.e2e.test.ts +113 -0
- package/test/e2e/helpers.ts +82 -0
- package/test/unit/bin/affected-actors-cli.test.ts +201 -0
- package/test/unit/bin/affected-actors.test.ts +182 -0
- package/test/unit/bin/build-cli.test.ts +55 -0
- package/test/unit/bin/build-from-local.test.ts +71 -0
- package/test/unit/bin/build.test.ts +279 -3
- package/test/unit/bin/compiled-cli.ts +17 -0
- package/test/unit/bin/diff-changes.test.ts +229 -85
- package/test/unit/bin/diff.test.ts +25 -28
- package/test/unit/bin/git.test.ts +85 -176
- package/test/unit/bin/slack.test.ts +84 -0
- package/test/unit/bin/stdin-input.test.ts +61 -0
- package/test/unit/github-scripts/mask-config-secrets.test.ts +61 -0
- package/test/unit/github-scripts/run-with-apify-tokens.test.ts +115 -0
- package/test/unit/run-checks.test.ts +76 -0
- package/test/unit/run-test-result.test.ts +44 -0
- package/test/unit/should-built-and-test.test.ts +46 -39
- package/.github/scripts/check-package-version-bump.mjs +0 -74
- package/.github/workflows-package-version +0 -1
package/bin/main.ts
CHANGED
|
@@ -6,25 +6,15 @@ import yargs, { type Options } from 'yargs';
|
|
|
6
6
|
// eslint-disable-next-line import/extensions --- With .js, it cannot find types
|
|
7
7
|
import { hideBin } from 'yargs/helpers';
|
|
8
8
|
|
|
9
|
-
import {
|
|
9
|
+
import { getAffectedActors } from './affected-actors.js';
|
|
10
|
+
import { buildActors, DEFAULT_VERSION_KEYWORD, deleteOldBuilds } from './build.js';
|
|
10
11
|
import { runBuildsFromLocal } from './build-from-local.js';
|
|
11
|
-
import {
|
|
12
|
-
import {
|
|
13
|
-
getBranchOnlyChangedFiles,
|
|
14
|
-
getChangedFiles,
|
|
15
|
-
getCommits,
|
|
16
|
-
getCurrentBranch,
|
|
17
|
-
getReleaseChanges,
|
|
18
|
-
getRepoName,
|
|
19
|
-
hasMergeFromTarget,
|
|
20
|
-
resolveReleaseBaseCommit,
|
|
21
|
-
resolveRepoUrl,
|
|
22
|
-
} from './git.js';
|
|
12
|
+
import { getChangedFiles, getCommits } from './git.js';
|
|
23
13
|
import { logger, logLevels } from './logger.js';
|
|
24
14
|
import { writeJson } from './output.js';
|
|
25
15
|
import { notifyToSlack } from './slack.js';
|
|
16
|
+
import { parseBuildInput, parseReleaseChanges, readStdin } from './stdin-input.js';
|
|
26
17
|
import { reportTestResults } from './test-report.js';
|
|
27
|
-
import type { Config } from './types.js';
|
|
28
18
|
import { readConfigFile } from './utils/config/load-config.js';
|
|
29
19
|
|
|
30
20
|
/**
|
|
@@ -64,9 +54,66 @@ const actorSelectionOptions = {
|
|
|
64
54
|
ignore: { type: 'string', array: true, default: [] as string[] },
|
|
65
55
|
} as const satisfies Record<string, Options>;
|
|
66
56
|
|
|
67
|
-
/**
|
|
57
|
+
/**
|
|
58
|
+
* Commit range for get-affected-actors, which always ends at the checked-out HEAD. `base` and `since` combine:
|
|
59
|
+
* `base` alone is a branch's own commits (a PR), `since` alone is everything after a commit (a release), both is
|
|
60
|
+
* a branch after its last validated commit.
|
|
61
|
+
*/
|
|
62
|
+
const affectedRangeOptions = {
|
|
63
|
+
base: { type: 'string', describe: 'Only count commits on HEAD that are not on this branch' },
|
|
64
|
+
since: {
|
|
65
|
+
type: 'string',
|
|
66
|
+
describe: 'Only count commits after this one. Ignored with --base if it is not on the branch',
|
|
67
|
+
},
|
|
68
|
+
'test-files-glob': {
|
|
69
|
+
type: 'string',
|
|
70
|
+
describe: 'Glob (from the repo root) of test files that count as changes even when .dockerignore lists them',
|
|
71
|
+
},
|
|
72
|
+
} as const satisfies Record<string, Options>;
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* For commands that change something (builds, deletes, Slack posts). A dry run still does every read and check
|
|
76
|
+
* the real run does, credentials included, and only skips the writes. So it needs the same tokens, though
|
|
77
|
+
* read-only ones are enough. Read-only commands don't take it since they have nothing to skip.
|
|
78
|
+
*/
|
|
79
|
+
const dryRunOptions = {
|
|
80
|
+
'dry-run': {
|
|
81
|
+
type: 'boolean',
|
|
82
|
+
default: false,
|
|
83
|
+
describe: 'Do every read and check (tokens included) but change nothing; print what would be done',
|
|
84
|
+
},
|
|
85
|
+
} as const satisfies Record<string, Options>;
|
|
86
|
+
|
|
87
|
+
/** Flags of `build`, which builds exactly the Actors it is given. */
|
|
68
88
|
const buildOptions = {
|
|
69
|
-
'
|
|
89
|
+
'version-number': {
|
|
90
|
+
type: 'string',
|
|
91
|
+
demandOption: true,
|
|
92
|
+
describe: `Version to build, like 0.1, or "${DEFAULT_VERSION_KEYWORD}" for each Actor's default version`,
|
|
93
|
+
coerce: (value: string) => {
|
|
94
|
+
if (value !== DEFAULT_VERSION_KEYWORD && !/^\d+\.\d+$/.test(value)) {
|
|
95
|
+
throw new Error(
|
|
96
|
+
`--version-number must look like 0.1 or be "${DEFAULT_VERSION_KEYWORD}", got "${value}"`,
|
|
97
|
+
);
|
|
98
|
+
}
|
|
99
|
+
return value;
|
|
100
|
+
},
|
|
101
|
+
},
|
|
102
|
+
} as const satisfies Record<string, Options>;
|
|
103
|
+
|
|
104
|
+
/** For the commands that build a version other than the default one, see assertEnvVarsMatchDefaultVersion. */
|
|
105
|
+
const envVarsSyncOptions = {
|
|
106
|
+
'require-env-vars-in-sync': {
|
|
107
|
+
type: 'boolean',
|
|
108
|
+
default: false,
|
|
109
|
+
describe: "Fail unless the built version's env var names match the default version's",
|
|
110
|
+
},
|
|
111
|
+
} as const satisfies Record<string, Options>;
|
|
112
|
+
|
|
113
|
+
/** Where `notify-release` posts the release. */
|
|
114
|
+
const releaseSlackOptions = {
|
|
115
|
+
'report-slack-channel': { type: 'string', describe: 'Channel for the full report: commits and changed files' },
|
|
116
|
+
'release-slack-channel': { type: 'string', describe: 'Channel for the changelog additions only' },
|
|
70
117
|
} as const satisfies Record<string, Options>;
|
|
71
118
|
|
|
72
119
|
/** Global flags, available to every command. */
|
|
@@ -77,44 +124,8 @@ const globalOptions = {
|
|
|
77
124
|
default: 'info',
|
|
78
125
|
describe: 'Minimum diagnostic level written to stderr; silent disables logging',
|
|
79
126
|
},
|
|
80
|
-
'dry-run': { type: 'boolean', default: false },
|
|
81
127
|
} as const satisfies Record<string, Options>;
|
|
82
128
|
|
|
83
|
-
const resolveChangedActors = async (config: Config, { isLatest }: { isLatest: boolean }) => {
|
|
84
|
-
const actorConfigs = await readConfigFile(config);
|
|
85
|
-
|
|
86
|
-
// This is an optimization for the common case where a branch only has cosmetic changes but had to smerge in
|
|
87
|
-
// functional changes from master (being up-to-date is a CI requirement). Master is already validated, and
|
|
88
|
-
// since the branch has no functional changes of its own, there is nothing new to validate.
|
|
89
|
-
// Exception: if the branch has any functional changes alongside the merge, we must re-test — even
|
|
90
|
-
// individually validated changes can have novel interactions when combined.
|
|
91
|
-
if (await hasMergeFromTarget(config.sourceBranch, config.targetBranch)) {
|
|
92
|
-
logger.info(
|
|
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',
|
|
94
|
-
);
|
|
95
|
-
const branchOnlyFiles = await getBranchOnlyChangedFiles(config.sourceBranch, config.targetBranch);
|
|
96
|
-
// Omit baseCommit to get full branch history. Validated functional commits can still interact with merged ones
|
|
97
|
-
const allBranchCommits = await getCommits({ ...config, baseCommit: undefined });
|
|
98
|
-
const branchOnlyActorsChanged = await getChangedActors({
|
|
99
|
-
filepathsChanged: branchOnlyFiles,
|
|
100
|
-
actorConfigs,
|
|
101
|
-
commits: allBranchCommits,
|
|
102
|
-
});
|
|
103
|
-
if (branchOnlyActorsChanged.length === 0) {
|
|
104
|
-
logger.info('[MERGE-FROM-TARGET-OPTIMIZATION]: Branch itself has no functional changes, skipping tests');
|
|
105
|
-
return [];
|
|
106
|
-
}
|
|
107
|
-
logger.info(
|
|
108
|
-
`[MERGE-FROM-TARGET-OPTIMIZATION]: Branch has ${branchOnlyActorsChanged.length} functional changes, cannot optimize, we continue with full check`,
|
|
109
|
-
);
|
|
110
|
-
}
|
|
111
|
-
|
|
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)
|
|
113
|
-
const commits = await getCommits(config);
|
|
114
|
-
const changedFiles = await getChangedFiles(commits);
|
|
115
|
-
return getChangedActors({ filepathsChanged: changedFiles, actorConfigs, isLatest, commits });
|
|
116
|
-
};
|
|
117
|
-
|
|
118
129
|
await yargs()
|
|
119
130
|
.scriptName('public-actors-utils')
|
|
120
131
|
.options(globalOptions)
|
|
@@ -160,11 +171,10 @@ await yargs()
|
|
|
160
171
|
)
|
|
161
172
|
.command(
|
|
162
173
|
'get-affected-actors',
|
|
163
|
-
'',
|
|
164
|
-
(y) => y.options(
|
|
165
|
-
async (
|
|
166
|
-
|
|
167
|
-
writeJson(actorsChanged);
|
|
174
|
+
'Find the Actors whose context changed in a commit range ending at the checked-out HEAD',
|
|
175
|
+
(y) => y.options(affectedRangeOptions).options(actorSelectionOptions),
|
|
176
|
+
async (args) => {
|
|
177
|
+
writeJson(await getAffectedActors(args));
|
|
168
178
|
},
|
|
169
179
|
)
|
|
170
180
|
.command(
|
|
@@ -172,6 +182,7 @@ await yargs()
|
|
|
172
182
|
'',
|
|
173
183
|
(y) =>
|
|
174
184
|
y
|
|
185
|
+
.options(dryRunOptions)
|
|
175
186
|
.option('report-file', { type: 'string', demandOption: true })
|
|
176
187
|
.option('report-slack-channel', { type: 'string' })
|
|
177
188
|
.option('job-url', { type: 'string' })
|
|
@@ -182,94 +193,57 @@ await yargs()
|
|
|
182
193
|
)
|
|
183
194
|
.command(
|
|
184
195
|
'build',
|
|
185
|
-
'',
|
|
186
|
-
(y) => y.options(
|
|
187
|
-
async (
|
|
188
|
-
const
|
|
189
|
-
const
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
196
|
+
'Build the Actors listed on stdin, as {"actors": [{"actorFullName": ...}]}, from the checked-out branch',
|
|
197
|
+
(y) => y.options(buildOptions).options(repoUrlOptions).options(envVarsSyncOptions).options(dryRunOptions),
|
|
198
|
+
async (args) => {
|
|
199
|
+
const input = parseBuildInput(await readStdin());
|
|
200
|
+
const names = input.actors.map(({ actorFullName }) => actorFullName);
|
|
201
|
+
// An empty --actors selects every Actor, but an empty list here means there is nothing to build
|
|
202
|
+
const actorConfigs = names.length > 0 ? await readConfigFile({ actors: names, ignore: [] }) : [];
|
|
203
|
+
const builds = await buildActors({
|
|
204
|
+
actorConfigs,
|
|
205
|
+
versionNumber: args.versionNumber,
|
|
206
|
+
repoUrl: args.repoUrl,
|
|
207
|
+
dryRun: args.dryRun,
|
|
208
|
+
requireEnvVarsInSync: args.requireEnvVarsInSync,
|
|
195
209
|
});
|
|
196
210
|
writeJson(builds);
|
|
197
211
|
},
|
|
198
212
|
)
|
|
199
213
|
.command(
|
|
200
|
-
'release',
|
|
201
|
-
'',
|
|
202
|
-
(y) =>
|
|
203
|
-
y
|
|
204
|
-
.options(actorSelectionOptions)
|
|
205
|
-
.options(buildOptions)
|
|
206
|
-
.options(repoUrlOptions)
|
|
207
|
-
// The last commit that is already released, everything after it up to HEAD gets released.
|
|
208
|
-
// Required, see the README section "Releasing Actors" for how to set it for each merge strategy.
|
|
209
|
-
.option('base-commit', { type: 'string', demandOption: true })
|
|
210
|
-
.option('report-slack-channel', { type: 'string' })
|
|
211
|
-
.option('release-slack-channel', { type: 'string' }),
|
|
214
|
+
'notify-release',
|
|
215
|
+
'Announce a release on Slack, from the get-affected-actors output on stdin',
|
|
216
|
+
(y) => y.options(releaseSlackOptions).options(repoUrlOptions).options(dryRunOptions),
|
|
212
217
|
async (args) => {
|
|
213
|
-
const
|
|
214
|
-
const branch = await getCurrentBranch();
|
|
215
|
-
const changes = await getReleaseChanges(baseSha);
|
|
216
|
-
if (!changes) {
|
|
217
|
-
logger.info(`HEAD is the base commit ${baseSha}, there is nothing new to release`);
|
|
218
|
-
return;
|
|
219
|
-
}
|
|
220
|
-
const { commits, changedFiles, changelog } = changes;
|
|
221
|
-
const { repoUrl, shouldVerifyRepoUrl } = await resolveRepoUrl(args.repoUrl);
|
|
222
|
-
|
|
223
|
-
const isLatest = true;
|
|
224
|
-
const actorConfigs = await readConfigFile(args);
|
|
225
|
-
const actorsChanged = await getChangedActors({
|
|
226
|
-
filepathsChanged: changedFiles,
|
|
227
|
-
actorConfigs,
|
|
228
|
-
isLatest,
|
|
229
|
-
commits,
|
|
230
|
-
});
|
|
231
|
-
const { dryRun, reportSlackChannel, releaseSlackChannel } = args;
|
|
232
|
-
const builds = await runBuilds({
|
|
233
|
-
isLatest,
|
|
234
|
-
repoUrl,
|
|
235
|
-
shouldVerifyRepoUrl,
|
|
236
|
-
actorConfigs: actorsChanged,
|
|
237
|
-
branch,
|
|
238
|
-
dryRun,
|
|
239
|
-
useDockerCache: args.useDockerCache,
|
|
240
|
-
});
|
|
241
|
-
logger.info(JSON.stringify(builds));
|
|
242
|
-
|
|
218
|
+
const { commits, changedFiles, changelog } = parseReleaseChanges(await readStdin());
|
|
243
219
|
await notifyToSlack({
|
|
244
|
-
changedFiles,
|
|
245
220
|
commits,
|
|
221
|
+
changedFiles,
|
|
246
222
|
changelog,
|
|
247
|
-
|
|
248
|
-
dryRun,
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
reportSlackChannel,
|
|
252
|
-
releaseSlackChannel,
|
|
223
|
+
repoUrl: args.repoUrl,
|
|
224
|
+
dryRun: args.dryRun,
|
|
225
|
+
reportSlackChannel: args.reportSlackChannel,
|
|
226
|
+
releaseSlackChannel: args.releaseSlackChannel,
|
|
253
227
|
});
|
|
254
228
|
},
|
|
255
229
|
)
|
|
256
230
|
.command(
|
|
257
231
|
'build-from-local',
|
|
258
232
|
'',
|
|
259
|
-
(y) => y.options(actorSelectionOptions),
|
|
260
|
-
async ({ actors, ignore, dryRun }) => {
|
|
233
|
+
(y) => y.options(actorSelectionOptions).options(envVarsSyncOptions).options(dryRunOptions),
|
|
234
|
+
async ({ actors, ignore, dryRun, requireEnvVarsInSync }) => {
|
|
261
235
|
const actorConfigs = await readConfigFile({ actors, ignore });
|
|
262
|
-
const builds = await runBuildsFromLocal({ actorConfigs, dryRun });
|
|
236
|
+
const builds = await runBuildsFromLocal({ actorConfigs, dryRun, requireEnvVarsInSync });
|
|
263
237
|
writeJson(builds);
|
|
264
238
|
},
|
|
265
239
|
)
|
|
266
240
|
.command(
|
|
267
241
|
'delete-old-builds',
|
|
268
242
|
'',
|
|
269
|
-
(y) => y.options(actorSelectionOptions),
|
|
270
|
-
async ({ actors, ignore }) => {
|
|
243
|
+
(y) => y.options(actorSelectionOptions).options(dryRunOptions),
|
|
244
|
+
async ({ actors, ignore, dryRun }) => {
|
|
271
245
|
const actorConfigs = await readConfigFile({ actors, ignore });
|
|
272
|
-
await deleteOldBuilds(actorConfigs);
|
|
246
|
+
await deleteOldBuilds(actorConfigs, { dryRun });
|
|
273
247
|
},
|
|
274
248
|
)
|
|
275
249
|
.strictCommands()
|
package/bin/slack.ts
CHANGED
|
@@ -1,46 +1,80 @@
|
|
|
1
1
|
import { WebClient } from '@slack/web-api';
|
|
2
2
|
|
|
3
|
+
import { getRepoName, resolveRepoUrl } from './git.js';
|
|
3
4
|
import { logger } from './logger.js';
|
|
4
5
|
import type { Commit } from './types.js';
|
|
5
6
|
import { getEnvVar } from './utils.js';
|
|
6
7
|
|
|
7
8
|
type NotifyToSlackOptions = {
|
|
8
|
-
repository: string;
|
|
9
9
|
changedFiles: string[];
|
|
10
10
|
changelog: string | null;
|
|
11
11
|
commits: Commit[];
|
|
12
|
+
// Defaults to the `origin` remote, see resolveRepoUrl
|
|
13
|
+
repoUrl?: string;
|
|
12
14
|
dryRun: boolean;
|
|
13
|
-
author: string;
|
|
14
15
|
releaseSlackChannel?: string;
|
|
15
16
|
reportSlackChannel?: string;
|
|
16
17
|
};
|
|
17
18
|
|
|
19
|
+
/**
|
|
20
|
+
* Creates the client the real run posts with. A dry run posts nothing but still requires the token and checks
|
|
21
|
+
* Slack accepts it (`auth.test` is read-only), so it fails wherever the real run would.
|
|
22
|
+
*/
|
|
23
|
+
export const getSlackClient = async (tokenEnvVar: string, dryRun: boolean): Promise<WebClient> => {
|
|
24
|
+
const slack = new WebClient(getEnvVar(tokenEnvVar));
|
|
25
|
+
if (dryRun) {
|
|
26
|
+
try {
|
|
27
|
+
await slack.auth.test();
|
|
28
|
+
} catch (err) {
|
|
29
|
+
throw new Error(`${tokenEnvVar} was rejected by Slack: ${(err as Error).message}`);
|
|
30
|
+
}
|
|
31
|
+
logger.info(`[DRY RUN] ${tokenEnvVar} is valid`);
|
|
32
|
+
}
|
|
33
|
+
return slack;
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
export const sendingLabel = (dryRun: boolean) => (dryRun ? '[DRY RUN] Would send' : 'Sending');
|
|
37
|
+
|
|
18
38
|
// Git authors come as "Name<email>", Slack only needs the name
|
|
19
39
|
const displayName = (author: string) => author.replace(/\s*<[^>]*>$/, '');
|
|
20
40
|
|
|
41
|
+
/**
|
|
42
|
+
* Posts what a release changed: its commits, changed files and changelog, as get-affected-actors reports them.
|
|
43
|
+
* Posts nothing when there are no commits, which means there is nothing new to release.
|
|
44
|
+
*/
|
|
21
45
|
export const notifyToSlack = async ({
|
|
22
46
|
changedFiles,
|
|
23
47
|
commits,
|
|
24
48
|
changelog,
|
|
25
|
-
|
|
49
|
+
repoUrl,
|
|
26
50
|
dryRun,
|
|
27
|
-
author,
|
|
28
51
|
releaseSlackChannel,
|
|
29
52
|
reportSlackChannel,
|
|
30
53
|
}: NotifyToSlackOptions) => {
|
|
31
|
-
|
|
32
|
-
|
|
54
|
+
if (commits.length === 0) {
|
|
55
|
+
logger.info('No new commits, so there is no release to announce');
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
33
58
|
if (!changelog) {
|
|
34
59
|
logger.warn('No new changelog entries found, did you forget to update it?');
|
|
35
60
|
}
|
|
36
61
|
|
|
62
|
+
if (!releaseSlackChannel && !reportSlackChannel) {
|
|
63
|
+
logger.info('Skipping slack notification, no --release-slack-channel or --report-slack-channel passed');
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
const slack = await getSlackClient('SLACK_TOKEN_RELEASES_BOT', dryRun);
|
|
67
|
+
|
|
68
|
+
const repository = getRepoName((await resolveRepoUrl(repoUrl)).repoUrl);
|
|
69
|
+
// The newest commit, same as the pushed head commit
|
|
70
|
+
const { author } = commits[commits.length - 1];
|
|
37
71
|
let shortMessage = `*${repository}* – New release (by ${displayName(author)}):\n\n`;
|
|
38
72
|
|
|
39
73
|
// This one is just for broader public that only cares about public facing changes
|
|
40
74
|
if (changelog && releaseSlackChannel) {
|
|
41
75
|
shortMessage += `*Additions to the changelog*:\n\n${changelog}\n`;
|
|
42
76
|
logger.info(`=========================================`);
|
|
43
|
-
logger.info(
|
|
77
|
+
logger.info(`**${sendingLabel(dryRun)} slack message to channel**: ${releaseSlackChannel}.\n\n${shortMessage}`);
|
|
44
78
|
logger.info(`=========================================`);
|
|
45
79
|
if (!dryRun) {
|
|
46
80
|
await slack.chat.postMessage({
|
|
@@ -62,7 +96,7 @@ export const notifyToSlack = async ({
|
|
|
62
96
|
// This one is for devs and project managers that need to know more details
|
|
63
97
|
if (reportSlackChannel) {
|
|
64
98
|
logger.info(`=========================================`);
|
|
65
|
-
logger.info(
|
|
99
|
+
logger.info(`${sendingLabel(dryRun)} slack message to channel: ${reportSlackChannel}.\n\n${longMessage}`);
|
|
66
100
|
logger.info(`=========================================`);
|
|
67
101
|
if (!dryRun) {
|
|
68
102
|
await slack.chat.postMessage({
|
|
@@ -73,8 +107,7 @@ export const notifyToSlack = async ({
|
|
|
73
107
|
}
|
|
74
108
|
};
|
|
75
109
|
|
|
76
|
-
export const sendSlackMessage = async (channel: string, text: string, blocks: string[]
|
|
77
|
-
const slack = new WebClient(token);
|
|
110
|
+
export const sendSlackMessage = async (slack: WebClient, channel: string, text: string, blocks: string[]) => {
|
|
78
111
|
const { ts } = await slack.chat.postMessage({ text, channel });
|
|
79
112
|
if (blocks.length > 0 && ts) {
|
|
80
113
|
await slack.chat.postMessage({
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import process from 'node:process';
|
|
2
|
+
|
|
3
|
+
import z, { prettifyError, type ZodType } from 'zod';
|
|
4
|
+
|
|
5
|
+
export const readStdin = async (): Promise<string> => {
|
|
6
|
+
// Nothing piped in: fail right away instead of waiting for someone to type
|
|
7
|
+
if (process.stdin.isTTY) {
|
|
8
|
+
throw new Error('Expected JSON on stdin, but nothing was piped in.');
|
|
9
|
+
}
|
|
10
|
+
let raw = '';
|
|
11
|
+
for await (const chunk of process.stdin) {
|
|
12
|
+
raw += chunk;
|
|
13
|
+
}
|
|
14
|
+
return raw;
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
const parseStdinJson = <T>(
|
|
18
|
+
raw: string,
|
|
19
|
+
schema: ZodType<T>,
|
|
20
|
+
{ expected, example }: { expected: string; example: string },
|
|
21
|
+
) => {
|
|
22
|
+
// Empty input is an error, not "nothing to do": it is what a failed command earlier in the pipe leaves behind
|
|
23
|
+
if (!raw.trim()) {
|
|
24
|
+
throw new Error(`Expected ${expected} as JSON on stdin, got nothing (${example}).`);
|
|
25
|
+
}
|
|
26
|
+
let json: unknown;
|
|
27
|
+
try {
|
|
28
|
+
json = JSON.parse(raw);
|
|
29
|
+
} catch (error) {
|
|
30
|
+
throw new Error(`The input on stdin is not valid JSON: ${(error as Error).message}`);
|
|
31
|
+
}
|
|
32
|
+
const result = schema.safeParse(json);
|
|
33
|
+
if (!result.success) {
|
|
34
|
+
throw new Error(`The input on stdin is not ${expected} (${example}):\n${prettifyError(result.error)}`);
|
|
35
|
+
}
|
|
36
|
+
return result.data;
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* What `build` reads: the Actors to build. Only their names are read, the config file stays the source of truth
|
|
41
|
+
* for everything else about them. Other fields are ignored, so a get-affected-actors document only needs its Actor
|
|
42
|
+
* list picked, e.g. `jq '{ actors: .functionallyChangedActors }'`.
|
|
43
|
+
*/
|
|
44
|
+
const buildInputSchema = z.object({
|
|
45
|
+
actors: z.array(z.object({ actorFullName: z.string() })),
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
export const parseBuildInput = (raw: string) =>
|
|
49
|
+
parseStdinJson(raw, buildInputSchema, {
|
|
50
|
+
expected: 'the Actors to build',
|
|
51
|
+
example: `e.g. echo '{"actors":[{"actorFullName":"owner/name"}]}' | apify-test-tools build --version-number 0.1`,
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
/** What `notify-release` reads: the parts of a get-affected-actors document that describe what changed. */
|
|
55
|
+
const releaseChangesSchema = z.object({
|
|
56
|
+
commits: z.array(z.object({ sha: z.string(), author: z.string(), date: z.string(), message: z.string() })),
|
|
57
|
+
changedFiles: z.array(z.string()),
|
|
58
|
+
changelog: z.string().nullable(),
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
export const parseReleaseChanges = (raw: string) =>
|
|
62
|
+
parseStdinJson(raw, releaseChangesSchema, {
|
|
63
|
+
expected: 'the output of get-affected-actors',
|
|
64
|
+
example: 'e.g. apify-test-tools get-affected-actors --since <sha> | apify-test-tools notify-release',
|
|
65
|
+
});
|
package/bin/test-report.ts
CHANGED
|
@@ -1,8 +1,7 @@
|
|
|
1
1
|
import fs from 'node:fs/promises';
|
|
2
2
|
|
|
3
3
|
import { logger } from './logger.js';
|
|
4
|
-
import { sendSlackMessage } from './slack.js';
|
|
5
|
-
import { getEnvVar } from './utils.js';
|
|
4
|
+
import { getSlackClient, sendingLabel, sendSlackMessage } from './slack.js';
|
|
6
5
|
|
|
7
6
|
interface ReportTestResultsOptions {
|
|
8
7
|
reportFile: string;
|
|
@@ -80,6 +79,9 @@ export const reportTestResults = async ({
|
|
|
80
79
|
);
|
|
81
80
|
return;
|
|
82
81
|
}
|
|
82
|
+
// Checked before knowing if there's anything to send, so a broken token shows up on a green run
|
|
83
|
+
// (and in dry runs) rather than on the day tests fail
|
|
84
|
+
const slack = await getSlackClient('SLACK_TOKEN_TESTS_BOT', dryRun);
|
|
83
85
|
|
|
84
86
|
if (failedAssertions.length === 0) {
|
|
85
87
|
return;
|
|
@@ -95,16 +97,11 @@ export const reportTestResults = async ({
|
|
|
95
97
|
.slice(1)
|
|
96
98
|
.map(({ message, runLink, actorId }) => `• ${message} --- <${runLink}|${actorId}>`);
|
|
97
99
|
|
|
98
|
-
logger.info(
|
|
100
|
+
logger.info(`${sendingLabel(dryRun)} slack message to channel ${reportSlackChannel}:`, slackMessage);
|
|
99
101
|
logger.info('\tblocks:', blocks.join('\n\t\t'));
|
|
100
102
|
|
|
101
|
-
if (!reportSlackChannel) {
|
|
102
|
-
return;
|
|
103
|
-
}
|
|
104
|
-
|
|
105
103
|
if (!dryRun) {
|
|
106
|
-
|
|
107
|
-
await sendSlackMessage(reportSlackChannel, slackMessage, blocks, slackToken);
|
|
104
|
+
await sendSlackMessage(slack, reportSlackChannel, slackMessage, blocks);
|
|
108
105
|
}
|
|
109
106
|
};
|
|
110
107
|
|
package/bin/types.ts
CHANGED
|
@@ -7,6 +7,13 @@ export interface Config {
|
|
|
7
7
|
baseCommit?: string;
|
|
8
8
|
actors: string[];
|
|
9
9
|
ignore: string[];
|
|
10
|
+
testFilesGlob?: string;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/** The changes from `fromRef` (excluded) to `toRef`, compared as trees like `git diff fromRef toRef`. */
|
|
14
|
+
export interface GitRange {
|
|
15
|
+
fromRef: string;
|
|
16
|
+
toRef: string;
|
|
10
17
|
}
|
|
11
18
|
|
|
12
19
|
export type Commit = {
|
|
@@ -23,6 +30,15 @@ export interface BuildData {
|
|
|
23
30
|
buildNumber: string;
|
|
24
31
|
}
|
|
25
32
|
|
|
33
|
+
/**
|
|
34
|
+
* What a dry run writes to stdout instead of the command's real result. It's an object, not an array, so
|
|
35
|
+
* it can't be mistaken for real builds: `ACTOR_BUILDS` rejects it before any test runs.
|
|
36
|
+
*/
|
|
37
|
+
export interface DryRunPlan<T> {
|
|
38
|
+
dryRun: true;
|
|
39
|
+
builds: T[];
|
|
40
|
+
}
|
|
41
|
+
|
|
26
42
|
export interface ActorConfig {
|
|
27
43
|
actorFullName: string;
|
|
28
44
|
folder: ExistingDir;
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import type { Commit } from './types.js';
|
|
2
|
+
interface AffectedActorsOptions {
|
|
3
|
+
/**
|
|
4
|
+
* Base usually points to the master branch
|
|
5
|
+
* Only what HEAD changes compared to base counts, like `git diff base...HEAD` or GitHub's "Files changed" tab:
|
|
6
|
+
* the branch's own changes, not what it merged in from base.
|
|
7
|
+
*/
|
|
8
|
+
base?: string;
|
|
9
|
+
/**
|
|
10
|
+
* With `base` it's the last validated commit of the branch: Actors that didn't change after it are left out.
|
|
11
|
+
* It's ignored when it is HEAD (a rerun) or isn't in the local history. Without `base` everything after it
|
|
12
|
+
* counts, and it must be an ancestor of HEAD (e.g. the last release).
|
|
13
|
+
*/
|
|
14
|
+
since?: string;
|
|
15
|
+
/** Glob relative to the repo root. Matching files count as changes even when .dockerignore lists them. */
|
|
16
|
+
testFilesGlob?: string;
|
|
17
|
+
actors: string[];
|
|
18
|
+
ignore: string[];
|
|
19
|
+
}
|
|
20
|
+
/** The JSON document `get-affected-actors` prints. Versioned, since other commands and workflows consume it. */
|
|
21
|
+
interface AffectedActors {
|
|
22
|
+
version: 1;
|
|
23
|
+
head: string;
|
|
24
|
+
/** The commits, changed files and changelog describe the whole range, `--since` with `--base` only narrows the Actors. */
|
|
25
|
+
commits: Commit[];
|
|
26
|
+
changedFiles: string[];
|
|
27
|
+
changelog: string | null;
|
|
28
|
+
/**
|
|
29
|
+
* Actors with at least one functional change. They need a new build and tests. Only the names: the config file
|
|
30
|
+
* stays the source of truth for the rest.
|
|
31
|
+
*/
|
|
32
|
+
functionallyChangedActors: {
|
|
33
|
+
actorFullName: string;
|
|
34
|
+
}[];
|
|
35
|
+
/** Actors whose changes are all cosmetic (README, CHANGELOG, cosmetic .actor JSON). They only need a release build. */
|
|
36
|
+
cosmeticallyChangedActors: {
|
|
37
|
+
actorFullName: string;
|
|
38
|
+
}[];
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Finds the Actors whose context changed in the given range. `base` selects the branch's own changes
|
|
42
|
+
* (a PR), `since` the changes after a known commit (a release, or the last validated commit of a PR).
|
|
43
|
+
*/
|
|
44
|
+
export declare const getAffectedActors: ({ base, since: sinceOption, testFilesGlob, actors, ignore, }: AffectedActorsOptions) => Promise<AffectedActors>;
|
|
45
|
+
export {};
|
|
46
|
+
//# sourceMappingURL=affected-actors.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"affected-actors.d.ts","sourceRoot":"","sources":["../../bin/affected-actors.ts"],"names":[],"mappings":"AAWA,OAAO,KAAK,EAAe,MAAM,EAAY,MAAM,YAAY,CAAC;AAMhE,UAAU,qBAAqB;IAC3B;;;;OAIG;IACH,IAAI,CAAC,EAAE,MAAM,CAAC;IACd;;;;OAIG;IACH,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,0GAA0G;IAC1G,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,MAAM,EAAE,MAAM,EAAE,CAAC;IACjB,MAAM,EAAE,MAAM,EAAE,CAAC;CACpB;AAED,gHAAgH;AAChH,UAAU,cAAc;IACpB,OAAO,EAAE,CAAC,CAAC;IACX,IAAI,EAAE,MAAM,CAAC;IACb,0HAA0H;IAC1H,OAAO,EAAE,MAAM,EAAE,CAAC;IAClB,YAAY,EAAE,MAAM,EAAE,CAAC;IACvB,SAAS,EAAE,MAAM,GAAG,IAAI,CAAC;IACzB;;;OAGG;IACH,yBAAyB,EAAE;QAAE,aAAa,EAAE,MAAM,CAAA;KAAE,EAAE,CAAC;IACvD,uHAAuH;IACvH,yBAAyB,EAAE;QAAE,aAAa,EAAE,MAAM,CAAA;KAAE,EAAE,CAAC;CAC1D;AAyFD;;;GAGG;AACH,eAAO,MAAM,iBAAiB,GAAU,8DAMrC,qBAAqB,KAAG,OAAO,CAAC,cAAc,CA+ChD,CAAC"}
|