apify-test-tools 0.10.2-beta.2 → 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.
- package/.github/cli/apify-test-tools.mjs +140 -140
- package/.github/scripts/run-with-apify-tokens.mjs +1 -1
- package/.github/workflows/public_pr-build-test.yaml +25 -25
- package/.github/workflows/public_push-build-latest.yaml +20 -4
- package/CHANGELOG.md +2 -0
- package/README.md +119 -14
- package/bin/affected-actors.ts +157 -0
- package/bin/build.ts +55 -38
- package/bin/diff-changes.ts +25 -17
- package/bin/git.ts +44 -44
- package/bin/main.ts +132 -106
- package/bin/slack.ts +24 -0
- package/bin/stdin-input.ts +65 -0
- package/dist/bin/affected-actors.d.ts +45 -0
- package/dist/bin/affected-actors.d.ts.map +1 -0
- package/dist/bin/affected-actors.js +87 -0
- package/dist/bin/affected-actors.js.map +1 -0
- package/dist/bin/build.d.ts +12 -5
- package/dist/bin/build.d.ts.map +1 -1
- package/dist/bin/build.js +45 -23
- package/dist/bin/build.js.map +1 -1
- package/dist/bin/diff-changes.d.ts +8 -2
- package/dist/bin/diff-changes.d.ts.map +1 -1
- package/dist/bin/diff-changes.js +16 -16
- package/dist/bin/diff-changes.js.map +1 -1
- package/dist/bin/git.d.ts +19 -14
- package/dist/bin/git.d.ts.map +1 -1
- package/dist/bin/git.js +37 -35
- package/dist/bin/git.js.map +1 -1
- package/dist/bin/main.js +110 -86
- package/dist/bin/main.js.map +1 -1
- package/dist/bin/slack.d.ts +8 -0
- package/dist/bin/slack.d.ts.map +1 -1
- package/dist/bin/slack.js +18 -0
- package/dist/bin/slack.js.map +1 -1
- package/dist/bin/stdin-input.d.ts +17 -0
- package/dist/bin/stdin-input.d.ts.map +1 -0
- package/dist/bin/stdin-input.js +54 -0
- package/dist/bin/stdin-input.js.map +1 -0
- package/dist/test/bundle/cli-bundle.test.js +36 -6
- package/dist/test/bundle/cli-bundle.test.js.map +1 -1
- package/dist/test/e2e/cli-bundle.e2e.test.js +16 -9
- package/dist/test/e2e/cli-bundle.e2e.test.js.map +1 -1
- package/dist/test/unit/bin/affected-actors.test.d.ts +2 -0
- package/dist/test/unit/bin/affected-actors.test.d.ts.map +1 -0
- package/dist/test/unit/bin/affected-actors.test.js +154 -0
- package/dist/test/unit/bin/affected-actors.test.js.map +1 -0
- package/dist/test/unit/bin/build-cli.test.d.ts +2 -0
- package/dist/test/unit/bin/build-cli.test.d.ts.map +1 -0
- package/dist/test/unit/bin/build-cli.test.js +52 -0
- package/dist/test/unit/bin/build-cli.test.js.map +1 -0
- package/dist/test/unit/bin/build.test.js +61 -4
- package/dist/test/unit/bin/build.test.js.map +1 -1
- package/dist/test/unit/bin/diff-changes.test.js +78 -49
- package/dist/test/unit/bin/diff-changes.test.js.map +1 -1
- package/dist/test/unit/bin/git.test.js +39 -54
- package/dist/test/unit/bin/git.test.js.map +1 -1
- package/dist/test/unit/bin/slack.test.js +39 -1
- package/dist/test/unit/bin/slack.test.js.map +1 -1
- package/dist/test/unit/bin/stdin-input.test.d.ts +2 -0
- package/dist/test/unit/bin/stdin-input.test.d.ts.map +1 -0
- package/dist/test/unit/bin/stdin-input.test.js +50 -0
- package/dist/test/unit/bin/stdin-input.test.js.map +1 -0
- package/dist/test/unit/should-built-and-test.test.js +23 -17
- package/dist/test/unit/should-built-and-test.test.js.map +1 -1
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/package.json +1 -1
- package/test/bundle/cli-bundle.test.ts +59 -6
- package/test/e2e/cli-bundle.e2e.test.ts +26 -11
- package/test/unit/bin/affected-actors.test.ts +184 -0
- package/test/unit/bin/build-cli.test.ts +61 -0
- package/test/unit/bin/build.test.ts +71 -3
- package/test/unit/bin/diff-changes.test.ts +87 -49
- package/test/unit/bin/git.test.ts +44 -55
- package/test/unit/bin/slack.test.ts +44 -1
- package/test/unit/bin/stdin-input.test.ts +61 -0
- package/test/unit/should-built-and-test.test.ts +27 -17
package/package.json
CHANGED
|
@@ -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
|
|
38
|
-
const result = spawnSync(process.execPath, [bundlePath, ...args], {
|
|
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
|
]) {
|
|
@@ -130,19 +139,63 @@ describe('CLI bundle', () => {
|
|
|
130
139
|
});
|
|
131
140
|
|
|
132
141
|
it('get-affected-actors returns only the Actor whose files changed', () => {
|
|
133
|
-
|
|
142
|
+
// `feature` is checked out, and the range always ends at HEAD
|
|
143
|
+
const { status, stdout, stderr } = runCli('get-affected-actors', '--base', 'main');
|
|
134
144
|
expect(status, stderr).toBe(0);
|
|
135
|
-
expect(JSON.parse(stdout)).toMatchObject(
|
|
145
|
+
expect(JSON.parse(stdout)).toMatchObject({
|
|
146
|
+
version: 1,
|
|
147
|
+
head: headSha,
|
|
148
|
+
functionallyChangedActors: [{ actorFullName: 'tester/foo' }],
|
|
149
|
+
// The root README isn't in tester/bar's folder, so it doesn't count for it
|
|
150
|
+
cosmeticallyChangedActors: [],
|
|
151
|
+
});
|
|
136
152
|
});
|
|
137
153
|
|
|
138
154
|
it('build --dry-run with nothing to build needs no token', () => {
|
|
139
|
-
const { status, stdout, stderr } =
|
|
155
|
+
const { status, stdout, stderr } = runCliWithInput(
|
|
156
|
+
JSON.stringify({ actors: [] }),
|
|
157
|
+
'build',
|
|
158
|
+
'--version-number',
|
|
159
|
+
'0.99',
|
|
160
|
+
'--dry-run',
|
|
161
|
+
);
|
|
140
162
|
expect(status, stderr).toBe(0);
|
|
141
163
|
expect(JSON.parse(stdout)).toEqual({ dryRun: true, builds: [] });
|
|
142
164
|
});
|
|
143
165
|
|
|
144
166
|
it('build --dry-run fails cleanly when the Actor token is missing', () => {
|
|
145
|
-
const { status, stderr } =
|
|
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');
|
|
146
199
|
expect(status).toBe(1);
|
|
147
200
|
expect(stderr).toContain('Env var FOO_TOKEN is not set (needed for actor "tester/foo")');
|
|
148
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
|
|
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
|
|
121
|
-
const { status, stdout, stderr } =
|
|
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
|
-
'--
|
|
124
|
-
'
|
|
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
|
});
|
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
import type { MockInstance } from 'vitest';
|
|
2
|
+
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
|
3
|
+
|
|
4
|
+
import { getAffectedActors, toAffectedActorsDocument } from '../../../bin/affected-actors.js';
|
|
5
|
+
import * as DiffChanges from '../../../bin/diff-changes.js';
|
|
6
|
+
import * as Git from '../../../bin/git.js';
|
|
7
|
+
import type { ActorConfig, Commit } from '../../../bin/types.js';
|
|
8
|
+
import * as LoadConfig from '../../../bin/utils/config/load-config.js';
|
|
9
|
+
|
|
10
|
+
// Everything that reads the configs is mocked, so only the identity and name of an Actor matter here
|
|
11
|
+
const actorA = { actorFullName: 'owner/a' } as ActorConfig;
|
|
12
|
+
const actorB = { actorFullName: 'owner/b' } as ActorConfig;
|
|
13
|
+
|
|
14
|
+
const headSha = 'f'.repeat(40);
|
|
15
|
+
const sinceSha = '5'.repeat(40);
|
|
16
|
+
const commit = (sha: string): Commit => ({ sha, author: 'Dev<dev@example.com>', date: 'Date', message: 'msg' });
|
|
17
|
+
const commits = [commit('1'.repeat(40)), commit('2'.repeat(40))];
|
|
18
|
+
const changedFiles = ['src/main.ts', 'CHANGELOG.md'];
|
|
19
|
+
|
|
20
|
+
const selection = { actors: [], ignore: [] };
|
|
21
|
+
|
|
22
|
+
describe('getAffectedActors', () => {
|
|
23
|
+
let changedActorsSpy: MockInstance;
|
|
24
|
+
|
|
25
|
+
beforeEach(() => {
|
|
26
|
+
vi.spyOn(LoadConfig, 'readConfigFile').mockResolvedValue([actorA, actorB]);
|
|
27
|
+
changedActorsSpy = vi
|
|
28
|
+
.spyOn(DiffChanges, 'getChangedActors')
|
|
29
|
+
.mockResolvedValue({ functional: [actorA], cosmetic: [actorB] });
|
|
30
|
+
vi.spyOn(Git, 'resolveCommitSha').mockResolvedValue(headSha);
|
|
31
|
+
vi.spyOn(Git, 'getChangedFilesBetween').mockResolvedValue(changedFiles);
|
|
32
|
+
vi.spyOn(Git, 'getChangelogAdditions').mockResolvedValue('- Added foo');
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
it('requires --base or --since', async () => {
|
|
36
|
+
const options = { ...selection };
|
|
37
|
+
await expect(getAffectedActors(options)).rejects.toThrow('Pass --base, --since, or both');
|
|
38
|
+
// An empty --since (e.g. from an empty cache file) counts as not passed
|
|
39
|
+
await expect(getAffectedActors({ ...options, since: '' })).rejects.toThrow('Pass --base, --since, or both');
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
describe('with --base', () => {
|
|
43
|
+
let getCommitsSpy: MockInstance;
|
|
44
|
+
|
|
45
|
+
beforeEach(() => {
|
|
46
|
+
vi.spyOn(Git, 'hasMergeFromTarget').mockResolvedValue(false);
|
|
47
|
+
getCommitsSpy = vi.spyOn(Git, 'getCommits').mockResolvedValue(commits);
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
it('inspects the branch commits after --since', async () => {
|
|
51
|
+
const result = await getAffectedActors({
|
|
52
|
+
base: 'origin/master',
|
|
53
|
+
since: sinceSha,
|
|
54
|
+
...selection,
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
expect(result).toStrictEqual({
|
|
58
|
+
headSha,
|
|
59
|
+
commits,
|
|
60
|
+
changedFiles,
|
|
61
|
+
changelog: '- Added foo',
|
|
62
|
+
functionallyChangedActors: [actorA],
|
|
63
|
+
cosmeticallyChangedActors: [actorB],
|
|
64
|
+
});
|
|
65
|
+
expect(getCommitsSpy).toHaveBeenCalledWith({
|
|
66
|
+
sourceBranch: 'HEAD',
|
|
67
|
+
targetBranch: 'origin/master',
|
|
68
|
+
baseCommit: sinceSha,
|
|
69
|
+
});
|
|
70
|
+
// From the parent of the oldest commit to the newest one, for the files and the changelog alike
|
|
71
|
+
expect(Git.getChangedFilesBetween).toHaveBeenCalledWith(`${commits[0].sha}~`, commits[1].sha);
|
|
72
|
+
expect(Git.getChangelogAdditions).toHaveBeenCalledWith(`${commits[0].sha}~`, commits[1].sha, changedFiles);
|
|
73
|
+
expect(changedActorsSpy).toHaveBeenCalledWith({
|
|
74
|
+
filepathsChanged: changedFiles,
|
|
75
|
+
actorConfigs: [actorA, actorB],
|
|
76
|
+
commits,
|
|
77
|
+
});
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
describe('when the branch merged --base in', () => {
|
|
81
|
+
beforeEach(() => {
|
|
82
|
+
vi.spyOn(Git, 'hasMergeFromTarget').mockResolvedValue(true);
|
|
83
|
+
vi.spyOn(Git, 'getBranchOnlyChangedFiles').mockResolvedValue(['own-file']);
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
const getWithOwnChanges = async (own: DiffChanges.ChangedActors) => {
|
|
87
|
+
changedActorsSpy.mockImplementation(async ({ filepathsChanged }) =>
|
|
88
|
+
filepathsChanged[0] === 'own-file' ? own : { functional: [actorA], cosmetic: [actorB] },
|
|
89
|
+
);
|
|
90
|
+
return getAffectedActors({ base: 'master', since: sinceSha, ...selection });
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
it("classifies the branch's own files against the whole branch, ignoring --since", async () => {
|
|
94
|
+
await getWithOwnChanges({ functional: [actorA], cosmetic: [] });
|
|
95
|
+
expect(changedActorsSpy).toHaveBeenCalledWith({
|
|
96
|
+
filepathsChanged: ['own-file'],
|
|
97
|
+
actorConfigs: [actorA, actorB],
|
|
98
|
+
commits,
|
|
99
|
+
});
|
|
100
|
+
expect(getCommitsSpy).toHaveBeenCalledWith({ sourceBranch: 'HEAD', targetBranch: 'master' });
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
it('reports everything when the branch has functional changes of its own', async () => {
|
|
104
|
+
const result = await getWithOwnChanges({ functional: [actorA], cosmetic: [] });
|
|
105
|
+
expect(result.functionallyChangedActors).toStrictEqual([actorA]);
|
|
106
|
+
expect(result.cosmeticallyChangedActors).toStrictEqual([actorB]);
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
it('reports only cosmetic Actors when its own changes are only cosmetic', async () => {
|
|
110
|
+
const result = await getWithOwnChanges({ functional: [], cosmetic: [actorB] });
|
|
111
|
+
expect(result.functionallyChangedActors).toStrictEqual([]);
|
|
112
|
+
expect(result.cosmeticallyChangedActors).toStrictEqual([actorB]);
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
it('reports nothing when it has no changes of its own', async () => {
|
|
116
|
+
const result = await getWithOwnChanges({ functional: [], cosmetic: [] });
|
|
117
|
+
expect(result.functionallyChangedActors).toStrictEqual([]);
|
|
118
|
+
expect(result.cosmeticallyChangedActors).toStrictEqual([]);
|
|
119
|
+
// The range is still reported
|
|
120
|
+
expect(result.commits).toStrictEqual(commits);
|
|
121
|
+
});
|
|
122
|
+
});
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
describe('with --since only', () => {
|
|
126
|
+
let resolveBaseSpy: MockInstance;
|
|
127
|
+
let commitsBetweenSpy: MockInstance;
|
|
128
|
+
|
|
129
|
+
beforeEach(() => {
|
|
130
|
+
resolveBaseSpy = vi.spyOn(Git, 'resolveReleaseBaseCommit').mockResolvedValue(sinceSha);
|
|
131
|
+
commitsBetweenSpy = vi.spyOn(Git, 'getCommitsBetween').mockResolvedValue(commits);
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
it('inspects everything after --since up to --head', async () => {
|
|
135
|
+
const result = await getAffectedActors({ since: 'HEAD~2', ...selection });
|
|
136
|
+
|
|
137
|
+
expect(result).toStrictEqual({
|
|
138
|
+
headSha,
|
|
139
|
+
commits,
|
|
140
|
+
changedFiles,
|
|
141
|
+
changelog: '- Added foo',
|
|
142
|
+
functionallyChangedActors: [actorA],
|
|
143
|
+
cosmeticallyChangedActors: [actorB],
|
|
144
|
+
});
|
|
145
|
+
expect(resolveBaseSpy).toHaveBeenCalledWith('HEAD~2');
|
|
146
|
+
expect(commitsBetweenSpy).toHaveBeenCalledWith(sinceSha, headSha);
|
|
147
|
+
expect(Git.getChangedFilesBetween).toHaveBeenCalledWith(sinceSha, headSha);
|
|
148
|
+
expect(Git.getChangelogAdditions).toHaveBeenCalledWith(sinceSha, headSha, changedFiles);
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
it('has no commits when head is the --since commit', async () => {
|
|
152
|
+
resolveBaseSpy.mockResolvedValue(headSha);
|
|
153
|
+
|
|
154
|
+
const result = await getAffectedActors({ since: headSha, ...selection });
|
|
155
|
+
|
|
156
|
+
expect(result.commits).toStrictEqual([]);
|
|
157
|
+
// Empty `git log x..x` output doesn't parse as a commit, so it's never run
|
|
158
|
+
expect(commitsBetweenSpy).not.toHaveBeenCalled();
|
|
159
|
+
});
|
|
160
|
+
});
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
describe('toAffectedActorsDocument', () => {
|
|
164
|
+
it('keeps only the Actor names, the config file stays the source of truth for the rest', () => {
|
|
165
|
+
expect(
|
|
166
|
+
toAffectedActorsDocument({
|
|
167
|
+
headSha,
|
|
168
|
+
commits,
|
|
169
|
+
changedFiles: ['src/main.ts'],
|
|
170
|
+
changelog: null,
|
|
171
|
+
functionallyChangedActors: [actorA],
|
|
172
|
+
cosmeticallyChangedActors: [actorB],
|
|
173
|
+
}),
|
|
174
|
+
).toStrictEqual({
|
|
175
|
+
version: 1,
|
|
176
|
+
head: headSha,
|
|
177
|
+
commits,
|
|
178
|
+
changedFiles: ['src/main.ts'],
|
|
179
|
+
changelog: null,
|
|
180
|
+
functionallyChangedActors: [{ actorFullName: 'owner/a' }],
|
|
181
|
+
cosmeticallyChangedActors: [{ actorFullName: 'owner/b' }],
|
|
182
|
+
});
|
|
183
|
+
});
|
|
184
|
+
});
|
|
@@ -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
|
-
|
|
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
|
-
|
|
253
|
+
versionNumber: '0.99',
|
|
186
254
|
}),
|
|
187
255
|
).rejects.toThrow('Repository mismatch');
|
|
188
256
|
});
|