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

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 (189) hide show
  1. package/.gitattributes +2 -0
  2. package/.github/actionlint.yaml +11 -0
  3. package/.github/actions/checkout-restore-dependencies/action.yaml +64 -28
  4. package/.github/cli/apify-test-tools.mjs +360 -0
  5. package/.github/scripts/mask-config-secrets.sh +17 -0
  6. package/.github/scripts/run-with-apify-tokens.mjs +37 -12
  7. package/.github/workflows/_check_code.yaml +25 -16
  8. package/.github/workflows/_move_major_tag.yaml +5 -83
  9. package/.github/workflows/_tests.yaml +34 -0
  10. package/.github/workflows/_update_release_metadata.yaml +0 -12
  11. package/.github/workflows/manual_move_major_tag.yaml +3 -3
  12. package/.github/workflows/manual_release_stable.yaml +3 -13
  13. package/.github/workflows/on_master.yaml +9 -5
  14. package/.github/workflows/on_pull_request.yaml +3 -48
  15. package/.github/workflows/public_platform-tests-claude-investigate-and-fix.yaml +16 -3
  16. package/.github/workflows/public_platform-tests.yaml +27 -3
  17. package/.github/workflows/public_pr-build-test.yaml +67 -29
  18. package/.github/workflows/public_push-build-latest.yaml +44 -10
  19. package/.github/workflows/public_review.yaml +5 -4
  20. package/.husky/pre-commit +5 -0
  21. package/.prettierignore +3 -0
  22. package/CHANGELOG.md +17 -0
  23. package/CONTRIBUTING.md +112 -24
  24. package/README.md +218 -600
  25. package/bin/affected-actors.ts +198 -0
  26. package/bin/build-from-local.ts +65 -13
  27. package/bin/build.ts +123 -41
  28. package/bin/diff-changes.ts +32 -23
  29. package/bin/diff-json-schema.ts +5 -6
  30. package/bin/git.ts +69 -99
  31. package/bin/main.ts +96 -135
  32. package/bin/slack.ts +43 -10
  33. package/bin/stdin-input.ts +65 -0
  34. package/bin/test-report.ts +6 -9
  35. package/bin/types.ts +15 -0
  36. package/dist/bin/affected-actors.d.ts +46 -0
  37. package/dist/bin/affected-actors.d.ts.map +1 -0
  38. package/dist/bin/affected-actors.js +135 -0
  39. package/dist/bin/affected-actors.js.map +1 -0
  40. package/dist/bin/build-from-local.d.ts +12 -3
  41. package/dist/bin/build-from-local.d.ts.map +1 -1
  42. package/dist/bin/build-from-local.js +34 -12
  43. package/dist/bin/build-from-local.js.map +1 -1
  44. package/dist/bin/build.d.ts +30 -7
  45. package/dist/bin/build.d.ts.map +1 -1
  46. package/dist/bin/build.js +85 -26
  47. package/dist/bin/build.js.map +1 -1
  48. package/dist/bin/diff-changes.d.ts +11 -4
  49. package/dist/bin/diff-changes.d.ts.map +1 -1
  50. package/dist/bin/diff-changes.js +19 -19
  51. package/dist/bin/diff-changes.js.map +1 -1
  52. package/dist/bin/diff-json-schema.d.ts +2 -2
  53. package/dist/bin/diff-json-schema.d.ts.map +1 -1
  54. package/dist/bin/diff-json-schema.js +1 -4
  55. package/dist/bin/diff-json-schema.js.map +1 -1
  56. package/dist/bin/git.d.ts +23 -21
  57. package/dist/bin/git.d.ts.map +1 -1
  58. package/dist/bin/git.js +60 -83
  59. package/dist/bin/git.js.map +1 -1
  60. package/dist/bin/main.js +82 -105
  61. package/dist/bin/main.js.map +1 -1
  62. package/dist/bin/slack.d.ts +14 -4
  63. package/dist/bin/slack.d.ts.map +1 -1
  64. package/dist/bin/slack.js +39 -6
  65. package/dist/bin/slack.js.map +1 -1
  66. package/dist/bin/stdin-input.d.ts +17 -0
  67. package/dist/bin/stdin-input.d.ts.map +1 -0
  68. package/dist/bin/stdin-input.js +54 -0
  69. package/dist/bin/stdin-input.js.map +1 -0
  70. package/dist/bin/test-report.d.ts.map +1 -1
  71. package/dist/bin/test-report.js +6 -8
  72. package/dist/bin/test-report.js.map +1 -1
  73. package/dist/bin/types.d.ts +13 -0
  74. package/dist/bin/types.d.ts.map +1 -1
  75. package/dist/lib/extend-expect.d.ts.map +1 -1
  76. package/dist/lib/extend-expect.js +37 -218
  77. package/dist/lib/extend-expect.js.map +1 -1
  78. package/dist/lib/lib.d.ts +1 -2
  79. package/dist/lib/lib.d.ts.map +1 -1
  80. package/dist/lib/lib.js +15 -22
  81. package/dist/lib/lib.js.map +1 -1
  82. package/dist/lib/run-checks.d.ts +14 -0
  83. package/dist/lib/run-checks.d.ts.map +1 -0
  84. package/dist/lib/run-checks.js +82 -0
  85. package/dist/lib/run-checks.js.map +1 -0
  86. package/dist/lib/run-test-result.d.ts +3 -6
  87. package/dist/lib/run-test-result.d.ts.map +1 -1
  88. package/dist/lib/run-test-result.js +41 -54
  89. package/dist/lib/run-test-result.js.map +1 -1
  90. package/dist/test/bundle/cli-bundle.test.d.ts +2 -0
  91. package/dist/test/bundle/cli-bundle.test.d.ts.map +1 -0
  92. package/dist/test/bundle/cli-bundle.test.js +190 -0
  93. package/dist/test/bundle/cli-bundle.test.js.map +1 -0
  94. package/dist/test/e2e/cli-bundle.e2e.test.d.ts +2 -0
  95. package/dist/test/e2e/cli-bundle.e2e.test.d.ts.map +1 -0
  96. package/dist/test/e2e/cli-bundle.e2e.test.js +138 -0
  97. package/dist/test/e2e/cli-bundle.e2e.test.js.map +1 -0
  98. package/dist/test/e2e/git-build-release.e2e.test.d.ts +2 -0
  99. package/dist/test/e2e/git-build-release.e2e.test.d.ts.map +1 -0
  100. package/dist/test/e2e/git-build-release.e2e.test.js +87 -0
  101. package/dist/test/e2e/git-build-release.e2e.test.js.map +1 -0
  102. package/dist/test/e2e/helpers.d.ts +37 -0
  103. package/dist/test/e2e/helpers.d.ts.map +1 -0
  104. package/dist/test/e2e/helpers.js +57 -0
  105. package/dist/test/e2e/helpers.js.map +1 -0
  106. package/dist/test/unit/bin/affected-actors-cli.test.d.ts +2 -0
  107. package/dist/test/unit/bin/affected-actors-cli.test.d.ts.map +1 -0
  108. package/dist/test/unit/bin/affected-actors-cli.test.js +167 -0
  109. package/dist/test/unit/bin/affected-actors-cli.test.js.map +1 -0
  110. package/dist/test/unit/bin/affected-actors.test.d.ts +2 -0
  111. package/dist/test/unit/bin/affected-actors.test.d.ts.map +1 -0
  112. package/dist/test/unit/bin/affected-actors.test.js +141 -0
  113. package/dist/test/unit/bin/affected-actors.test.js.map +1 -0
  114. package/dist/test/unit/bin/build-cli.test.d.ts +2 -0
  115. package/dist/test/unit/bin/build-cli.test.d.ts.map +1 -0
  116. package/dist/test/unit/bin/build-cli.test.js +44 -0
  117. package/dist/test/unit/bin/build-cli.test.js.map +1 -0
  118. package/dist/test/unit/bin/build-from-local.test.js +65 -1
  119. package/dist/test/unit/bin/build-from-local.test.js.map +1 -1
  120. package/dist/test/unit/bin/build.test.js +223 -3
  121. package/dist/test/unit/bin/build.test.js.map +1 -1
  122. package/dist/test/unit/bin/compiled-cli.d.ts +6 -0
  123. package/dist/test/unit/bin/compiled-cli.d.ts.map +1 -0
  124. package/dist/test/unit/bin/compiled-cli.js +16 -0
  125. package/dist/test/unit/bin/compiled-cli.js.map +1 -0
  126. package/dist/test/unit/bin/diff-changes.test.js +130 -101
  127. package/dist/test/unit/bin/diff-changes.test.js.map +1 -1
  128. package/dist/test/unit/bin/diff.test.js +25 -28
  129. package/dist/test/unit/bin/diff.test.js.map +1 -1
  130. package/dist/test/unit/bin/git.test.js +69 -152
  131. package/dist/test/unit/bin/git.test.js.map +1 -1
  132. package/dist/test/unit/bin/slack.test.d.ts +2 -0
  133. package/dist/test/unit/bin/slack.test.d.ts.map +1 -0
  134. package/dist/test/unit/bin/slack.test.js +71 -0
  135. package/dist/test/unit/bin/slack.test.js.map +1 -0
  136. package/dist/test/unit/bin/stdin-input.test.d.ts +2 -0
  137. package/dist/test/unit/bin/stdin-input.test.d.ts.map +1 -0
  138. package/dist/test/unit/bin/stdin-input.test.js +50 -0
  139. package/dist/test/unit/bin/stdin-input.test.js.map +1 -0
  140. package/dist/test/unit/github-scripts/mask-config-secrets.test.d.ts +2 -0
  141. package/dist/test/unit/github-scripts/mask-config-secrets.test.d.ts.map +1 -0
  142. package/dist/test/unit/github-scripts/mask-config-secrets.test.js +50 -0
  143. package/dist/test/unit/github-scripts/mask-config-secrets.test.js.map +1 -0
  144. package/dist/test/unit/github-scripts/run-with-apify-tokens.test.d.ts +2 -0
  145. package/dist/test/unit/github-scripts/run-with-apify-tokens.test.d.ts.map +1 -0
  146. package/dist/test/unit/github-scripts/run-with-apify-tokens.test.js +102 -0
  147. package/dist/test/unit/github-scripts/run-with-apify-tokens.test.js.map +1 -0
  148. package/dist/test/unit/run-checks.test.d.ts +2 -0
  149. package/dist/test/unit/run-checks.test.d.ts.map +1 -0
  150. package/dist/test/unit/run-checks.test.js +62 -0
  151. package/dist/test/unit/run-checks.test.js.map +1 -0
  152. package/dist/test/unit/run-test-result.test.d.ts +2 -0
  153. package/dist/test/unit/run-test-result.test.d.ts.map +1 -0
  154. package/dist/test/unit/run-test-result.test.js +34 -0
  155. package/dist/test/unit/run-test-result.test.js.map +1 -0
  156. package/dist/test/unit/should-built-and-test.test.js +41 -38
  157. package/dist/test/unit/should-built-and-test.test.js.map +1 -1
  158. package/dist/tsconfig.tsbuildinfo +1 -1
  159. package/docs/cli.md +373 -0
  160. package/docs/lib.md +271 -0
  161. package/docs/workflows.md +370 -0
  162. package/lib/extend-expect.ts +48 -258
  163. package/lib/lib.ts +24 -28
  164. package/lib/run-checks.ts +101 -0
  165. package/lib/run-test-result.ts +45 -55
  166. package/package.json +13 -3
  167. package/test/bundle/cli-bundle.test.ts +235 -0
  168. package/test/e2e/cli-bundle.e2e.test.ts +207 -0
  169. package/test/e2e/git-build-release.e2e.test.ts +113 -0
  170. package/test/e2e/helpers.ts +82 -0
  171. package/test/unit/bin/affected-actors-cli.test.ts +201 -0
  172. package/test/unit/bin/affected-actors.test.ts +182 -0
  173. package/test/unit/bin/build-cli.test.ts +55 -0
  174. package/test/unit/bin/build-from-local.test.ts +71 -0
  175. package/test/unit/bin/build.test.ts +279 -3
  176. package/test/unit/bin/compiled-cli.ts +17 -0
  177. package/test/unit/bin/diff-changes.test.ts +139 -101
  178. package/test/unit/bin/diff.test.ts +25 -28
  179. package/test/unit/bin/git.test.ts +85 -176
  180. package/test/unit/bin/slack.test.ts +84 -0
  181. package/test/unit/bin/stdin-input.test.ts +61 -0
  182. package/test/unit/github-scripts/mask-config-secrets.test.ts +61 -0
  183. package/test/unit/github-scripts/run-with-apify-tokens.test.ts +115 -0
  184. package/test/unit/run-checks.test.ts +76 -0
  185. package/test/unit/run-test-result.test.ts +44 -0
  186. package/test/unit/should-built-and-test.test.ts +46 -39
  187. package/.github/scripts/check-major-tag-refs.mjs +0 -72
  188. package/.github/scripts/check-package-version-bump.mjs +0 -74
  189. 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 { deleteOldBuilds, runBuilds } from './build.js';
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 { getChangedActors } from './diff-changes.js';
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
- /** Flags for commands that trigger Apify builds. */
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
- 'use-docker-cache': { type: 'boolean', default: false },
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,51 +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
- testFilesGlob: config.testFilesGlob,
103
- });
104
- if (branchOnlyActorsChanged.length === 0) {
105
- logger.info('[MERGE-FROM-TARGET-OPTIMIZATION]: Branch itself has no functional changes, skipping tests');
106
- return [];
107
- }
108
- logger.info(
109
- `[MERGE-FROM-TARGET-OPTIMIZATION]: Branch has ${branchOnlyActorsChanged.length} functional changes, cannot optimize, we continue with full check`,
110
- );
111
- }
112
-
113
- // 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)
114
- const commits = await getCommits(config);
115
- const changedFiles = await getChangedFiles(commits);
116
- return getChangedActors({
117
- filepathsChanged: changedFiles,
118
- actorConfigs,
119
- isLatest,
120
- commits,
121
- testFilesGlob: config.testFilesGlob,
122
- });
123
- };
124
-
125
129
  await yargs()
