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

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 (70) hide show
  1. package/.gitattributes +2 -0
  2. package/.github/actions/checkout-restore-dependencies/action.yaml +13 -29
  3. package/.github/cli/apify-test-tools.mjs +362 -0
  4. package/.github/scripts/run-with-apify-tokens.mjs +2 -2
  5. package/.github/workflows/_check_code.yaml +25 -0
  6. package/.github/workflows/_move_major_tag.yaml +3 -81
  7. package/.github/workflows/_tests.yaml +34 -0
  8. package/.github/workflows/_update_release_metadata.yaml +0 -12
  9. package/.github/workflows/manual_move_major_tag.yaml +3 -3
  10. package/.github/workflows/manual_release_stable.yaml +3 -13
  11. package/.github/workflows/on_master.yaml +9 -5
  12. package/.github/workflows/on_pull_request.yaml +3 -48
  13. package/.github/workflows/public_platform-tests.yaml +1 -1
  14. package/.github/workflows/public_pr-build-test.yaml +2 -2
  15. package/.github/workflows/public_push-build-latest.yaml +2 -2
  16. package/.husky/pre-commit +5 -0
  17. package/.prettierignore +3 -0
  18. package/CHANGELOG.md +4 -0
  19. package/CONTRIBUTING.md +8 -18
  20. package/README.md +39 -44
  21. package/bin/build-from-local.ts +47 -13
  22. package/bin/build.ts +35 -19
  23. package/bin/main.ts +20 -5
  24. package/bin/slack.ts +28 -6
  25. package/bin/test-report.ts +6 -9
  26. package/bin/types.ts +9 -0
  27. package/dist/bin/build-from-local.d.ts +10 -2
  28. package/dist/bin/build-from-local.d.ts.map +1 -1
  29. package/dist/bin/build-from-local.js +29 -11
  30. package/dist/bin/build-from-local.js.map +1 -1
  31. package/dist/bin/build.d.ts +15 -5
  32. package/dist/bin/build.d.ts.map +1 -1
  33. package/dist/bin/build.js +24 -15
  34. package/dist/bin/build.js.map +1 -1
  35. package/dist/bin/main.js +18 -4
  36. package/dist/bin/main.js.map +1 -1
  37. package/dist/bin/slack.d.ts +8 -1
  38. package/dist/bin/slack.d.ts.map +1 -1
  39. package/dist/bin/slack.js +26 -5
  40. package/dist/bin/slack.js.map +1 -1
  41. package/dist/bin/test-report.d.ts.map +1 -1
  42. package/dist/bin/test-report.js +6 -8
  43. package/dist/bin/test-report.js.map +1 -1
  44. package/dist/bin/types.d.ts +8 -0
  45. package/dist/bin/types.d.ts.map +1 -1
  46. package/dist/test/bundle/cli-bundle.test.d.ts +2 -0
  47. package/dist/test/bundle/cli-bundle.test.d.ts.map +1 -0
  48. package/dist/test/bundle/cli-bundle.test.js +162 -0
  49. package/dist/test/bundle/cli-bundle.test.js.map +1 -0
  50. package/dist/test/e2e/cli-bundle.e2e.test.d.ts +2 -0
  51. package/dist/test/e2e/cli-bundle.e2e.test.d.ts.map +1 -0
  52. package/dist/test/e2e/cli-bundle.e2e.test.js +172 -0
  53. package/dist/test/e2e/cli-bundle.e2e.test.js.map +1 -0
  54. package/dist/test/unit/bin/build-from-local.test.js +53 -1
  55. package/dist/test/unit/bin/build-from-local.test.js.map +1 -1
  56. package/dist/test/unit/bin/build.test.js +113 -3
  57. package/dist/test/unit/bin/build.test.js.map +1 -1
  58. package/dist/test/unit/bin/slack.test.d.ts +2 -0
  59. package/dist/test/unit/bin/slack.test.d.ts.map +1 -0
  60. package/dist/test/unit/bin/slack.test.js +54 -0
  61. package/dist/test/unit/bin/slack.test.js.map +1 -0
  62. package/dist/tsconfig.tsbuildinfo +1 -1
  63. package/package.json +13 -3
  64. package/test/bundle/cli-bundle.test.ts +192 -0
  65. package/test/e2e/cli-bundle.e2e.test.ts +237 -0
  66. package/test/unit/bin/build-from-local.test.ts +55 -0
  67. package/test/unit/bin/build.test.ts +135 -3
  68. package/test/unit/bin/slack.test.ts +65 -0
  69. package/.github/scripts/check-package-version-bump.mjs +0 -74
  70. package/.github/workflows-package-version +0 -1
