apify-test-tools 0.10.2-beta.3 → 0.10.2-beta.5

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 (107) hide show
  1. package/.github/cli/apify-test-tools.mjs +117 -118
  2. package/.github/scripts/run-with-apify-tokens.mjs +1 -1
  3. package/.github/workflows/public_pr-build-test.yaml +25 -25
  4. package/.github/workflows/public_push-build-latest.yaml +20 -4
  5. package/CHANGELOG.md +6 -0
  6. package/CONTRIBUTING.md +4 -1
  7. package/README.md +104 -56
  8. package/bin/affected-actors.ts +135 -103
  9. package/bin/build.ts +53 -38
  10. package/bin/diff-changes.ts +7 -6
  11. package/bin/diff-json-schema.ts +5 -6
  12. package/bin/git.ts +24 -69
  13. package/bin/main.ts +46 -80
  14. package/bin/slack.ts +15 -4
  15. package/bin/stdin-input.ts +65 -0
  16. package/bin/types.ts +6 -0
  17. package/dist/bin/affected-actors.d.ts +23 -27
  18. package/dist/bin/affected-actors.d.ts.map +1 -1
  19. package/dist/bin/affected-actors.js +111 -65
  20. package/dist/bin/affected-actors.js.map +1 -1
  21. package/dist/bin/build.d.ts +11 -5
  22. package/dist/bin/build.d.ts.map +1 -1
  23. package/dist/bin/build.js +43 -23
  24. package/dist/bin/build.js.map +1 -1
  25. package/dist/bin/diff-changes.d.ts +4 -3
  26. package/dist/bin/diff-changes.d.ts.map +1 -1
  27. package/dist/bin/diff-changes.js +4 -4
  28. package/dist/bin/diff-changes.js.map +1 -1
  29. package/dist/bin/diff-json-schema.d.ts +2 -2
  30. package/dist/bin/diff-json-schema.d.ts.map +1 -1
  31. package/dist/bin/diff-json-schema.js +1 -4
  32. package/dist/bin/diff-json-schema.js.map +1 -1
  33. package/dist/bin/git.d.ts +7 -15
  34. package/dist/bin/git.d.ts.map +1 -1
  35. package/dist/bin/git.js +23 -61
  36. package/dist/bin/git.js.map +1 -1
  37. package/dist/bin/main.js +39 -71
  38. package/dist/bin/main.js.map +1 -1
  39. package/dist/bin/slack.d.ts +6 -3
  40. package/dist/bin/slack.d.ts.map +1 -1
  41. package/dist/bin/slack.js +13 -1
  42. package/dist/bin/slack.js.map +1 -1
  43. package/dist/bin/stdin-input.d.ts +17 -0
  44. package/dist/bin/stdin-input.d.ts.map +1 -0
  45. package/dist/bin/stdin-input.js +54 -0
  46. package/dist/bin/stdin-input.js.map +1 -0
  47. package/dist/bin/types.d.ts +5 -0
  48. package/dist/bin/types.d.ts.map +1 -1
  49. package/dist/test/bundle/cli-bundle.test.js +32 -11
  50. package/dist/test/bundle/cli-bundle.test.js.map +1 -1
  51. package/dist/test/e2e/cli-bundle.e2e.test.js +23 -57
  52. package/dist/test/e2e/cli-bundle.e2e.test.js.map +1 -1
  53. package/dist/test/e2e/git-build-release.e2e.test.d.ts +2 -0
  54. package/dist/test/e2e/git-build-release.e2e.test.d.ts.map +1 -0
  55. package/dist/test/e2e/git-build-release.e2e.test.js +87 -0
  56. package/dist/test/e2e/git-build-release.e2e.test.js.map +1 -0
  57. package/dist/test/e2e/helpers.d.ts +37 -0
  58. package/dist/test/e2e/helpers.d.ts.map +1 -0
  59. package/dist/test/e2e/helpers.js +57 -0
  60. package/dist/test/e2e/helpers.js.map +1 -0
  61. package/dist/test/unit/bin/affected-actors-cli.test.d.ts +2 -0
  62. package/dist/test/unit/bin/affected-actors-cli.test.d.ts.map +1 -0
  63. package/dist/test/unit/bin/affected-actors-cli.test.js +161 -0
  64. package/dist/test/unit/bin/affected-actors-cli.test.js.map +1 -0
  65. package/dist/test/unit/bin/affected-actors.test.js +68 -87
  66. package/dist/test/unit/bin/affected-actors.test.js.map +1 -1
  67. package/dist/test/unit/bin/build-cli.test.d.ts +2 -0
  68. package/dist/test/unit/bin/build-cli.test.d.ts.map +1 -0
  69. package/dist/test/unit/bin/build-cli.test.js +44 -0
  70. package/dist/test/unit/bin/build-cli.test.js.map +1 -0
  71. package/dist/test/unit/bin/build.test.js +61 -4
  72. package/dist/test/unit/bin/build.test.js.map +1 -1
  73. package/dist/test/unit/bin/compiled-cli.d.ts +6 -0
  74. package/dist/test/unit/bin/compiled-cli.d.ts.map +1 -0
  75. package/dist/test/unit/bin/compiled-cli.js +16 -0
  76. package/dist/test/unit/bin/compiled-cli.js.map +1 -0
  77. package/dist/test/unit/bin/diff-changes.test.js +55 -55
  78. package/dist/test/unit/bin/diff-changes.test.js.map +1 -1
  79. package/dist/test/unit/bin/diff.test.js +25 -28
  80. package/dist/test/unit/bin/diff.test.js.map +1 -1
  81. package/dist/test/unit/bin/git.test.js +28 -108
  82. package/dist/test/unit/bin/git.test.js.map +1 -1
  83. package/dist/test/unit/bin/slack.test.js +19 -2
  84. package/dist/test/unit/bin/slack.test.js.map +1 -1
  85. package/dist/test/unit/bin/stdin-input.test.d.ts +2 -0
  86. package/dist/test/unit/bin/stdin-input.test.d.ts.map +1 -0
  87. package/dist/test/unit/bin/stdin-input.test.js +50 -0
  88. package/dist/test/unit/bin/stdin-input.test.js.map +1 -0
  89. package/dist/test/unit/should-built-and-test.test.js +18 -21
  90. package/dist/test/unit/should-built-and-test.test.js.map +1 -1
  91. package/dist/tsconfig.tsbuildinfo +1 -1
  92. package/package.json +1 -1
  93. package/test/bundle/cli-bundle.test.ts +47 -11
  94. package/test/e2e/cli-bundle.e2e.test.ts +47 -77
  95. package/test/e2e/git-build-release.e2e.test.ts +113 -0
  96. package/test/e2e/helpers.ts +82 -0
  97. package/test/unit/bin/affected-actors-cli.test.ts +193 -0
  98. package/test/unit/bin/affected-actors.test.ts +86 -94
  99. package/test/unit/bin/build-cli.test.ts +55 -0
  100. package/test/unit/bin/build.test.ts +71 -3
  101. package/test/unit/bin/compiled-cli.ts +17 -0
  102. package/test/unit/bin/diff-changes.test.ts +55 -55
  103. package/test/unit/bin/diff.test.ts +25 -28
  104. package/test/unit/bin/git.test.ts +35 -132
  105. package/test/unit/bin/slack.test.ts +21 -2
  106. package/test/unit/bin/stdin-input.test.ts +61 -0
  107. package/test/unit/should-built-and-test.test.ts +19 -22