126
130
  .scriptName('public-actors-utils')
127
131
  .options(globalOptions)
@@ -167,11 +171,10 @@ await yargs()
167
171
  )
168
172
  .command(
169
173
  'get-affected-actors',
170
- '',
171
- (y) => y.options(gitRangeOptions).options(actorSelectionOptions),
172
- async (config) => {
173
- const actorsChanged = await resolveChangedActors(config, { isLatest: false });
174
- 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));
175
178
  },
176
179
  )
177
180
  .command(
@@ -179,6 +182,7 @@ await yargs()
179
182
  '',
180
183
  (y) =>
181
184
  y
185
+ .options(dryRunOptions)
182
186
  .option('report-file', { type: 'string', demandOption: true })
183
187
  .option('report-slack-channel', { type: 'string' })
184
188
  .option('job-url', { type: 'string' })
@@ -189,100 +193,57 @@ await yargs()
189
193
  )
190
194
  .command(
191
195
  'build',
192
- '',
193
- (y) =>
194
- y
195
- .options(gitRangeOptions)
196
- .options(actorSelectionOptions)
197
- .options(buildOptions)
198
- .options(repoUrlOptions)
199
- .option('test-files-glob', { type: 'string' }),
200
- async (config) => {
201
- const actorsChanged = await resolveChangedActors(config, { isLatest: false });
202
- const builds = await runBuilds({
203
- ...(await resolveRepoUrl(config.repoUrl)),
204
- actorConfigs: actorsChanged,
205
- branch: config.sourceBranch.replace('origin/', ''),
206
- dryRun: config.dryRun,
207
- useDockerCache: config.useDockerCache,
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,
208
209
  });
209
210
  writeJson(builds);
210
211
  },
211
212
  )
