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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (64) hide show
  1. package/.github/cli/apify-test-tools.mjs +126 -125
  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 +1 -0
  6. package/README.md +67 -17
  7. package/bin/affected-actors.ts +16 -25
  8. package/bin/build.ts +55 -38
  9. package/bin/git.ts +3 -14
  10. package/bin/main.ts +96 -40
  11. package/bin/slack.ts +24 -0
  12. package/bin/stdin-input.ts +65 -0
  13. package/dist/bin/affected-actors.d.ts +4 -9
  14. package/dist/bin/affected-actors.d.ts.map +1 -1
  15. package/dist/bin/affected-actors.js +17 -19
  16. package/dist/bin/affected-actors.js.map +1 -1
  17. package/dist/bin/build.d.ts +12 -5
  18. package/dist/bin/build.d.ts.map +1 -1
  19. package/dist/bin/build.js +45 -23
  20. package/dist/bin/build.js.map +1 -1
  21. package/dist/bin/git.d.ts +1 -6
  22. package/dist/bin/git.d.ts.map +1 -1
  23. package/dist/bin/git.js +3 -13
  24. package/dist/bin/git.js.map +1 -1
  25. package/dist/bin/main.js +78 -40
  26. package/dist/bin/main.js.map +1 -1
  27. package/dist/bin/slack.d.ts +8 -0
  28. package/dist/bin/slack.d.ts.map +1 -1
  29. package/dist/bin/slack.js +18 -0
  30. package/dist/bin/slack.js.map +1 -1
  31. package/dist/bin/stdin-input.d.ts +17 -0
  32. package/dist/bin/stdin-input.d.ts.map +1 -0
  33. package/dist/bin/stdin-input.js +54 -0
  34. package/dist/bin/stdin-input.js.map +1 -0
  35. package/dist/test/bundle/cli-bundle.test.js +27 -4
  36. package/dist/test/bundle/cli-bundle.test.js.map +1 -1
  37. package/dist/test/e2e/cli-bundle.e2e.test.js +16 -9
  38. package/dist/test/e2e/cli-bundle.e2e.test.js.map +1 -1
  39. package/dist/test/unit/bin/affected-actors.test.js +7 -13
  40. package/dist/test/unit/bin/affected-actors.test.js.map +1 -1
  41. package/dist/test/unit/bin/build-cli.test.d.ts +2 -0
  42. package/dist/test/unit/bin/build-cli.test.d.ts.map +1 -0
  43. package/dist/test/unit/bin/build-cli.test.js +52 -0
  44. package/dist/test/unit/bin/build-cli.test.js.map +1 -0
  45. package/dist/test/unit/bin/build.test.js +61 -4
  46. package/dist/test/unit/bin/build.test.js.map +1 -1
  47. package/dist/test/unit/bin/git.test.js +2 -14
  48. package/dist/test/unit/bin/git.test.js.map +1 -1
  49. package/dist/test/unit/bin/slack.test.js +39 -1
  50. package/dist/test/unit/bin/slack.test.js.map +1 -1
  51. package/dist/test/unit/bin/stdin-input.test.d.ts +2 -0
  52. package/dist/test/unit/bin/stdin-input.test.d.ts.map +1 -0
  53. package/dist/test/unit/bin/stdin-input.test.js +50 -0
  54. package/dist/test/unit/bin/stdin-input.test.js.map +1 -0
  55. package/dist/tsconfig.tsbuildinfo +1 -1
  56. package/package.json +1 -1
  57. package/test/bundle/cli-bundle.test.ts +50 -4
  58. package/test/e2e/cli-bundle.e2e.test.ts +26 -11
  59. package/test/unit/bin/affected-actors.test.ts +7 -13
  60. package/test/unit/bin/build-cli.test.ts +61 -0
  61. package/test/unit/bin/build.test.ts +71 -3
  62. package/test/unit/bin/git.test.ts +1 -15
  63. package/test/unit/bin/slack.test.ts +44 -1
  64. package/test/unit/bin/stdin-input.test.ts +61 -0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "apify-test-tools",
3
- "version": "0.10.2-beta.3",
3
+ "version": "0.10.2-beta.4",
4
4
  "type": "module",