@@ -1,8 +1,25 @@
1
+ import fs from 'node:fs';
2
+ import os from 'node:os';
3
+ import path from 'node:path';
4
+
1
5
  import type * as ApifyClientTypes from 'apify-client';
2
- import { ActorSourceType } from 'apify-client';
3
- import { describe, expect, it } from 'vitest';
6
+ import { ActorSourceType, ApifyClient } from 'apify-client';
7
+ import { afterAll, afterEach, beforeAll, beforeEach, describe, expect, it, vi } from 'vitest';
8
+
9
+ import {
10
+ assertRepoUrlMatchesDefaultVersion,
11
+ deleteOldBuilds,
12
+ resolveDefaultVersion,
13
+ runBuilds,
14
+ } from '../../../bin/build.js';
15
+ import type { ActorConfig } from '../../../bin/types.js';
16
+ import { ExistingDir, ExistingFile, RelativeDir } from '../../../bin/utils/path/repo-relative.js';
4
17
 
5
- import { assertRepoUrlMatchesDefaultVersion, resolveDefaultVersion } from '../../../bin/build.js';
18
+ // Only the client is faked, so the tests exercise the real builder code against a stubbed API
19
+ vi.mock('apify-client', async (importOriginal) => {
20
+ const actual = await importOriginal<typeof ApifyClientTypes>();
21
+ return { ...actual, ApifyClient: vi.fn() };
22
+ });
6
23
 
7
24
  const ACTOR = 'owner/actor';
8
25
  const REPO_URL = 'git@github.com:owner/repo';
@@ -66,3 +83,118 @@ describe('assertRepoUrlMatchesDefaultVersion', () => {
66
83
  expect(() => assertRepoUrlMatchesDefaultVersion(ACTOR, undefined, REPO_URL)).toThrow('no longer exists');
67
84
  });
68
85
  });