212
213
  .command(
213
- 'release',
214
- '',
215
- (y) =>
216
- y
217
- .options(actorSelectionOptions)
218
- .options(buildOptions)
219
- .options(repoUrlOptions)
220
- // The last commit that is already released, everything after it up to HEAD gets released.
221
- // Required, see the README section "Releasing Actors" for how to set it for each merge strategy.
222
- .option('base-commit', { type: 'string', demandOption: true })
223
- .option('report-slack-channel', { type: 'string' })
224
- .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),
225
217
  async (args) => {
226
- const baseSha = await resolveReleaseBaseCommit(args.baseCommit);
227
- const branch = await getCurrentBranch();
228
- const changes = await getReleaseChanges(baseSha);
229
- if (!changes) {
230
- logger.info(`HEAD is the base commit ${baseSha}, there is nothing new to release`);
231
- return;
232
- }
233
- const { commits, changedFiles, changelog } = changes;
234
- const { repoUrl, shouldVerifyRepoUrl } = await resolveRepoUrl(args.repoUrl);
235
-
236
- const isLatest = true;
237
- const actorConfigs = await readConfigFile(args);
238
- const actorsChanged = await getChangedActors({
239
- filepathsChanged: changedFiles,
240
- actorConfigs,
241
- isLatest,
242
- commits,
243
- });
244
- const { dryRun, reportSlackChannel, releaseSlackChannel } = args;
245
- const builds = await runBuilds({
246
- isLatest,
247
- repoUrl,
248
- shouldVerifyRepoUrl,
249
- actorConfigs: actorsChanged,
250
- branch,
251
- dryRun,
252
- useDockerCache: args.useDockerCache,
253
- });
254
- logger.info(JSON.stringify(builds));
255
-
218
+ const { commits, changedFiles, changelog } = parseReleaseChanges(await readStdin());
256
219
  await notifyToSlack({
257
- changedFiles,
258
220
  commits,
221
+ changedFiles,
259
222
  changelog,
260
- repository: getRepoName(repoUrl),
261
- dryRun,
262
- // The newest commit, same as the pushed head commit
263
- author: commits[commits.length - 1].author,
264
- reportSlackChannel,
265
- releaseSlackChannel,
223
+ repoUrl: args.repoUrl,
224
+ dryRun: args.dryRun,
225
+ reportSlackChannel: args.reportSlackChannel,
226
+ releaseSlackChannel: args.releaseSlackChannel,
266
227
  });
267
228
  },