5
5
  "description": "TBD",
6
6
  "repository": {
@@ -34,11 +34,18 @@ const writeRepoFile = async (file: string, content: string) => {
34
34
  await writeFile(path.join(repoDir, file), content);
35
35
  };
36
36
 
37
- const runCli = (...args: string[]) => {
38
- const result = spawnSync(process.execPath, [bundlePath, ...args], { cwd: repoDir, env: cliEnv, encoding: 'utf8' });
37
+ const runCliWithInput = (input: string, ...args: string[]) => {
38
+ const result = spawnSync(process.execPath, [bundlePath, ...args], {
39
+ cwd: repoDir,
40
+ env: cliEnv,
41
+ encoding: 'utf8',
42
+ input,
43
+ });
39
44
  return { status: result.status, stdout: result.stdout, stderr: result.stderr };
40
45
  };
41
46
 
47
+ const runCli = (...args: string[]) => runCliWithInput('', ...args);
48
+
42
49
  const gitRange = ['--source-branch', 'feature', '--target-branch', 'main'];
43
50
 
44
51
  describe('CLI bundle', () => {
@@ -91,7 +98,9 @@ describe('CLI bundle', () => {
91
98
  'get-affected-actors',
92
99
  'report-tests',
93
100
  'build',
101
+ 'build-for-test',
94
102
  'release',
103
+ 'notify-release',
95
104
  'build-from-local',
96
105
  'delete-old-builds',
97
106
  ]) {
@@ -143,13 +152,50 @@ describe('CLI bundle', () => {
143
152
  });
144
153
 
145
154
  it('build --dry-run with nothing to build needs no token', () => {
146
- const { status, stdout, stderr } = runCli('build', ...gitRange, '--actors', 'tester/bar', '--dry-run');
155
+ const { status, stdout, stderr } = runCliWithInput(
156
+ JSON.stringify({ actors: [] }),
157
+ 'build',
158
+ '--version-number',
159
+ '0.99',
160
+ '--dry-run',
161
+ );
147
162
  expect(status, stderr).toBe(0);
148
163
  expect(JSON.parse(stdout)).toEqual({ dryRun: true, builds: [] });
149
164
  });
150
165
 
151
166
  it('build --dry-run fails cleanly when the Actor token is missing', () => {
152
- const { status, stderr } = runCli('build', ...gitRange, '--dry-run');
167
+ const { status, stderr } = runCliWithInput(
168
+ JSON.stringify({ actors: [{ actorFullName: 'tester/foo' }] }),
169
+ 'build',
170
+ '--version-number',
171
+ '0.99',
172
+ '--dry-run',
173
+ );
174
+ expect(status).toBe(1);
175
+ expect(stderr).toContain('Env var FOO_TOKEN is not set (needed for actor "tester/foo")');
176
+ });
177
+
178
+ it('build fails on empty input instead of building nothing', () => {
179
+ const { status, stderr } = runCli('build', '--version-number', '0.99', '--dry-run');
180
+ expect(status).toBe(1);
181
+ expect(stderr).toContain('got nothing');
182
+ });
183
+
184
+ it('build-for-test --dry-run with nothing to build needs no token', () => {
185
+ const { status, stdout, stderr } = runCli(
186
+ 'build-for-test',
187
+ '--target-branch',
188
+ 'main',
189
+ '--actors',
190
+ 'tester/bar',
191
+ '--dry-run',
192
+ );
193
+ expect(status, stderr).toBe(0);
194
+ expect(JSON.parse(stdout)).toEqual({ dryRun: true, builds: [] });
195
+ });
196
+
197
+ it('build-for-test --dry-run fails cleanly when the Actor token is missing', () => {
198
+ const { status, stderr } = runCli('build-for-test', '--target-branch', 'main', '--dry-run');
153
199
  expect(status).toBe(1);
154
200
  expect(stderr).toContain('Env var FOO_TOKEN is not set (needed for actor "tester/foo")');
155
201
  });
@@ -57,17 +57,20 @@ const writeRepoFile = async (file: string, content: string) => {
57
57
  // retries an unreachable API for minutes, so a hang is killed here and fails with a clear error.
58
58
  const PROCESS_TIMEOUT_MS = 8 * 60_000;
59
59
 
60
- const runCli = (...args: string[]) => {
60
+ const runCliWithInput = (input: string, ...args: string[]) => {
61
61
  const result = spawnSync(process.execPath, [bundlePath, ...args], {
62
62
  cwd: repoDir,
63
63
  env: cliEnv,
64
64
  encoding: 'utf8',
65
65
  timeout: PROCESS_TIMEOUT_MS,
66
+ input,
66
67
  });
67
68
  const error = result.error ? `\n${result.error.message}` : '';
68
69
  return { status: result.status, stdout: result.stdout, stderr: result.stderr + error };
69
70
  };
70
71
 
72
+ const runCli = (...args: string[]) => runCliWithInput('', ...args);
73
+
71
74
  interface ActorBuild {
72
75
  buildId: string;
73
76
  actorRawId: string;
@@ -117,11 +120,28 @@ describe.runIf(token)('CLI bundle against real Actors', { timeout: 2 * 60_000 },
117
120
  if (repoDir) await rm(repoDir, { recursive: true, force: true });
118
121
  });
119
122
 
120
- it('build --dry-run plans a test build of the changed Actor only', () => {
121
- const { status, stdout, stderr } = runCli(
123
+ it('build --dry-run plans a test build of the Actors it is given', () => {
124
+ const { status, stdout, stderr } = runCliWithInput(
125
+ JSON.stringify({ actors: [{ actorFullName: ACTORS[0] }] }),
122
126
  'build',
123
- '--source-branch',
124
- 'feature',
127
+ '--version-number',
128
+ '0.99',
129
+ '--repo-url',
130
+ REPO_URL,
131
+ '--dry-run',
132
+ );
133
+ expect(status, stderr).toBe(0);
134
+ expect(JSON.parse(stdout)).toEqual({
135
+ dryRun: true,
136
+ builds: [
137
+ { actorFullName: ACTORS[0], versionNumber: '0.99', gitRepoUrl: `${REPO_URL}#feature:actors/dummy-1` },
138
+ ],
139
+ });
140
+ });
141
+
142
+ it('build-for-test --dry-run plans a test build of the changed Actor only', () => {
143
+ const { status, stdout, stderr } = runCli(
144
+ 'build-for-test',
125
145
  '--target-branch',
126
146
  'main',
127
147
  '--repo-url',
@@ -132,12 +152,7 @@ describe.runIf(token)('CLI bundle against real Actors', { timeout: 2 * 60_000 },
132
152
  expect(JSON.parse(stdout)).toEqual({
133
153
  dryRun: true,
134
154
  builds: [
135
- {
136
- actorFullName: ACTORS[0],
137
- versionNumber: '0.99',
138
- gitRepoUrl: `${REPO_URL}#feature:actors/dummy-1`,
139
- useDockerCache: false,
140
- },
155
+ { actorFullName: ACTORS[0], versionNumber: '0.99', gitRepoUrl: `${REPO_URL}#feature:actors/dummy-1` },
141
156
  ],
142
157
  });
143
158
  });
@@ -28,13 +28,12 @@ describe('getAffectedActors', () => {
28
28
  .spyOn(DiffChanges, 'getChangedActors')
29
29
  .mockResolvedValue({ functional: [actorA], cosmetic: [actorB] });
30
30
  vi.spyOn(Git, 'resolveCommitSha').mockResolvedValue(headSha);
31
- vi.spyOn(Git, 'resolveBranchName').mockResolvedValue('feature');
32
31
  vi.spyOn(Git, 'getChangedFilesBetween').mockResolvedValue(changedFiles);
33
32
  vi.spyOn(Git, 'getChangelogAdditions').mockResolvedValue('- Added foo');
34
33
  });
35
34
 
36
35
  it('requires --base or --since', async () => {
37
- const options = { head: 'HEAD', ...selection };
36
+ const options = { ...selection };
38
37
  await expect(getAffectedActors(options)).rejects.toThrow('Pass --base, --since, or both');
39
38
  // An empty --since (e.g. from an empty cache file) counts as not passed
40
39
  await expect(getAffectedActors({ ...options, since: '' })).rejects.toThrow('Pass --base, --since, or both');
@@ -50,7 +49,6 @@ describe('getAffectedActors', () => {
50
49
 
51
50
  it('inspects the branch commits after --since', async () => {
52
51
  const result = await getAffectedActors({
53
- head: 'origin/feature',
54
52
  base: 'origin/master',
55
53
  since: sinceSha,
56
54
  ...selection,
@@ -58,7 +56,6 @@ describe('getAffectedActors', () => {
58
56
 
59
57
  expect(result).toStrictEqual({
60
58
  headSha,
61
- ref: 'feature',
62
59
  commits,
63
60
  changedFiles,
64
61
  changelog: '- Added foo',
@@ -66,7 +63,7 @@ describe('getAffectedActors', () => {
66
63
  cosmeticallyChangedActors: [actorB],
67
64
  });
68
65
  expect(getCommitsSpy).toHaveBeenCalledWith({
69
- sourceBranch: 'origin/feature',
66
+ sourceBranch: 'HEAD',
70
67
  targetBranch: 'origin/master',
71
68
  baseCommit: sinceSha,
72
69
  });
@@ -90,7 +87,7 @@ describe('getAffectedActors', () => {
90
87
  changedActorsSpy.mockImplementation(async ({ filepathsChanged }) =>
91
88
  filepathsChanged[0] === 'own-file' ? own : { functional: [actorA], cosmetic: [actorB] },
92
89
  );
93
- return getAffectedActors({ head: 'feature', base: 'master', since: sinceSha, ...selection });
90
+ return getAffectedActors({ base: 'master', since: sinceSha, ...selection });
94
91
  };
95
92
 
96
93
  it("classifies the branch's own files against the whole branch, ignoring --since", async () => {
@@ -100,7 +97,7 @@ describe('getAffectedActors', () => {
100
97
  actorConfigs: [actorA, actorB],
101
98
  commits,
102
99
  });
103
- expect(getCommitsSpy).toHaveBeenCalledWith({ sourceBranch: 'feature', targetBranch: 'master' });
100
+ expect(getCommitsSpy).toHaveBeenCalledWith({ sourceBranch: 'HEAD', targetBranch: 'master' });
104
101
  });
105
102
 
106
103
  it('reports everything when the branch has functional changes of its own', async () => {
@@ -135,18 +132,17 @@ describe('getAffectedActors', () => {
135
132
  });
136
133
 
137
134
  it('inspects everything after --since up to --head', async () => {
138
- const result = await getAffectedActors({ head: 'release', since: 'HEAD~2', ...selection });
135
+ const result = await getAffectedActors({ since: 'HEAD~2', ...selection });
139
136
 
140
137
  expect(result).toStrictEqual({
141
138
  headSha,
142
- ref: 'feature',
143
139
  commits,
144
140
  changedFiles,
145
141
  changelog: '- Added foo',
146
142
  functionallyChangedActors: [actorA],
147
143
  cosmeticallyChangedActors: [actorB],
148
144
  });
149
- expect(resolveBaseSpy).toHaveBeenCalledWith('HEAD~2', 'release');
145
+ expect(resolveBaseSpy).toHaveBeenCalledWith('HEAD~2');
150
146
  expect(commitsBetweenSpy).toHaveBeenCalledWith(sinceSha, headSha);
151
147
  expect(Git.getChangedFilesBetween).toHaveBeenCalledWith(sinceSha, headSha);
152
148
  expect(Git.getChangelogAdditions).toHaveBeenCalledWith(sinceSha, headSha, changedFiles);
@@ -155,7 +151,7 @@ describe('getAffectedActors', () => {
155
151
  it('has no commits when head is the --since commit', async () => {
156
152
  resolveBaseSpy.mockResolvedValue(headSha);
157
153
 
158
- const result = await getAffectedActors({ head: 'HEAD', since: headSha, ...selection });
154
+ const result = await getAffectedActors({ since: headSha, ...selection });
159
155
 
160
156
  expect(result.commits).toStrictEqual([]);
161
157
  // Empty `git log x..x` output doesn't parse as a commit, so it's never run
@@ -169,7 +165,6 @@ describe('toAffectedActorsDocument', () => {
169
165
  expect(
170
166
  toAffectedActorsDocument({
171
167
  headSha,
172
- ref: null,
173
168
  commits,
174
169
  changedFiles: ['src/main.ts'],
175
170
  changelog: null,
@@ -178,7 +173,6 @@ describe('toAffectedActorsDocument', () => {
178
173
  }),
179
174
  ).toStrictEqual({
180
175
  version: 1,
181
- ref: null,
182
176
  head: headSha,
183
177
  commits,
184
178
  changedFiles: ['src/main.ts'],
@@ -0,0 +1,61 @@
1
+ import { execFileSync, spawnSync } from 'node:child_process';
2
+ import { chmod, mkdtemp, rm } from 'node:fs/promises';
3
+ import path from 'node:path';
4
+ import process from 'node:process';
5
+ import { fileURLToPath } from 'node:url';
6
+
7
+ import { afterAll, beforeAll, describe, expect, it } from 'vitest';
8
+
9
+ const projectRoot = fileURLToPath(new URL('../../../', import.meta.url));
10
+ const compilerPath = fileURLToPath(new URL('../../../node_modules/typescript/bin/tsc', import.meta.url));
11
+ let cliPath: string;
12
+ let buildDir: string | undefined;
13
+
14
+ const run = (command: string[], input: string, env: NodeJS.ProcessEnv = process.env) =>
15
+ spawnSync(cliPath, command, { encoding: 'utf8', input, stdio: ['pipe', 'pipe', 'pipe'], env });
16
+ const runBuild = (input: string, args: string[] = ['--version-number', '0.99']) => run(['build', ...args], input);
17
+
18
+ describe('build and notify-release CLI', () => {
19
+ beforeAll(async () => {
20
+ buildDir = await mkdtemp(path.join(projectRoot, '.cli-test-'));
21
+ execFileSync(process.execPath, [compilerPath, '--outDir', buildDir], { cwd: projectRoot });
22
+ cliPath = path.join(buildDir, 'bin/main.js');
23
+ await chmod(cliPath, 0o755);
24
+ });
25
+
26
+ afterAll(async () => {
27
+ if (buildDir) await rm(buildDir, { recursive: true, force: true });
28
+ });
29
+
30
+ it('builds nothing for an empty list and prints an empty build list', () => {
31
+ const result = runBuild('{"actors":[]}');
32
+ expect(result.status, result.stderr).toBe(0);
33
+ expect(JSON.parse(result.stdout)).toStrictEqual([]);
34
+ });
35
+
36
+ it('fails on empty stdin instead of building nothing', () => {
37
+ const result = runBuild('');
38
+ expect(result.status).toBe(1);
39
+ expect(result.stderr).toContain('Expected the Actors to build as JSON on stdin, got nothing');
40
+ expect(result.stdout).toBe('');
41
+ });
42
+
43
+ it('rejects a malformed --version-number', () => {
44
+ const result = runBuild('{"actors":[]}', ['--version-number', 'latest']);
45
+ expect(result.status).toBe(1);
46
+ expect(result.stderr).toContain('--version-number must look like 0.1 or be "default"');
47
+ });
48
+
49
+ it('requires --version-number', () => {
50
+ const result = runBuild('{"actors":[]}', []);
51
+ expect(result.status).toBe(1);
52
+ expect(result.stderr).toContain('Missing required argument: version-number');
53
+ });
54
+
55
+ it('notify-release announces nothing when there are no new commits', () => {
56
+ const document = { commits: [], changedFiles: [], changelog: null, functionallyChangedActors: [] };
57
+ const result = run(['notify-release'], JSON.stringify(document));
58
+ expect(result.status, result.stderr).toBe(0);
59
+ expect(result.stderr).toContain('No new commits, so there is no release to announce');
60
+ });
61
+ });
@@ -7,11 +7,15 @@ import { ActorSourceType, ApifyClient } from 'apify-client';
7
7
  import { afterAll, afterEach, beforeAll, beforeEach, describe, expect, it, vi } from 'vitest';
8
8
 
9
9
  import {
10
+ ApifyBuilder,
10
11
  assertRepoUrlMatchesDefaultVersion,
12
+ buildActors,
11
13
  deleteOldBuilds,
12
14
  resolveDefaultVersion,
13
15
  runBuilds,
14
16
  } from '../../../bin/build.js';
17
+ import * as Git from '../../../bin/git.js';
18
+ import { logger } from '../../../bin/logger.js';
15
19
  import type { ActorConfig } from '../../../bin/types.js';
16
20
  import { ExistingDir, ExistingFile, RelativeDir } from '../../../bin/utils/path/repo-relative.js';
17
21
 
@@ -84,6 +88,71 @@ describe('assertRepoUrlMatchesDefaultVersion', () => {
84
88
  });
85
89
  });
86
90
 
91
+ describe('runBuilds version number', () => {
92
+ // Only the name and folder of the Actor matter here, the builder is stubbed
93
+ const actorConfig = { actorFullName: ACTOR, folder: { isRoot: () => true } } as unknown as ActorConfig;
94
+ // Restored after each test, so the stub never reaches the tests that run the real builder
95
+ afterEach(() => {
96
+ vi.mocked(ApifyBuilder.fromActorConfig).mockRestore();
97
+ });
98
+
99
+ const dryRunBuild = async (versionNumber: string, actorInfo = makeActorInfo()) => {
100
+ vi.spyOn(ApifyBuilder, 'fromActorConfig').mockReturnValue({
101
+ getActorInfo: async () => actorInfo,
102
+ } as unknown as ApifyBuilder);
103
+ const result = await runBuilds({
104
+ actorConfigs: [actorConfig],
105
+ versionNumber,
106
+ repoUrl: REPO_URL,
107
+ shouldVerifyRepoUrl: false,
108
+ branch: 'master',
109
+ dryRun: true,
110
+ });
111
+ if (Array.isArray(result)) throw new Error('A dry run must return a plan, not builds');
112
+ return result.builds[0].versionNumber;
113
+ };
114
+
115
+ it('builds the version it is given', async () => {
116
+ await expect(dryRunBuild('0.99')).resolves.toBe('0.99');
117
+ });
118
+
119
+ it("resolves default to the version the Actor's default build tag points to", async () => {
120
+ await expect(dryRunBuild('default')).resolves.toBe('0.1');
121
+ });
122
+
123
+ it('warns when the default version does not give its builds the default build tag', async () => {
124
+ const warnSpy = vi.spyOn(logger, 'warn').mockImplementation(() => undefined);
125
+ await dryRunBuild('default');
126
+ // The fixture's version 0.1 has no build tag of its own
127
+ expect(warnSpy).toHaveBeenCalledWith(expect.stringContaining('users keep running the previous build'));
128
+ });
129
+
130
+ it('does not warn when the default version gives its builds the default build tag', async () => {
131
+ const warnSpy = vi.spyOn(logger, 'warn').mockImplementation(() => undefined);
132
+ const versions = [{ versionNumber: '0.1', buildTag: 'latest', sourceType: ActorSourceType.GitRepo }];
133
+ await dryRunBuild('default', makeActorInfo({ versions } as Partial<ApifyClientTypes.Actor>));
134
+ expect(warnSpy).not.toHaveBeenCalled();
135
+ });
136
+ });
137
+
138
+ describe('buildActors', () => {
139
+ it('builds nothing and needs no branch or repository when there are no Actors', async () => {
140
+ const branchSpy = vi.spyOn(Git, 'getCurrentBranch');
141
+ const repoSpy = vi.spyOn(Git, 'resolveRepoUrl');
142
+
143
+ await expect(buildActors({ actorConfigs: [], versionNumber: '0.99', dryRun: false })).resolves.toStrictEqual(
144
+ [],
145
+ );
146
+ // A dry run keeps the dry-run shape even with nothing to build
147
+ await expect(buildActors({ actorConfigs: [], versionNumber: '0.99', dryRun: true })).resolves.toStrictEqual({
148
+ dryRun: true,
149
+ builds: [],
150
+ });
151
+ expect(branchSpy).not.toHaveBeenCalled();
152
+ expect(repoSpy).not.toHaveBeenCalled();
153
+ });
154
+ });
155
+
87
156
  describe('dry run', () => {
88
157
  const TOKEN_ENV_VAR = 'APIFY_TOKEN_OWNER';
89
158
  const originalCwd = process.cwd();
@@ -150,7 +219,7 @@ describe('dry run', () => {
150
219
  shouldVerifyRepoUrl: true,
151
220
  branch: 'feature',
152
221
  dryRun: true,
153
- useDockerCache: false,
222
+ versionNumber: '0.99',
154
223
  });
155
224
 
156
225
  it('should return a build plan without creating versions or builds', async () => {
@@ -161,7 +230,6 @@ describe('dry run', () => {
161
230
  actorFullName: ACTOR,
162
231
  versionNumber: '0.99',
163
232
  gitRepoUrl: `${REPO_URL}#feature:actors/a`,
164
- useDockerCache: false,
165
233
  },
166
234
  ],
167
235
  });
@@ -182,7 +250,7 @@ describe('dry run', () => {
182
250
  shouldVerifyRepoUrl: true,
183
251
  branch: 'feature',
184
252
  dryRun: true,
185
- useDockerCache: false,
253
+ versionNumber: '0.99',
186
254
  }),
187
255
  ).rejects.toThrow('Repository mismatch');
188
256
  });
@@ -12,7 +12,6 @@ import {
12
12
  hasMergeFromTarget,
13
13
  normalizeRepoUrl,
14
14
  parseBaseCommit,
15
- resolveBranchName,
16
15
  resolveCommitSha,
17
16
  resolveReleaseBaseCommit,
18
17
  resolveRepoUrl,
@@ -323,7 +322,7 @@ describe('getCurrentBranch', () => {
323
322
  const gitCommandSpy = vi.spyOn(Utils, 'runGitCommand').mockResolvedValue('master');
324
323
  await expect(getCurrentBranch()).resolves.toBe('master');
325
324
  expect(gitCommandSpy).toHaveBeenCalledWith(['rev-parse', '--abbrev-ref', 'HEAD'], {
326
- operation: 'determine the current branch for release',
325
+ operation: 'determine the checked-out branch',
327
326
  });
328
327
  });
329
328
 
@@ -532,16 +531,3 @@ describe('resolveCommitSha', () => {
532
531
  });
533
532
  });
534
533
  });
535
-
536
- describe('resolveBranchName', () => {
537
- it.each([
538
- ['a local branch', 'refs/heads/feature/foo', 'feature/foo'],
539
- ['a remote branch, dropping the remote', 'refs/remotes/origin/feature/foo', 'feature/foo'],
540
- ['a tag', 'refs/tags/v1.0.0', null],
541
- ['a detached HEAD', 'HEAD', null],
542
- ['a SHA', '', null],
543
- ])('handles %s', async (_, fullName, expected) => {
544
- vi.spyOn(Utils, 'runGitCommand').mockResolvedValue(fullName);
545
- await expect(resolveBranchName('some-ref')).resolves.toBe(expected);
546
- });
547
- });
@@ -1,7 +1,7 @@
1
1
  import { WebClient } from '@slack/web-api';
2
2
  import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
3
3
 
4
- import { notifyToSlack } from '../../../bin/slack.js';
4
+ import { announceRelease, notifyToSlack } from '../../../bin/slack.js';
5
5
 
6
6
  vi.mock('@slack/web-api', () => ({ WebClient: vi.fn() }));
7
7
 
@@ -63,3 +63,46 @@ describe('notifyToSlack', () => {
63
63
  expect(postMessage).toHaveBeenCalledTimes(2);
64
64
  });
65
65
  });
66
+
67
+ describe('announceRelease', () => {
68
+ const commit = (author: string, message: string) => ({ sha: '1', author, date: '', message });
69
+
70
+ beforeEach(() => {
71
+ vi.stubEnv(TOKEN_ENV_VAR, 'token');
72
+ vi.mocked(WebClient).mockImplementation(function () {
73
+ return { chat: { postMessage }, auth: { test: authTest } };
74
+ } as never);
75
+ });
76
+
77
+ afterEach(() => {
78
+ vi.unstubAllEnvs();
79
+ vi.clearAllMocks();
80
+ });
81
+
82
+ it('should announce nothing, and need no token, when there are no new commits', async () => {
83
+ vi.stubEnv(TOKEN_ENV_VAR, '');
84
+ await announceRelease({
85
+ commits: [],
86
+ changedFiles: [],
87
+ changelog: null,
88
+ dryRun: false,
89
+ reportSlackChannel: '#notif',
90
+ });
91
+ expect(WebClient).not.toHaveBeenCalled();
92
+ });
93
+
94
+ it('should announce the release by the author of the newest commit, named after the repository', async () => {
95
+ await announceRelease({
96
+ commits: [commit('Old<old@example.com>', 'fix: older'), commit('New<new@example.com>', 'feat: newer')],
97
+ changedFiles: ['src/main.ts'],
98
+ changelog: '- Added foo',
99
+ repoUrl: 'git@github.com:owner/my-repo',
100
+ dryRun: false,
101
+ reportSlackChannel: '#notif',
102
+ });
103
+ expect(postMessage).toHaveBeenCalledWith({
104
+ channel: '#notif',
105
+ text: expect.stringContaining('*my-repo* – New release (by New)'),
106
+ });
107
+ });
108
+ });
@@ -0,0 +1,61 @@
1
+ import { describe, expect, it } from 'vitest';
2
+
3
+ import { parseBuildInput, parseReleaseChanges } from '../../../bin/stdin-input.js';
4
+
5
+ describe('parseBuildInput', () => {
6
+ it('reads the Actors', () => {
7
+ expect(parseBuildInput('{"actors":[{"actorFullName":"owner/a"}]}')).toStrictEqual({
8
+ actors: [{ actorFullName: 'owner/a' }],
9
+ });
10
+ });
11
+
12
+ it('ignores the other fields, so whole get-affected-actors fields can be passed along', () => {
13
+ const input = { head: 'f'.repeat(40), ref: 'feature', actors: [{ actorFullName: 'owner/a', extra: 1 }] };
14
+ expect(parseBuildInput(JSON.stringify(input))).toStrictEqual({ actors: [{ actorFullName: 'owner/a' }] });
15
+ });
16
+
17
+ it('accepts an empty list, which builds nothing', () => {
18
+ expect(parseBuildInput('{"actors":[]}')).toStrictEqual({ actors: [] });
19
+ });
20
+
21
+ it('rejects empty input, which is what a failed command earlier in a pipe leaves behind', () => {
22
+ expect(() => parseBuildInput(' \n')).toThrow('Expected the Actors to build as JSON on stdin, got nothing');
23
+ });
24
+
25
+ it('rejects input that is not JSON', () => {
26
+ expect(() => parseBuildInput('owner/a')).toThrow('not valid JSON');
27
+ });
28
+
29
+ it('rejects JSON that is not a list of Actors, e.g. a whole get-affected-actors document', () => {
30
+ expect(() => parseBuildInput('{"functionallyChangedActors":[{"actorFullName":"owner/a"}]}')).toThrow(
31
+ 'is not the Actors to build',
32
+ );
33
+ });
34
+ });
35
+
36
+ describe('parseReleaseChanges', () => {
37
+ const commit = { sha: '1'.repeat(40), author: 'Dev<dev@example.com>', date: 'Date', message: 'feat: foo' };
38
+
39
+ it('reads what changed from a get-affected-actors document, ignoring the rest', () => {
40
+ const document = {
41
+ version: 1,
42
+ ref: 'master',
43
+ head: '1'.repeat(40),
44
+ commits: [commit],
45
+ changedFiles: ['src/main.ts'],
46
+ changelog: '- Added foo',
47
+ functionallyChangedActors: [{ actorFullName: 'owner/a' }],
48
+ cosmeticallyChangedActors: [],
49
+ };
50
+ expect(parseReleaseChanges(JSON.stringify(document))).toStrictEqual({
51
+ commits: [commit],
52
+ changedFiles: ['src/main.ts'],
53
+ changelog: '- Added foo',
54
+ });
55
+ });
56
+
57
+ it('rejects empty input and input that is not a get-affected-actors document', () => {
58
+ expect(() => parseReleaseChanges('')).toThrow('got nothing');
59
+ expect(() => parseReleaseChanges('{"actors":[]}')).toThrow('not the output of get-affected-actors');
60
+ });
61
+ });