package/bin/git.ts CHANGED
@@ -9,8 +9,8 @@ const GIT_LOG_FORMAT = ['%H', '%aN<%aE>', '%aD', '%s'].join(GIT_FORMAT_SEPARATOR
9
9
  * Gets the list of changed files between the given commits (inclusive), so it diffs from the parent of the oldest.
10
10
  */
11
11
  export const getChangedFiles = async (commits: Commit[]) => {
12
- // getCommits never returns an empty list (the rerun check returns all commits when the base commit
13
- // is the branch HEAD, and an empty git range throws when parsing), so this signals a programmer error
12
+ // getCommits never returns an empty list (it throws on an empty range, and the rerun check returns all
13
+ // commits when the base commit is the branch HEAD), so this signals a programmer error
14
14
  if (commits.length === 0) {
15
15
  throw new Error('Cannot get changed files: the commit list is empty. This should never happen.');
16
16
  }
@@ -35,59 +35,22 @@ export const getChangedFilesBetween = async (fromRef: string, toRef: string) =>
35
35
  };
36
36
 
37
37
  /**
38
- * Returns true if the branch contains a merge commit whose parent is reachable from targetBranch
39
- * (i.e. a genuine "merge from target" commit, not a merge of some unrelated branch).
40
- * Uses the full targetBranch..sourceBranch range, ignoring baseCommit.
38
+ * The commit `git diff base...head` compares against: where `head` forked from `base`, or the last commit of `base`
39
+ * that `head` merged in. The diff from it holds only what `head` itself changed, like GitHub's "Files changed" tab.
40
+ * With criss-cross merges there are several such commits and git picks one.
41
41
  */
42
- export const hasMergeFromTarget = async (sourceBranch: string, targetBranch: string): Promise<boolean> => {
43
- const mergeShas = (
44
- await runGitCommand(['log', '--merges', '--pretty=format:%H', `${targetBranch}..${sourceBranch}`], {
45
- operation: 'find merge commits on the source branch that are absent from the target branch',
46
- })
47
- )
48
- .split('\n')
49
- .filter(Boolean);
42
+ export const getMergeBase = async (base: string, head: string): Promise<string> =>
43
+ runGitCommand(['merge-base', base, head], { operation: `find where ${head} forked from ${base}` });
50
44
 
51
- for (const sha of mergeShas) {
52
- const parents = (
53
- await runGitCommand(['log', '-1', '--pretty=format:%P', sha], {
54
- operation: 'get parents of a merge commit',
55
- })
56
- ).split(' ');
57
- for (const parent of parents) {
58
- // git merge-base A B outputs the common ancestor.
59
- // If that equals A, then A is an ancestor of B (i.e. parent is reachable from targetBranch).
60
- let mergeBase: string;
61
- try {
62
- mergeBase = await runGitCommand(['merge-base', parent, targetBranch], {
63
- operation: 'check whether a merge parent is reachable from the target branch',
64
- });
65
- } catch (error) {
66
- // Exit code 1 on merge-base means the histories have no common ancestor.
67
- if (error instanceof GitCommandError && error.exitCode === 1) {
68
- continue;
69
- }
70
- throw error;
71
- }
72
- if (mergeBase === parent) {
73
- return true;
74
- }
75
- }
76
- }
77
- return false;
78
- };
79
-
80
- /**
81
- * Returns all files touched by non-merge commits on the branch (full history, ignoring baseCommit).
82
- * Used to check whether the branch itself has any functional changes, independent of what master merged in.
83
- */
84
- export const getBranchOnlyChangedFiles = async (sourceBranch: string, targetBranch: string): Promise<string[]> => {
85
- const output = await runGitCommand(
86
- ['log', '--no-merges', '--name-only', '--pretty=format:', `${targetBranch}..${sourceBranch}`],
87
- { operation: 'list files changed by non-merge commits on the source branch' },
88
- );
89
- return output.split('\n').filter(Boolean);
90
- };
45
+ /** Resolves a ref to a commit, or undefined when it isn't in the local history (e.g. it was force-pushed away). */
46
+ export const findCommitSha = async (ref: string): Promise<string | undefined> =>
47
+ runGitCommand(['rev-parse', '--verify', '--quiet', `${ref}^{commit}`], {
48
+ operation: `look up ${ref} in local Git history`,
49
+ }).catch((error: unknown) => {
50
+ // --quiet suppresses Git's error text for a missing commit, but Git still exits with code 1
51
+ if (error instanceof GitCommandError && error.exitCode === 1) return undefined;
52
+ throw error;
53
+ });
91
54
 
92
55
  const SHA_REGEX = /^[0-9a-f]{40}$/i;
93
56
 
@@ -120,8 +83,8 @@ export const getCommitsBetween = async (fromRef: string, toRef: string): Promise
120
83
  const output = await runGitCommand(['log', `--pretty=format:${GIT_LOG_FORMAT}`, `${fromRef}..${toRef}`], {
121
84
  operation: 'list commits reachable from the source ref but absent from the target ref',
122
85
  });
123
- const commitsStrings = output.split('\n');
124
- const commits = commitsStrings.map((commitString) => parseCommit(commitString));
86
+ // An empty range (e.g. `x..x`) prints nothing
87
+ const commits = output.split('\n').filter(Boolean).map(parseCommit);
125
88
  commits.reverse();
126
89
  return commits;
127
90
  };