86
+
87
+ describe('dry run', () => {
88
+ const TOKEN_ENV_VAR = 'APIFY_TOKEN_OWNER';
89
+ const originalCwd = process.cwd();
90
+ let fixtureRoot: string;
91
+ let actorConfig: ActorConfig;
92
+
93
+ // ExistingDir checks the folder exists, so the config is built inside a throwaway repo root
94
+ beforeAll(() => {
95
+ fixtureRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'apify-build-dry-run-'));
96
+ process.chdir(fixtureRoot);
97
+ fs.mkdirSync('actors/a/.actor', { recursive: true });
98
+ fs.writeFileSync('actors/a/.actor/actor.json', '{}');
99
+ actorConfig = {
100
+ actorFullName: ACTOR,
101
+ folder: new ExistingDir('actors/a'),
102
+ tokenEnvVar: TOKEN_ENV_VAR,
103
+ actorJson: {
104
+ file: new ExistingFile('actors/a/.actor/actor.json'),
105
+ dockerContextDir: new RelativeDir('actors/a'),
106
+ },
107
+ dockerContextDir: new RelativeDir('actors/a'),
108
+ contextPaths: [new RelativeDir('actors/a')],
109
+ };
110
+ });
111
+
112
+ afterAll(() => {
113
+ process.chdir(originalCwd);
114
+ fs.rmSync(fixtureRoot, { recursive: true, force: true });
115
+ });
116
+ const deleteBuild = vi.fn();
117
+ const startBuild = vi.fn();
118
+ const createVersion = vi.fn();
119
+ const buildClient = vi.fn(() => ({ delete: deleteBuild }));
120
+
121
+ beforeEach(() => {
122
+ vi.stubEnv(TOKEN_ENV_VAR, 'token');
123
+ const fakeClient = {
124
+ actor: () => ({
125
+ get: async () => makeActorInfo(),
126
+ // Old enough to be deleted, and not the default build 0.1.5
127
+ builds: () => ({
128
+ list: async () => ({ items: [{ id: 'old-build', buildNumber: '0.0.1', startedAt: new Date(0) }] }),
129
+ }),
130
+ versions: () => ({ create: createVersion }),
131
+ version: () => ({ update: createVersion }),
132
+ build: startBuild,
133
+ }),
134
+ build: buildClient,
135
+ };
136
+ vi.mocked(ApifyClient).mockImplementation(function () {
137
+ return fakeClient;
138
+ } as never);
139
+ });
140
+
141
+ afterEach(() => {
142
+ vi.unstubAllEnvs();
143
+ vi.clearAllMocks();
144
+ });
145
+
146
+ const runDryBuilds = async () =>
147
+ runBuilds({
148
+ actorConfigs: [actorConfig],
149
+ repoUrl: REPO_URL,
150
+ shouldVerifyRepoUrl: true,
151
+ branch: 'feature',
152
+ dryRun: true,
153
+ useDockerCache: false,
154
+ });
155
+
156
+ it('should return a build plan without creating versions or builds', async () => {
157
+ expect(await runDryBuilds()).toEqual({
158
+ dryRun: true,
159
+ builds: [
160
+ {
161
+ actorFullName: ACTOR,
162
+ versionNumber: '0.99',
163
+ gitRepoUrl: `${REPO_URL}#feature:actors/a`,
164
+ useDockerCache: false,
165
+ },
166
+ ],
167
+ });
168
+ expect(createVersion).not.toHaveBeenCalled();
169
+ expect(startBuild).not.toHaveBeenCalled();
170
+ });
171
+
172
+ it('should still require the Apify token', async () => {
173
+ vi.stubEnv(TOKEN_ENV_VAR, '');
174
+ await expect(runDryBuilds()).rejects.toThrow(`Env var ${TOKEN_ENV_VAR} is not set`);
175
+ });
176
+
177
+ it('should still check the repository URL', async () => {
178
+ await expect(
179
+ runBuilds({
180
+ actorConfigs: [actorConfig],
181
+ repoUrl: 'git@github.com:fork/repo',
182
+ shouldVerifyRepoUrl: true,
183
+ branch: 'feature',
184
+ dryRun: true,
185
+ useDockerCache: false,
186
+ }),
187
+ ).rejects.toThrow('Repository mismatch');
188
+ });
189
+
190
+ it('should not delete old builds', async () => {
191
+ await deleteOldBuilds([actorConfig], { dryRun: true });
192
+ expect(deleteBuild).not.toHaveBeenCalled();
193
+ });
194
+
195
+ it('should delete old builds outside of a dry run', async () => {
196
+ await deleteOldBuilds([actorConfig], { dryRun: false });
197
+ expect(buildClient).toHaveBeenCalledWith('old-build');
198
+ expect(deleteBuild).toHaveBeenCalledTimes(1);
199
+ });
200
+ });
@@ -0,0 +1,65 @@
1
+ import { WebClient } from '@slack/web-api';
2
+ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
3
+
4
+ import { notifyToSlack } from '../../../bin/slack.js';
5
+
6
+ vi.mock('@slack/web-api', () => ({ WebClient: vi.fn() }));
7
+
8
+ const TOKEN_ENV_VAR = 'SLACK_TOKEN_RELEASES_BOT';
9
+ const postMessage = vi.fn();
10
+ const authTest = vi.fn();
11
+
12
+ const notify = async (overrides: Partial<Parameters<typeof notifyToSlack>[0]> = {}) =>
13
+ notifyToSlack({
14
+ repository: 'owner/repo',
15
+ changedFiles: ['src/main.ts'],
16
+ changelog: '- Fixed a bug',
17
+ commits: [{ sha: '1', author: 'Test<test@example.com>', date: '', message: 'fix: bug' }],
18
+ author: 'Test<test@example.com>',
19
+ dryRun: true,
20
+ releaseSlackChannel: '#releases',
21
+ reportSlackChannel: '#notif',
22
+ ...overrides,
23
+ });
24
+
25
+ describe('notifyToSlack', () => {
26
+ beforeEach(() => {
27
+ vi.stubEnv(TOKEN_ENV_VAR, 'token');
28
+ vi.mocked(WebClient).mockImplementation(function () {
29
+ return { chat: { postMessage }, auth: { test: authTest } };
30
+ } as never);
31
+ });
32
+
33
+ afterEach(() => {
34
+ vi.unstubAllEnvs();
35
+ vi.clearAllMocks();
36
+ });
37
+
38
+ it('should verify the token but post nothing in a dry run', async () => {
39
+ await notify();
40
+ expect(authTest).toHaveBeenCalledTimes(1);
41
+ expect(postMessage).not.toHaveBeenCalled();
42
+ });
43
+
44
+ it('should still require the token in a dry run', async () => {
45
+ vi.stubEnv(TOKEN_ENV_VAR, '');
46
+ await expect(notify()).rejects.toThrow(`${TOKEN_ENV_VAR} not defined`);
47
+ });
48
+
49
+ it('should fail a dry run when Slack rejects the token', async () => {
50
+ authTest.mockRejectedValueOnce(new Error('An API error occurred: invalid_auth'));
51
+ await expect(notify()).rejects.toThrow(`${TOKEN_ENV_VAR} was rejected by Slack`);
52
+ });
53
+
54
+ it('should not need a token when no channel is passed', async () => {
55
+ vi.stubEnv(TOKEN_ENV_VAR, '');
56
+ await notify({ dryRun: false, releaseSlackChannel: undefined, reportSlackChannel: undefined });
57
+ expect(WebClient).not.toHaveBeenCalled();
58
+ });
59
+
60
+ it('should post to both channels outside of a dry run', async () => {
61
+ await notify({ dryRun: false });
62
+ expect(authTest).not.toHaveBeenCalled();
63
+ expect(postMessage).toHaveBeenCalledTimes(2);
64
+ });
65
+ });
@@ -1,74 +0,0 @@
1
- #!/usr/bin/env node
2
- // Reads changed file paths on stdin (one per line) and fails if a PR changes both a consumer-facing
3
- // workflow and the package code it calls, without repinning in workflows-package-version.
4
- //
5
- // The case this catches: a workflow starts using a CLI feature from the same PR. On merge the tag
6
- // moves, the workflow goes live, and it calls something that is not on npm yet — breaking every
7
- // consumer's CI at once. Pinning the unreleased version instead holds the tag until it publishes.
8
- //
9
- // It deliberately only looks at one PR. A workflow could also start depending on package code that
10
- // landed in an *earlier*, still-unreleased PR, which this will not see — that needs someone to ship
11
- // a breaking CLI change and then sit on it unreleased, and catching it means flagging every workflow
12
- // edit made while any package change is unreleased, which is far more noise than the case is worth.
13
-
14
- const PUBLIC_WORKFLOW = /^\.github\/workflows\/public_.*\.ya?ml$/;
15
- // The composite action is consumer-facing too, and it is what installs the package.
16
- const PUBLIC_ACTION = /^\.github\/actions\//;
17
- // What `npx apify-test-tools ...` runs, and the library the platform tests import.
18
- const PACKAGE_CODE = /^(bin\/|lib\/|index\.ts$)/;
19
- const PIN_FILE = '.github/workflows-package-version';
20
- const OVERRIDE_LABEL = 'no-version-bump-needed';
21
-
22
- const changed = await new Promise((resolve) => {
23
- let buffer = '';
24
- process.stdin.setEncoding('utf8');
25
- process.stdin.on('data', (chunk) => (buffer += chunk));
26
- process.stdin.on('end', () =>
27
- resolve(
28
- buffer
29
- .split('\n')
30
- .map((l) => l.trim())
31
- .filter(Boolean),
32
- ),
33
- );
34
- });
35
-
36
- const publicSurface = changed.filter((f) => PUBLIC_WORKFLOW.test(f) || PUBLIC_ACTION.test(f));
37
- const packageCode = changed.filter((f) => PACKAGE_CODE.test(f));
38
- const versionRepinned = changed.includes(PIN_FILE);
39
-
40
- if (publicSurface.length === 0 || packageCode.length === 0) {
41
- console.log('No consumer-facing workflow and package change in the same PR; nothing to check.');
42
- process.exit(0);
43
- }
44
-
45
- if (versionRepinned) {
46
- console.log(`Both sides changed and ${PIN_FILE} was repinned. The tag will hold until that version ships.`);
47
- process.exit(0);
48
- }
49
-
50
- if (
51
- (process.env.PR_LABELS ?? '')
52
- .split(',')
53
- .map((l) => l.trim())
54
- .includes(OVERRIDE_LABEL)
55
- ) {
56
- console.log(`Both sides changed without a version bump, allowed by the "${OVERRIDE_LABEL}" label.`);
57
- process.exit(0);
58
- }
59
-
60
- console.error(`This PR changes consumer-facing workflows and the package code they call:
61
-
62
- workflows: ${publicSurface.join(', ')}
63
- package: ${packageCode.join(', ')}
64
-
65
- On merge the tag moves and those workflows go live immediately, against whatever is on npm today.
66
- If they rely on anything from the package change in this PR, that is not published yet and every
67
- consumer's CI breaks.
68
-
69
- If they do rely on it, set ${PIN_FILE} to the version that will contain it. The tag is then held
70
- until you cut that release, and moves on its own once it is published.
71
-
72
- If they do not rely on it — the two changes just happen to be in one PR — add the
73
- "${OVERRIDE_LABEL}" label.`);
74
- process.exit(1);
@@ -1 +0,0 @@
1
- 0.11.0