268
229
  )
269
230
  .command(
270
231
  'build-from-local',
271
232
  '',
272
- (y) => y.options(actorSelectionOptions),
273
- async ({ actors, ignore, dryRun }) => {
233
+ (y) => y.options(actorSelectionOptions).options(envVarsSyncOptions).options(dryRunOptions),
234
+ async ({ actors, ignore, dryRun, requireEnvVarsInSync }) => {
274
235
  const actorConfigs = await readConfigFile({ actors, ignore });
275
- const builds = await runBuildsFromLocal({ actorConfigs, dryRun });
236
+ const builds = await runBuildsFromLocal({ actorConfigs, dryRun, requireEnvVarsInSync });
276
237
  writeJson(builds);
277
238
  },
278
239
  )
279
240
  .command(
280
241
  'delete-old-builds',
281
242
  '',
282
- (y) => y.options(actorSelectionOptions),
283
- async ({ actors, ignore }) => {
243
+ (y) => y.options(actorSelectionOptions).options(dryRunOptions),
244
+ async ({ actors, ignore, dryRun }) => {
284
245
  const actorConfigs = await readConfigFile({ actors, ignore });
285
- await deleteOldBuilds(actorConfigs);
246
+ await deleteOldBuilds(actorConfigs, { dryRun });
286
247
  },
287
248
  )
288
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
- repository,
49
+ repoUrl,
26
50
  dryRun,
27
- author,
28
51
  releaseSlackChannel,
29
52
  reportSlackChannel,
30
53
  }: NotifyToSlackOptions) => {
31
- const slack = new WebClient(getEnvVar('SLACK_TOKEN_RELEASES_BOT'));
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(`**Sending slack message to channel**: ${releaseSlackChannel}.\n\n${shortMessage}`);
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(`Sending slack message to channel: ${reportSlackChannel}.\n\n${longMessage}`);
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[], token: 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
+ });
@@ -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('SLACK:', slackMessage);
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
- const slackToken = getEnvVar('SLACK_TOKEN_TESTS_BOT');
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
@@ -10,6 +10,12 @@ export interface Config {
10
10
  testFilesGlob?: string;
11
11
  }
12
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;
17
+ }
18
+
13
19
  export type Commit = {
14
20
  sha: string;
15
21
  author: string;
@@ -24,6 +30,15 @@ export interface BuildData {
24
30
  buildNumber: string;
25
31
  }
26
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
+
27
42
  export interface ActorConfig {
28
43
  actorFullName: string;
29
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"}