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.
- package/.github/cli/apify-test-tools.mjs +117 -118
- 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 +6 -0
- package/CONTRIBUTING.md +4 -1
- package/README.md +104 -56
- package/bin/affected-actors.ts +135 -103
- package/bin/build.ts +53 -38
- package/bin/diff-changes.ts +7 -6
- package/bin/diff-json-schema.ts +5 -6
- package/bin/git.ts +24 -69
- package/bin/main.ts +46 -80
- package/bin/slack.ts +15 -4
- package/bin/stdin-input.ts +65 -0
- package/bin/types.ts +6 -0
- package/dist/bin/affected-actors.d.ts +23 -27
- package/dist/bin/affected-actors.d.ts.map +1 -1
- package/dist/bin/affected-actors.js +111 -65
- package/dist/bin/affected-actors.js.map +1 -1
- package/dist/bin/build.d.ts +11 -5
- package/dist/bin/build.d.ts.map +1 -1
- package/dist/bin/build.js +43 -23
- package/dist/bin/build.js.map +1 -1
- package/dist/bin/diff-changes.d.ts +4 -3
- package/dist/bin/diff-changes.d.ts.map +1 -1
- package/dist/bin/diff-changes.js +4 -4
- package/dist/bin/diff-changes.js.map +1 -1
- package/dist/bin/diff-json-schema.d.ts +2 -2
- package/dist/bin/diff-json-schema.d.ts.map +1 -1
- package/dist/bin/diff-json-schema.js +1 -4
- package/dist/bin/diff-json-schema.js.map +1 -1
- package/dist/bin/git.d.ts +7 -15
- package/dist/bin/git.d.ts.map +1 -1
- package/dist/bin/git.js +23 -61
- package/dist/bin/git.js.map +1 -1
- package/dist/bin/main.js +39 -71
- package/dist/bin/main.js.map +1 -1
- package/dist/bin/slack.d.ts +6 -3
- package/dist/bin/slack.d.ts.map +1 -1
- package/dist/bin/slack.js +13 -1
- package/dist/bin/slack.js.map +1 -1
- package/dist/bin/stdin-input.d.ts +17 -0
- package/dist/bin/stdin-input.d.ts.map +1 -0
- package/dist/bin/stdin-input.js +54 -0
- package/dist/bin/stdin-input.js.map +1 -0
- package/dist/bin/types.d.ts +5 -0
- package/dist/bin/types.d.ts.map +1 -1
- package/dist/test/bundle/cli-bundle.test.js +32 -11
- package/dist/test/bundle/cli-bundle.test.js.map +1 -1
- package/dist/test/e2e/cli-bundle.e2e.test.js +23 -57
- package/dist/test/e2e/cli-bundle.e2e.test.js.map +1 -1
- package/dist/test/e2e/git-build-release.e2e.test.d.ts +2 -0
- package/dist/test/e2e/git-build-release.e2e.test.d.ts.map +1 -0
- package/dist/test/e2e/git-build-release.e2e.test.js +87 -0
- package/dist/test/e2e/git-build-release.e2e.test.js.map +1 -0
- package/dist/test/e2e/helpers.d.ts +37 -0
- package/dist/test/e2e/helpers.d.ts.map +1 -0
- package/dist/test/e2e/helpers.js +57 -0
- package/dist/test/e2e/helpers.js.map +1 -0
- package/dist/test/unit/bin/affected-actors-cli.test.d.ts +2 -0
- package/dist/test/unit/bin/affected-actors-cli.test.d.ts.map +1 -0
- package/dist/test/unit/bin/affected-actors-cli.test.js +161 -0
- package/dist/test/unit/bin/affected-actors-cli.test.js.map +1 -0
- package/dist/test/unit/bin/affected-actors.test.js +68 -87
- package/dist/test/unit/bin/affected-actors.test.js.map +1 -1
- package/dist/test/unit/bin/build-cli.test.d.ts +2 -0
- package/dist/test/unit/bin/build-cli.test.d.ts.map +1 -0
- package/dist/test/unit/bin/build-cli.test.js +44 -0
- package/dist/test/unit/bin/build-cli.test.js.map +1 -0
- package/dist/test/unit/bin/build.test.js +61 -4
- package/dist/test/unit/bin/build.test.js.map +1 -1
- package/dist/test/unit/bin/compiled-cli.d.ts +6 -0
- package/dist/test/unit/bin/compiled-cli.d.ts.map +1 -0
- package/dist/test/unit/bin/compiled-cli.js +16 -0
- package/dist/test/unit/bin/compiled-cli.js.map +1 -0
- package/dist/test/unit/bin/diff-changes.test.js +55 -55
- package/dist/test/unit/bin/diff-changes.test.js.map +1 -1
- package/dist/test/unit/bin/diff.test.js +25 -28
- package/dist/test/unit/bin/diff.test.js.map +1 -1
- package/dist/test/unit/bin/git.test.js +28 -108
- package/dist/test/unit/bin/git.test.js.map +1 -1
- package/dist/test/unit/bin/slack.test.js +19 -2
- 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 +18 -21
- 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 +47 -11
- package/test/e2e/cli-bundle.e2e.test.ts +47 -77
- package/test/e2e/git-build-release.e2e.test.ts +113 -0
- package/test/e2e/helpers.ts +82 -0
- package/test/unit/bin/affected-actors-cli.test.ts +193 -0
- package/test/unit/bin/affected-actors.test.ts +86 -94
- package/test/unit/bin/build-cli.test.ts +55 -0
- package/test/unit/bin/build.test.ts +71 -3
- package/test/unit/bin/compiled-cli.ts +17 -0
- package/test/unit/bin/diff-changes.test.ts +55 -55
- package/test/unit/bin/diff.test.ts +25 -28
- package/test/unit/bin/git.test.ts +35 -132
- package/test/unit/bin/slack.test.ts +21 -2
- package/test/unit/bin/stdin-input.test.ts +61 -0
- package/test/unit/should-built-and-test.test.ts +19 -22
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,7 @@ describe('CLI bundle', () => {
|
|
|
91
98
|
'get-affected-actors',
|
|
92
99
|
'report-tests',
|
|
93
100
|
'build',
|
|
94
|
-
'release',
|
|
101
|
+
'notify-release',
|
|
95
102
|
'build-from-local',
|
|
96
103
|
'delete-old-builds',
|
|
97
104
|
]) {
|
|
@@ -143,27 +150,56 @@ describe('CLI bundle', () => {
|
|
|
143
150
|
});
|
|
144
151
|
|
|
145
152
|
it('build --dry-run with nothing to build needs no token', () => {
|
|
146
|
-
const { status, stdout, stderr } =
|
|
153
|
+
const { status, stdout, stderr } = runCliWithInput(
|
|
154
|
+
JSON.stringify({ actors: [] }),
|
|
155
|
+
'build',
|
|
156
|
+
'--version-number',
|
|
157
|
+
'0.99',
|
|
158
|
+
'--dry-run',
|
|
159
|
+
);
|
|
147
160
|
expect(status, stderr).toBe(0);
|
|
148
161
|
expect(JSON.parse(stdout)).toEqual({ dryRun: true, builds: [] });
|
|
149
162
|
});
|
|
150
163
|
|
|
151
164
|
it('build --dry-run fails cleanly when the Actor token is missing', () => {
|
|
152
|
-
const { status, stderr } =
|
|
165
|
+
const { status, stderr } = runCliWithInput(
|
|
166
|
+
JSON.stringify({ actors: [{ actorFullName: 'tester/foo' }] }),
|
|
167
|
+
'build',
|
|
168
|
+
'--version-number',
|
|
169
|
+
'0.99',
|
|
170
|
+
'--dry-run',
|
|
171
|
+
);
|
|
153
172
|
expect(status).toBe(1);
|
|
154
173
|
expect(stderr).toContain('Env var FOO_TOKEN is not set (needed for actor "tester/foo")');
|
|
155
174
|
});
|
|
156
175
|
|
|
157
|
-
it('
|
|
158
|
-
const { status, stderr } = runCli('
|
|
176
|
+
it('build fails on empty input instead of building nothing', () => {
|
|
177
|
+
const { status, stderr } = runCli('build', '--version-number', '0.99', '--dry-run');
|
|
178
|
+
expect(status).toBe(1);
|
|
179
|
+
expect(stderr).toContain('got nothing');
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
it('get-affected-actors --since HEAD reports nothing', () => {
|
|
183
|
+
const { status, stdout, stderr } = runCli('get-affected-actors', '--since', headSha);
|
|
184
|
+
expect(status, stderr).toBe(0);
|
|
185
|
+
expect(JSON.parse(stdout)).toMatchObject({
|
|
186
|
+
commits: [],
|
|
187
|
+
functionallyChangedActors: [],
|
|
188
|
+
cosmeticallyChangedActors: [],
|
|
189
|
+
});
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
it('notify-release announces nothing, and needs no token, when there are no new commits', () => {
|
|
193
|
+
const affected = runCli('get-affected-actors', '--since', headSha).stdout;
|
|
194
|
+
const { status, stderr } = runCliWithInput(affected, 'notify-release', '--release-slack-channel', '#releases');
|
|
159
195
|
expect(status, stderr).toBe(0);
|
|
160
|
-
expect(stderr).toContain(
|
|
196
|
+
expect(stderr).toContain('No new commits, so there is no release to announce');
|
|
161
197
|
});
|
|
162
198
|
|
|
163
|
-
it('release
|
|
164
|
-
const
|
|
199
|
+
it('notify-release skips Slack without a channel', () => {
|
|
200
|
+
const affected = runCli('get-affected-actors', '--since', baseSha).stdout;
|
|
201
|
+
const { status, stderr } = runCliWithInput(affected, 'notify-release', '--dry-run');
|
|
165
202
|
expect(status, stderr).toBe(0);
|
|
166
|
-
expect(stderr).toContain('{"dryRun":true,"builds":[]}');
|
|
167
203
|
expect(stderr).toContain('Skipping slack notification');
|
|
168
204
|
});
|
|
169
205
|
|
|
@@ -1,48 +1,37 @@
|
|
|
1
|
-
import { execFileSync
|
|
1
|
+
import { execFileSync } from 'node:child_process';
|
|
2
2
|
import { mkdir, mkdtemp, readFile, rm, symlink, writeFile } from 'node:fs/promises';
|
|
3
3
|
import os from 'node:os';
|
|
4
4
|
import path from 'node:path';
|
|
5
5
|
import process from 'node:process';
|
|
6
|
-
import { fileURLToPath } from 'node:url';
|
|
7
6
|
|
|
8
7
|
import { ApifyClient } from 'apify-client';
|
|
9
8
|
import { afterAll, beforeAll, describe, expect, it } from 'vitest';
|
|
10
9
|
|
|
10
|
+
import type { ActorBuild } from './helpers.js';
|
|
11
|
+
import {
|
|
12
|
+
cliEnv,
|
|
13
|
+
gitEnv,
|
|
14
|
+
projectRoot,
|
|
15
|
+
runCli as runCliIn,
|
|
16
|
+
runCliWithInput as runCliWithInputIn,
|
|
17
|
+
runProcess,
|
|
18
|
+
token,
|
|
19
|
+
TOKEN_ENV_VAR,
|
|
20
|
+
} from './helpers.js';
|
|
21
|
+
|
|
11
22
|
// End-to-end run of the shipped CLI bundle against two dummy Actors in an isolated sandbox account:
|
|
12
23
|
// dry-run plans read the real Actors, `build-from-local` really builds them, the lib runs them
|
|
13
24
|
// through `testActor` on exactly those builds (the ACTOR_BUILDS handoff), `report-tests` reads the
|
|
14
|
-
// result, and `delete-old-builds` cleans up. Git builds
|
|
15
|
-
//
|
|
25
|
+
// result, and `delete-old-builds` cleans up. Git builds of a real repository are in
|
|
26
|
+
// git-build-release.e2e.test.ts.
|
|
16
27
|
//
|
|
17
28
|
// Runs with `npm run test:e2e`, kept out of `npm test`. Skipped without the token, e.g. on fork PRs.
|
|
18
|
-
const TOKEN_ENV_VAR = 'E2E_TEST_APIFY_TOKEN_DBYPODOIHUZABNQTR';
|
|
19
|
-
const token = process.env[TOKEN_ENV_VAR];
|
|
20
|
-
|
|
21
29
|
const ACTORS = ['test-tools-testing-sandbox/dummy-for-testing-1', 'test-tools-testing-sandbox/dummy-for-testing-2'];
|
|
22
30
|
// Only used as the source URL in dry-run plans; nothing is cloned from it.
|
|
23
31
|
const REPO_URL = 'https://github.com/apify/apify-test-tools-e2e-fixture';
|
|
24
32
|
|
|
25
|
-
const projectRoot = fileURLToPath(new URL('../../', import.meta.url));
|
|
26
|
-
const bundlePath = path.join(projectRoot, '.github/cli/apify-test-tools.mjs');
|
|
27
33
|
const vitestPath = path.join(projectRoot, 'node_modules/vitest/vitest.mjs');
|
|
28
34
|
|
|
29
|
-
// Proxy and CA settings pass through so the API is reachable behind a proxy; no other variable does.
|
|
30
|
-
const networkEnv = Object.fromEntries(
|
|
31
|
-
['HTTPS_PROXY', 'https_proxy', 'HTTP_PROXY', 'http_proxy', 'NO_PROXY', 'no_proxy', 'NODE_EXTRA_CA_CERTS']
|
|
32
|
-
.filter((name) => process.env[name] !== undefined)
|
|
33
|
-
.map((name) => [name, process.env[name]!]),
|
|
34
|
-
);
|
|
35
|
-
const baseEnv = { PATH: process.env.PATH ?? '', HOME: os.tmpdir() };
|
|
36
|
-
// The CLI gets only the sandbox token, under the name the fixture config points at.
|
|
37
|
-
const cliEnv = { ...baseEnv, ...networkEnv, [TOKEN_ENV_VAR]: token ?? '' };
|
|
38
|
-
const gitEnv = {
|
|
39
|
-
...baseEnv,
|
|
40
|
-
GIT_AUTHOR_NAME: 'Tester',
|
|
41
|
-
GIT_AUTHOR_EMAIL: 'tester@example.com',
|
|
42
|
-
GIT_COMMITTER_NAME: 'Tester',
|
|
43
|
-
GIT_COMMITTER_EMAIL: 'tester@example.com',
|
|
44
|
-
};
|
|
45
|
-
|
|
46
35
|
let repoDir: string;
|
|
47
36
|
let baseSha: string;
|
|
48
37
|
|
|
@@ -53,27 +42,8 @@ const writeRepoFile = async (file: string, content: string) => {
|
|
|
53
42
|
await writeFile(path.join(repoDir, file), content);
|
|
54
43
|
};
|
|
55
44
|
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
const PROCESS_TIMEOUT_MS = 8 * 60_000;
|
|
59
|
-
|
|
60
|
-
const runCli = (...args: string[]) => {
|
|
61
|
-
const result = spawnSync(process.execPath, [bundlePath, ...args], {
|
|
62
|
-
cwd: repoDir,
|
|
63
|
-
env: cliEnv,
|
|
64
|
-
encoding: 'utf8',
|
|
65
|
-
timeout: PROCESS_TIMEOUT_MS,
|
|
66
|
-
});
|
|
67
|
-
const error = result.error ? `\n${result.error.message}` : '';
|
|
68
|
-
return { status: result.status, stdout: result.stdout, stderr: result.stderr + error };
|
|
69
|
-
};
|
|
70
|
-
|
|
71
|
-
interface ActorBuild {
|
|
72
|
-
buildId: string;
|
|
73
|
-
actorRawId: string;
|
|
74
|
-
buildNumber: string;
|
|
75
|
-
actorFullName: string;
|
|
76
|
-
}
|
|
45
|
+
const runCli = async (...args: string[]) => runCliIn(repoDir, ...args);
|
|
46
|
+
const runCliWithInput = async (input: string, ...args: string[]) => runCliWithInputIn(repoDir, input, ...args);
|
|
77
47
|
|
|
78
48
|
describe.runIf(token)('CLI bundle against real Actors', { timeout: 2 * 60_000 }, () => {
|
|
79
49
|
let builds: ActorBuild[];
|
|
@@ -117,13 +87,12 @@ describe.runIf(token)('CLI bundle against real Actors', { timeout: 2 * 60_000 },
|
|
|
117
87
|
if (repoDir) await rm(repoDir, { recursive: true, force: true });
|
|
118
88
|
});
|
|
119
89
|
|
|
120
|
-
it('build --dry-run plans a test build of the
|
|
121
|
-
const { status, stdout, stderr } =
|
|
90
|
+
it('build --dry-run plans a test build of the Actors it is given', async () => {
|
|
91
|
+
const { status, stdout, stderr } = await runCliWithInput(
|
|
92
|
+
JSON.stringify({ actors: [{ actorFullName: ACTORS[0] }] }),
|
|
122
93
|
'build',
|
|
123
|
-
'--
|
|
124
|
-
'
|
|
125
|
-
'--target-branch',
|
|
126
|
-
'main',
|
|
94
|
+
'--version-number',
|
|
95
|
+
'0.99',
|
|
127
96
|
'--repo-url',
|
|
128
97
|
REPO_URL,
|
|
129
98
|
'--dry-run',
|
|
@@ -132,27 +101,33 @@ describe.runIf(token)('CLI bundle against real Actors', { timeout: 2 * 60_000 },
|
|
|
132
101
|
expect(JSON.parse(stdout)).toEqual({
|
|
133
102
|
dryRun: true,
|
|
134
103
|
builds: [
|
|
135
|
-
{
|
|
136
|
-
actorFullName: ACTORS[0],
|
|
137
|
-
versionNumber: '0.99',
|
|
138
|
-
gitRepoUrl: `${REPO_URL}#feature:actors/dummy-1`,
|
|
139
|
-
useDockerCache: false,
|
|
140
|
-
},
|
|
104
|
+
{ actorFullName: ACTORS[0], versionNumber: '0.99', gitRepoUrl: `${REPO_URL}#feature:actors/dummy-1` },
|
|
141
105
|
],
|
|
142
106
|
});
|
|
143
107
|
});
|
|
144
108
|
|
|
145
|
-
it('release
|
|
146
|
-
const
|
|
109
|
+
it('a release pipeline plans a build of the default version of every changed Actor', async () => {
|
|
110
|
+
const affected = await runCli('get-affected-actors', '--since', baseSha);
|
|
111
|
+
expect(affected.status, affected.stderr).toBe(0);
|
|
112
|
+
const { functionallyChangedActors, cosmeticallyChangedActors } = JSON.parse(affected.stdout);
|
|
113
|
+
|
|
114
|
+
const { status, stderr } = await runCliWithInput(
|
|
115
|
+
JSON.stringify({ actors: [...functionallyChangedActors, ...cosmeticallyChangedActors] }),
|
|
116
|
+
'build',
|
|
117
|
+
'--version-number',
|
|
118
|
+
'default',
|
|
119
|
+
'--repo-url',
|
|
120
|
+
REPO_URL,
|
|
121
|
+
'--dry-run',
|
|
122
|
+
);
|
|
147
123
|
expect(status, stderr).toBe(0);
|
|
148
124
|
expect(stderr).toContain(
|
|
149
125
|
`[DRY RUN] Would build:\n ${ACTORS[0]} (0.0) from ${REPO_URL}#feature:actors/dummy-1`,
|
|
150
126
|
);
|
|
151
|
-
expect(stderr).toContain('Skipping slack notification');
|
|
152
127
|
});
|
|
153
128
|
|
|
154
|
-
it('build-from-local --dry-run plans both Actors from the working tree', () => {
|
|
155
|
-
const { status, stdout, stderr } = runCli('build-from-local', '--dry-run');
|
|
129
|
+
it('build-from-local --dry-run plans both Actors from the working tree', async () => {
|
|
130
|
+
const { status, stdout, stderr } = await runCli('build-from-local', '--dry-run');
|
|
156
131
|
expect(status, stderr).toBe(0);
|
|
157
132
|
expect(JSON.parse(stdout)).toMatchObject({
|
|
158
133
|
dryRun: true,
|
|
@@ -160,8 +135,8 @@ describe.runIf(token)('CLI bundle against real Actors', { timeout: 2 * 60_000 },
|
|
|
160
135
|
});
|
|
161
136
|
});
|
|
162
137
|
|
|
163
|
-
it('build-from-local builds both Actors', { timeout: 10 * 60_000 }, () => {
|
|
164
|
-
const { status, stdout, stderr } = runCli('build-from-local');
|
|
138
|
+
it('build-from-local builds both Actors', { timeout: 10 * 60_000 }, async () => {
|
|
139
|
+
const { status, stdout, stderr } = await runCli('build-from-local');
|
|
165
140
|
expect(status, stderr).toBe(0);
|
|
166
141
|
builds = JSON.parse(stdout);
|
|
167
142
|
expect(builds.map((build) => build.actorFullName)).toEqual(ACTORS);
|
|
@@ -191,20 +166,15 @@ describe('sandbox dummies', () => {${actorTests}
|
|
|
191
166
|
`,
|
|
192
167
|
);
|
|
193
168
|
|
|
194
|
-
const vitest =
|
|
169
|
+
const vitest = await runProcess(
|
|
195
170
|
process.execPath,
|
|
196
171
|
[vitestPath, 'run', 'test/platform', '--reporter=json', '--outputFile=./test-output.json'],
|
|
197
172
|
{
|
|
198
173
|
cwd: repoDir,
|
|
199
|
-
|
|
200
|
-
timeout: PROCESS_TIMEOUT_MS,
|
|
201
|
-
env: { ...cliEnv, TESTER_APIFY_TOKEN: token, ACTOR_BUILDS: JSON.stringify(builds) },
|
|
174
|
+
env: { ...cliEnv, TESTER_APIFY_TOKEN: token!, ACTOR_BUILDS: JSON.stringify(builds) },
|
|
202
175
|
},
|
|
203
176
|
);
|
|
204
|
-
expect(
|
|
205
|
-
vitest.status,
|
|
206
|
-
`${vitest.stdout}\n${vitest.stderr}${vitest.error ? `\n${vitest.error.message}` : ''}`,
|
|
207
|
-
).toBe(0);
|
|
177
|
+
expect(vitest.status, `${vitest.stdout}\n${vitest.stderr}`).toBe(0);
|
|
208
178
|
|
|
209
179
|
// Each run used the build from ACTOR_BUILDS, not the Actor's default build.
|
|
210
180
|
const report = JSON.parse(await readFile(path.join(repoDir, 'test-output.json'), 'utf8'));
|
|
@@ -220,15 +190,15 @@ describe('sandbox dummies', () => {${actorTests}
|
|
|
220
190
|
);
|
|
221
191
|
expect(runBuildIds.sort()).toEqual(builds.map((build) => build.buildId).sort());
|
|
222
192
|
|
|
223
|
-
const { status, stderr } = runCli('report-tests', '--report-file', 'test-output.json', '--dry-run');
|
|
193
|
+
const { status, stderr } = await runCli('report-tests', '--report-file', 'test-output.json', '--dry-run');
|
|
224
194
|
expect(status, stderr).toBe(0);
|
|
225
195
|
expect(stderr).toContain(`PASSED: ${ACTORS.length}, FAILED: 0`);
|
|
226
196
|
expect(stderr).toContain('https://console.apify.com/view/runs/');
|
|
227
197
|
});
|
|
228
198
|
|
|
229
199
|
// Last, so builds from earlier runs of this suite don't pile up in the sandbox.
|
|
230
|
-
it('delete-old-builds keeps the default builds', () => {
|
|
231
|
-
const { status, stderr } = runCli('delete-old-builds');
|
|
200
|
+
it('delete-old-builds keeps the default builds', async () => {
|
|
201
|
+
const { status, stderr } = await runCli('delete-old-builds');
|
|
232
202
|
expect(status, stderr).toBe(0);
|
|
233
203
|
for (const actorFullName of ACTORS) {
|
|
234
204
|
expect(stderr).toContain(`[DELETE OLD BUILDS][${actorFullName}]: Skipping default build`);
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
import { execFileSync } from 'node:child_process';
|
|
2
|
+
import { mkdtemp, rm } from 'node:fs/promises';
|
|
3
|
+
import os from 'node:os';
|
|
4
|
+
import path from 'node:path';
|
|
5
|
+
|
|
6
|
+
import { ApifyClient } from 'apify-client';
|
|
7
|
+
import { afterAll, beforeAll, describe, expect, it } from 'vitest';
|
|
8
|
+
|
|
9
|
+
import type { ActorBuild } from './helpers.js';
|
|
10
|
+
import { gitEnv, runCli as runCliIn, runCliWithInput as runCliWithInputIn, token } from './helpers.js';
|
|
11
|
+
|
|
12
|
+
// Real git builds of a real repository: the shipped CLI bundle builds an Actor from a clone of
|
|
13
|
+
// apify-store/testing-repo-for-github-actions, and the Apify builder clones the same repository
|
|
14
|
+
// (over SSH, with the sandbox org's deploy key installed on it).
|
|
15
|
+
//
|
|
16
|
+
// The repo's `e2e/base` and `e2e/feature` branches are fixtures and never change: `e2e/feature`
|
|
17
|
+
// adds one commit touching only dummy-for-testing-1. Like the reusable workflows, the commands run
|
|
18
|
+
// with `e2e/feature` checked out and compare it with `origin/e2e/base`. The release pipeline rebuilds
|
|
19
|
+
// dummy-for-testing-1 under `latest` on every run, which is fine for a sandbox Actor nobody else uses.
|
|
20
|
+
//
|
|
21
|
+
// Runs with `npm run test:e2e`, kept out of `npm test`. Skipped without the token, e.g. on fork PRs.
|
|
22
|
+
const FIXTURE_REPO = 'https://github.com/apify-store/testing-repo-for-github-actions';
|
|
23
|
+
const BASE_BRANCH = 'e2e/base';
|
|
24
|
+
const FEATURE_BRANCH = 'e2e/feature';
|
|
25
|
+
const CHANGED_ACTOR = 'test-tools-testing-sandbox/dummy-for-testing-1';
|
|
26
|
+
const CHANGED_ACTOR_FOLDER = 'actors/dummy-for-testing-1';
|
|
27
|
+
// The CLI builds from the SSH form of the origin URL, which is what lets a deploy key work.
|
|
28
|
+
const BUILD_SOURCE = `git@github.com:apify-store/testing-repo-for-github-actions#${FEATURE_BRANCH}:${CHANGED_ACTOR_FOLDER}`;
|
|
29
|
+
|
|
30
|
+
let repoDir: string;
|
|
31
|
+
|
|
32
|
+
const git = (...args: string[]) => execFileSync('git', args, { cwd: repoDir, env: gitEnv, encoding: 'utf8' }).trim();
|
|
33
|
+
const runCli = async (...args: string[]) => runCliIn(repoDir, ...args);
|
|
34
|
+
const runCliWithInput = async (input: string, ...args: string[]) => runCliWithInputIn(repoDir, input, ...args);
|
|
35
|
+
|
|
36
|
+
const getAffectedActors = async () => {
|
|
37
|
+
const { status, stdout, stderr } = await runCli('get-affected-actors', '--base', `origin/${BASE_BRANCH}`);
|
|
38
|
+
expect(status, stderr).toBe(0);
|
|
39
|
+
return JSON.parse(stdout);
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
describe.runIf(token)('CLI bundle building a real repository', { timeout: 2 * 60_000 }, () => {
|
|
43
|
+
const client = new ApifyClient({ token });
|
|
44
|
+
|
|
45
|
+
const versionSource = async (versionNumber: string) => {
|
|
46
|
+
const actor = await client.actor(CHANGED_ACTOR).get();
|
|
47
|
+
const version = actor?.versions.find(({ versionNumber: number }) => number === versionNumber);
|
|
48
|
+
return version && 'gitRepoUrl' in version ? version.gitRepoUrl : undefined;
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
// A full clone checked out on the feature branch, like actions/checkout with fetch-depth: 0.
|
|
52
|
+
beforeAll(async () => {
|
|
53
|
+
repoDir = await mkdtemp(path.join(os.tmpdir(), 'apify-cli-e2e-git-'));
|
|
54
|
+
execFileSync('git', ['clone', '-q', FIXTURE_REPO, repoDir], { env: gitEnv });
|
|
55
|
+
git('checkout', '-q', '-b', FEATURE_BRANCH, `origin/${FEATURE_BRANCH}`);
|
|
56
|
+
}, 5 * 60_000);
|
|
57
|
+
|
|
58
|
+
afterAll(async () => {
|
|
59
|
+
if (repoDir) await rm(repoDir, { recursive: true, force: true });
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
it('get-affected-actors finds only the Actor the feature branch changed', async () => {
|
|
63
|
+
expect(await getAffectedActors()).toMatchObject({
|
|
64
|
+
functionallyChangedActors: [{ actorFullName: CHANGED_ACTOR }],
|
|
65
|
+
cosmeticallyChangedActors: [],
|
|
66
|
+
});
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
it('build builds the changed Actor from the feature branch', { timeout: 10 * 60_000 }, async () => {
|
|
70
|
+
// What the PR workflow does: the functionally changed Actors, piped into a test build
|
|
71
|
+
const { functionallyChangedActors } = await getAffectedActors();
|
|
72
|
+
const { status, stdout, stderr } = await runCliWithInput(
|
|
73
|
+
JSON.stringify({ actors: functionallyChangedActors }),
|
|
74
|
+
'build',
|
|
75
|
+
'--version-number',
|
|
76
|
+
'0.99',
|
|
77
|
+
);
|
|
78
|
+
expect(status, stderr).toBe(0);
|
|
79
|
+
const builds: ActorBuild[] = JSON.parse(stdout);
|
|
80
|
+
expect(builds).toMatchObject([
|
|
81
|
+
{ actorFullName: CHANGED_ACTOR, buildNumber: expect.stringMatching(/^0\.99\.\d+$/) },
|
|
82
|
+
]);
|
|
83
|
+
|
|
84
|
+
// The version builds the checked-out branch, as it is on the remote.
|
|
85
|
+
expect(await versionSource('0.99')).toBe(BUILD_SOURCE);
|
|
86
|
+
expect((await client.build(builds[0].buildId).get())?.status).toBe('SUCCEEDED');
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
it('the release pipeline rebuilds the default version of the changed Actor', { timeout: 10 * 60_000 }, async () => {
|
|
90
|
+
// What the release workflow does: every Actor changed since the last release, built into its default version
|
|
91
|
+
const baseSha = git('rev-parse', `origin/${BASE_BRANCH}`);
|
|
92
|
+
const affected = await runCli('get-affected-actors', '--since', baseSha);
|
|
93
|
+
expect(affected.status, affected.stderr).toBe(0);
|
|
94
|
+
const { functionallyChangedActors, cosmeticallyChangedActors } = JSON.parse(affected.stdout);
|
|
95
|
+
|
|
96
|
+
const { status, stderr } = await runCliWithInput(
|
|
97
|
+
JSON.stringify({ actors: [...functionallyChangedActors, ...cosmeticallyChangedActors] }),
|
|
98
|
+
'build',
|
|
99
|
+
'--version-number',
|
|
100
|
+
'default',
|
|
101
|
+
);
|
|
102
|
+
expect(status, stderr).toBe(0);
|
|
103
|
+
expect(stderr).toMatch(new RegExp(`\\[${CHANGED_ACTOR}\\]: 0\\.0\\.\\d+`));
|
|
104
|
+
|
|
105
|
+
// Releasing builds the default version from the branch being released.
|
|
106
|
+
expect(await versionSource('0.0')).toBe(BUILD_SOURCE);
|
|
107
|
+
|
|
108
|
+
// Without a channel the announcement is skipped, so it needs no Slack token
|
|
109
|
+
const notify = await runCliWithInput(affected.stdout, 'notify-release');
|
|
110
|
+
expect(notify.status, notify.stderr).toBe(0);
|
|
111
|
+
expect(notify.stderr).toContain('Skipping slack notification');
|
|
112
|
+
});
|
|
113
|
+
});
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import { execFile, type ExecFileException } from 'node:child_process';
|
|
2
|
+
import os from 'node:os';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import process from 'node:process';
|
|
5
|
+
import { fileURLToPath } from 'node:url';
|
|
6
|
+
import { promisify } from 'node:util';
|
|
7
|
+
|
|
8
|
+
// Shared by the e2e suites. They run the shipped CLI bundle against Actors in the isolated
|
|
9
|
+
// `test-tools-testing-sandbox` org, whose token can reach nothing else.
|
|
10
|
+
export const TOKEN_ENV_VAR = 'E2E_TEST_APIFY_TOKEN_DBYPODOIHUZABNQTR';
|
|
11
|
+
export const token = process.env[TOKEN_ENV_VAR];
|
|
12
|
+
|
|
13
|
+
export const projectRoot = fileURLToPath(new URL('../../', import.meta.url));
|
|
14
|
+
export const bundlePath = path.join(projectRoot, '.github/cli/apify-test-tools.mjs');
|
|
15
|
+
|
|
16
|
+
// Proxy and CA settings pass through so the API is reachable behind a proxy; no other variable does.
|
|
17
|
+
const networkEnv = Object.fromEntries(
|
|
18
|
+
['HTTPS_PROXY', 'https_proxy', 'HTTP_PROXY', 'http_proxy', 'NO_PROXY', 'no_proxy', 'NODE_EXTRA_CA_CERTS']
|
|
19
|
+
.filter((name) => process.env[name] !== undefined)
|
|
20
|
+
.map((name) => [name, process.env[name]!]),
|
|
21
|
+
);
|
|
22
|
+
const baseEnv = { PATH: process.env.PATH ?? '', HOME: os.tmpdir() };
|
|
23
|
+
// The CLI gets only the sandbox token, under the name the fixture configs point at.
|
|
24
|
+
export const cliEnv = { ...baseEnv, ...networkEnv, [TOKEN_ENV_VAR]: token ?? '' };
|
|
25
|
+
export const gitEnv = {
|
|
26
|
+
...baseEnv,
|
|
27
|
+
...networkEnv,
|
|
28
|
+
GIT_AUTHOR_NAME: 'Tester',
|
|
29
|
+
GIT_AUTHOR_EMAIL: 'tester@example.com',
|
|
30
|
+
GIT_COMMITTER_NAME: 'Tester',
|
|
31
|
+
GIT_COMMITTER_EMAIL: 'tester@example.com',
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
// Processes are spawned asynchronously: a blocking spawn would stall the test worker for the length
|
|
35
|
+
// of a build, long enough for vitest's own worker RPC to time out. The kill timeout still applies on
|
|
36
|
+
// top of vitest's, since apify-client retries an unreachable API for minutes.
|
|
37
|
+
export const PROCESS_TIMEOUT_MS = 8 * 60_000;
|
|
38
|
+
const execFileAsync = promisify(execFile);
|
|
39
|
+
|
|
40
|
+
export interface ProcessResult {
|
|
41
|
+
status: number | null;
|
|
42
|
+
stdout: string;
|
|
43
|
+
stderr: string;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export const runProcess = async (
|
|
47
|
+
command: string,
|
|
48
|
+
args: string[],
|
|
49
|
+
{ input = '', ...options }: { cwd: string; env: Record<string, string>; input?: string },
|
|
50
|
+
): Promise<ProcessResult> => {
|
|
51
|
+
try {
|
|
52
|
+
const promise = execFileAsync(command, args, {
|
|
53
|
+
...options,
|
|
54
|
+
encoding: 'utf8',
|
|
55
|
+
timeout: PROCESS_TIMEOUT_MS,
|
|
56
|
+
maxBuffer: 100 * 1024 * 1024,
|
|
57
|
+
});
|
|
58
|
+
// Closing stdin hands a command that reads it (e.g. `build`) the input, and one that doesn't never waits
|
|
59
|
+
promise.child.stdin?.end(input);
|
|
60
|
+
const { stdout, stderr } = await promise;
|
|
61
|
+
return { status: 0, stdout, stderr };
|
|
62
|
+
} catch (error) {
|
|
63
|
+
// A non-zero exit rejects too, with the output on the error.
|
|
64
|
+
const { code, stdout = '', stderr = '', message } = error as ExecFileException;
|
|
65
|
+
// Without an exit code (killed by the timeout, or never started) the message says why.
|
|
66
|
+
return typeof code === 'number'
|
|
67
|
+
? { status: code, stdout, stderr }
|
|
68
|
+
: { status: null, stdout, stderr: `${stderr}\n${message}` };
|
|
69
|
+
}
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
export const runCliWithInput = async (cwd: string, input: string, ...args: string[]) =>
|
|
73
|
+
runProcess(process.execPath, [bundlePath, ...args], { cwd, env: cliEnv, input });
|
|
74
|
+
|
|
75
|
+
export const runCli = async (cwd: string, ...args: string[]) => runCliWithInput(cwd, '', ...args);
|
|
76
|
+
|
|
77
|
+
export interface ActorBuild {
|
|
78
|
+
buildId: string;
|
|
79
|
+
actorRawId: string;
|
|
80
|
+
buildNumber: string;
|
|
81
|
+
actorFullName: string;
|
|
82
|
+
}
|