@@ -137,6 +100,9 @@ export const getCommits = async ({
137
100
  }: Pick<Config, 'sourceBranch' | 'targetBranch' | 'baseCommit'>): Promise<Commit[]> => {
138
101
  const baseCommitSha = parseBaseCommit(baseCommit);
139
102
  const commits = await getCommitsBetween(targetBranch, sourceBranch);
103
+ if (commits.length === 0) {
104
+ throw new Error(`${sourceBranch} has no commits that are not on ${targetBranch}.`);
105
+ }
140
106
 
141
107
  // The last validated (base) commit being the branch HEAD means nothing new was pushed since the last
142
108
  // validation — the dev reran the workflow (or force-pushed to the same state) to trigger a clean test
@@ -181,16 +147,16 @@ export const parseCommit = (commitString: string): Commit => {
181
147
  };
182
148
 
183
149
  /**
184
- * Returns the currently checked-out branch. Release builds point the Actor version at this branch,
150
+ * Returns the currently checked-out branch. Builds point the Actor version at this branch,
185
151
  * so a detached HEAD (no branch to point at) is an error rather than a guess.
186
152
  */
187
153
  export const getCurrentBranch = async (): Promise<string> => {
188
154
  const branch = await runGitCommand(['rev-parse', '--abbrev-ref', 'HEAD'], {
189
- operation: 'determine the current branch for release',
155
+ operation: 'determine the checked-out branch',
190
156
  });
191
157
  if (branch === 'HEAD') {
192
158
  throw new Error(
193
- 'Cannot determine the branch to release: HEAD is detached. Check out the branch you want to release.',
159
+ 'Cannot determine the branch to build from: HEAD is detached. Check out the branch you want to build.',
194
160
  );
195
161
  }
196
162
  return branch;
@@ -200,17 +166,6 @@ export const getCurrentBranch = async (): Promise<string> => {
200
166
  export const resolveCommitSha = async (ref: string): Promise<string> =>
201
167
  runGitCommand(['rev-parse', '--verify', `${ref}^{commit}`], { operation: `resolve ${ref} to a commit` });
202
168
 
203
- /**
204
- * Branch name a ref points to, without the remote: `origin/foo` and `foo` both give `foo`, and `HEAD`
205
- * gives the checked-out branch. Null when there is no branch to point at (a SHA, a tag, a detached HEAD).
206
- */
207
- export const resolveBranchName = async (ref: string): Promise<string | null> => {
208
- const fullName = await runGitCommand(['rev-parse', '--symbolic-full-name', ref], {
209
- operation: `resolve the branch name of ${ref}`,
210
- });
211
- return fullName.match(/^refs\/(?:heads|remotes\/[^/]+)\/(.+)$/)?.[1] ?? null;
212
- };
213
-
214
169
  /**
215
170
  * Reads the repository URL from the `origin` remote, rewritten to the SSH form the Apify platform
216
171
  * uses for Git repo sources, e.g. git@github.com:apify-store/google-maps
package/bin/main.ts CHANGED
@@ -6,13 +6,14 @@ 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 { getAffectedActors, toAffectedActorsDocument } from './affected-actors.js';
10
- import { deleteOldBuilds, runBuilds } from './build.js';
9
+ import { getAffectedActors } from './affected-actors.js';
10
+ import { buildActors, DEFAULT_VERSION_KEYWORD, deleteOldBuilds } from './build.js';
11
11
  import { runBuildsFromLocal } from './build-from-local.js';
12
- import { getChangedFiles, getCommits, getCurrentBranch, getRepoName, resolveRepoUrl } from './git.js';
12
+ import { getChangedFiles, getCommits } from './git.js';
13
13
  import { logger, logLevels } from './logger.js';
14
14
  import { writeJson } from './output.js';
15
15
  import { notifyToSlack } from './slack.js';
16
+ import { parseBuildInput, parseReleaseChanges, readStdin } from './stdin-input.js';
16
17
  import { reportTestResults } from './test-report.js';
17
18
  import { readConfigFile } from './utils/config/load-config.js';
18
19
 
@@ -83,9 +84,27 @@ const dryRunOptions = {
83
84
  },
84
85
  } as const satisfies Record<string, Options>;
85
86
 
86
- /** Flags for commands that trigger Apify builds. */
87
+ /** Flags of `build`, which builds exactly the Actors it is given. */
87
88
  const buildOptions = {
88
- '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
+ /** Where `notify-release` posts the release. */
105
+ const releaseSlackOptions = {
106
+ 'report-slack-channel': { type: 'string', describe: 'Channel for the full report: commits and changed files' },
107
+ 'release-slack-channel': { type: 'string', describe: 'Channel for the changelog additions only' },
89
108
  } as const satisfies Record<string, Options>;
90
109
 
91
110
  /** Global flags, available to every command. */
@@ -146,7 +165,7 @@ await yargs()
146
165
  'Find the Actors whose context changed in a commit range ending at the checked-out HEAD',
147
166
  (y) => y.options(affectedRangeOptions).options(actorSelectionOptions),
148
167
  async (args) => {
149
- writeJson(toAffectedActorsDocument(await getAffectedActors({ ...args, head: 'HEAD' })));
168
+ writeJson(await getAffectedActors(args));
150
169
  },
151
170
  )
152
171
  .command(
@@ -165,89 +184,36 @@ await yargs()
165
184
  )
166
185
  .command(
167
186
  'build',
168
- '',
169
- (y) =>
170
- y
171
- .options(gitRangeOptions)
172
- .options(actorSelectionOptions)
173
- .options(buildOptions)
174
- .options(repoUrlOptions)
175
- .options(dryRunOptions)
176
- .option('test-files-glob', { type: 'string' }),
177
- async (config) => {
178
- // Cosmetic-only changes don't need testing, so they don't need a build either
179
- const { functionallyChangedActors: actorsChanged } = await getAffectedActors({
180
- head: config.sourceBranch,
181
- base: config.targetBranch,
182
- since: config.baseCommit,
183
- testFilesGlob: config.testFilesGlob,
184
- actors: config.actors,
185
- ignore: config.ignore,
186
- });
187
- const builds = await runBuilds({
188
- ...(await resolveRepoUrl(config.repoUrl)),
189
- actorConfigs: actorsChanged,
190
- branch: config.sourceBranch.replace('origin/', ''),
191
- dryRun: config.dryRun,
192
- useDockerCache: config.useDockerCache,
187
+ 'Build the Actors listed on stdin, as {"actors": [{"actorFullName": ...}]}, from the checked-out branch',
188
+ (y) => y.options(buildOptions).options(repoUrlOptions).options(dryRunOptions),
189
+ async (args) => {
190
+ const input = parseBuildInput(await readStdin());
191
+ const names = input.actors.map(({ actorFullName }) => actorFullName);
192
+ // An empty --actors selects every Actor, but an empty list here means there is nothing to build
193
+ const actorConfigs = names.length > 0 ? await readConfigFile({ actors: names, ignore: [] }) : [];
194
+ const builds = await buildActors({
195
+ actorConfigs,
196
+ versionNumber: args.versionNumber,
197
+ repoUrl: args.repoUrl,
198
+ dryRun: args.dryRun,
193
199
  });
194
200
  writeJson(builds);
195
201
  },
196
202
  )
197
203
  .command(
198
- 'release',
199
- '',
200
- (y) =>
201
- y
202
- .options(actorSelectionOptions)
203
- .options(buildOptions)
204
- .options(repoUrlOptions)
205
- .options(dryRunOptions)
206
- // The last commit that is already released, everything after it up to HEAD gets released.
207
- // Required, see the README section "Releasing Actors" for how to set it for each merge strategy.
208
- .option('base-commit', { type: 'string', demandOption: true })
209
- .option('report-slack-channel', { type: 'string' })
210
- .option('release-slack-channel', { type: 'string' }),
204
+ 'notify-release',
205
+ 'Announce a release on Slack, from the get-affected-actors output on stdin',
206
+ (y) => y.options(releaseSlackOptions).options(repoUrlOptions).options(dryRunOptions),
211
207
  async (args) => {
212
- const branch = await getCurrentBranch();
213
- const { commits, changedFiles, changelog, functionallyChangedActors, cosmeticallyChangedActors } =
214
- await getAffectedActors({
215
- head: 'HEAD',
216
- since: args.baseCommit,
217
- actors: args.actors,
218
- ignore: args.ignore,
219
- });
220
- // A release ships cosmetic changes (README, input schema descriptions, ...) too
221
- const actorsChanged = [...functionallyChangedActors, ...cosmeticallyChangedActors];
222
- if (commits.length === 0) {
223
- logger.info(`HEAD is the base commit ${args.baseCommit}, there is nothing new to release`);
224
- return;
225
- }
226
- const { repoUrl, shouldVerifyRepoUrl } = await resolveRepoUrl(args.repoUrl);
227
-
228
- const isLatest = true;
229
- const { dryRun, reportSlackChannel, releaseSlackChannel } = args;
230
- const builds = await runBuilds({
231
- isLatest,
232
- repoUrl,
233
- shouldVerifyRepoUrl,
234
- actorConfigs: actorsChanged,
235
- branch,
236
- dryRun,
237
- useDockerCache: args.useDockerCache,
238
- });
239
- logger.info(JSON.stringify(builds));
240
-
208
+ const { commits, changedFiles, changelog } = parseReleaseChanges(await readStdin());
241
209
  await notifyToSlack({
242
- changedFiles,
243
210
  commits,
211
+ changedFiles,
244
212
  changelog,
245
- repository: getRepoName(repoUrl),
246
- dryRun,
247
- // The newest commit, same as the pushed head commit
248
- author: commits[commits.length - 1].author,
249
- reportSlackChannel,
250
- releaseSlackChannel,
213
+ repoUrl: args.repoUrl,
214
+ dryRun: args.dryRun,
215
+ reportSlackChannel: args.reportSlackChannel,
216
+ releaseSlackChannel: args.releaseSlackChannel,
251
217
  });
252
218
  },
253
219
  )
package/bin/slack.ts CHANGED
@@ -1,16 +1,17 @@
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
  };
@@ -37,16 +38,23 @@ export const sendingLabel = (dryRun: boolean) => (dryRun ? '[DRY RUN] Would send
37
38
  // Git authors come as "Name<email>", Slack only needs the name
38
39
  const displayName = (author: string) => author.replace(/\s*<[^>]*>$/, '');
39
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
+ */
40
45
  export const notifyToSlack = async ({
41
46
  changedFiles,
42
47
  commits,
43
48
  changelog,
44
- repository,
49
+ repoUrl,
45
50
  dryRun,
46
- author,
47
51
  releaseSlackChannel,
48
52
  reportSlackChannel,
49
53
  }: NotifyToSlackOptions) => {
54
+ if (commits.length === 0) {
55
+ logger.info('No new commits, so there is no release to announce');
56
+ return;
57
+ }
50
58
  if (!changelog) {
51
59
  logger.warn('No new changelog entries found, did you forget to update it?');
52
60
  }
@@ -57,6 +65,9 @@ export const notifyToSlack = async ({
57
65
  }
58
66
  const slack = await getSlackClient('SLACK_TOKEN_RELEASES_BOT', dryRun);
59
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];
60
71
  let shortMessage = `*${repository}* – New release (by ${displayName(author)}):\n\n`;
61
72
 
62
73
  // This one is just for broader public that only cares about public facing changes
@@ -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/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;
@@ -1,12 +1,15 @@
1
- import type { ActorConfig, Commit } from './types.js';
1
+ import type { Commit } from './types.js';
2
2
  interface AffectedActorsOptions {
3
- /** End of the range. */
4
- head: string;
5
- /** Only the commits on head that are not on base count, i.e. the branch's own changes. */
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
+ */
6
8
  base?: string;
7
9
  /**
8
- * Only changes after this commit count. With `base` it's the last validated commit of the branch and is
9
- * ignored when it isn't on the branch. Without `base` it must be an ancestor of head (e.g. the last release).
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).
10
13
  */
11
14
  since?: string;
12
15
  /** Glob relative to the repo root. Matching files count as changes even when .dockerignore lists them. */
@@ -14,37 +17,30 @@ interface AffectedActorsOptions {
14
17
  actors: string[];
15
18
  ignore: string[];
16
19
  }
17
- interface AffectedActors {
18
- headSha: string;
19
- /** Branch head points to, which is what git builds are made from. Null for a SHA or a detached HEAD. */
20
- ref: string | null;
21
- commits: Commit[];
22
- changedFiles: string[];
23
- changelog: string | null;
24
- /** Actors with at least one functional change. They need a new build and tests. */
25
- functionallyChangedActors: ActorConfig[];
26
- /** Actors whose changes are all cosmetic (README, CHANGELOG, cosmetic .actor JSON). They only need a release build. */
27
- cosmeticallyChangedActors: ActorConfig[];
28
- }
29
- /**
30
- * Finds the Actors whose context changed in the given range. `base` selects the branch's own commits
31
- * (a PR), `since` the commits after a known one (a release, or the last validated commit of a PR).
32
- */
33
- export declare const getAffectedActors: ({ head, base, since: sinceOption, testFilesGlob, actors, ignore, }: AffectedActorsOptions) => Promise<AffectedActors>;
34
20
  /** The JSON document `get-affected-actors` prints. Versioned, since other commands and workflows consume it. */
35
- export declare const toAffectedActorsDocument: ({ headSha, ref, commits, changedFiles, changelog, functionallyChangedActors, cosmeticallyChangedActors, }: AffectedActors) => {
36
- version: number;
37
- ref: string | null;
21
+ interface AffectedActors {
22
+ version: 1;
38
23
  head: string;
24
+ /** The commits, changed files and changelog describe the whole range, `--since` with `--base` only narrows the Actors. */
39
25
  commits: Commit[];
40
26
  changedFiles: string[];
41
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
+ */
42
32
  functionallyChangedActors: {
43
33
  actorFullName: string;
44
34
  }[];
35
+ /** Actors whose changes are all cosmetic (README, CHANGELOG, cosmetic .actor JSON). They only need a release build. */
45
36
  cosmeticallyChangedActors: {
46
37
  actorFullName: string;
47
38
  }[];
48
- };
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>;
49
45
  export {};
50
46
  //# sourceMappingURL=affected-actors.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"affected-actors.d.ts","sourceRoot":"","sources":["../../bin/affected-actors.ts"],"names":[],"mappings":"AAaA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,EAAE,MAAM,YAAY,CAAC;AAGtD,UAAU,qBAAqB;IAC3B,wBAAwB;IACxB,IAAI,EAAE,MAAM,CAAC;IACb,0FAA0F;IAC1F,IAAI,CAAC,EAAE,MAAM,CAAC;IACd;;;OAGG;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,UAAU,cAAc;IACpB,OAAO,EAAE,MAAM,CAAC;IAChB,wGAAwG;IACxG,GAAG,EAAE,MAAM,GAAG,IAAI,CAAC;IACnB,OAAO,EAAE,MAAM,EAAE,CAAC;IAClB,YAAY,EAAE,MAAM,EAAE,CAAC;IACvB,SAAS,EAAE,MAAM,GAAG,IAAI,CAAC;IACzB,mFAAmF;IACnF,yBAAyB,EAAE,WAAW,EAAE,CAAC;IACzC,uHAAuH;IACvH,yBAAyB,EAAE,WAAW,EAAE,CAAC;CAC5C;AA4DD;;;GAGG;AACH,eAAO,MAAM,iBAAiB,GAAU,oEAOrC,qBAAqB,KAAG,OAAO,CAAC,cAAc,CA4BhD,CAAC;AAEF,gHAAgH;AAChH,eAAO,MAAM,wBAAwB,GAAI,2GAQtC,cAAc;;;;;;;;;;;;;CAYhB,CAAC"}
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"}