apify-test-tools 0.9.1-beta.11 → 0.9.1-beta.13

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 (48) hide show
  1. package/.github/workflows/public_push-build-latest.yaml +3 -1
  2. package/.github/workflows-package-version +1 -1
  3. package/CHANGELOG.md +1 -0
  4. package/CONTRIBUTING.md +2 -4
  5. package/README.md +68 -8
  6. package/bin/build-from-local.ts +2 -2
  7. package/bin/build.ts +114 -70
  8. package/bin/git.ts +140 -0
  9. package/bin/main.ts +128 -91
  10. package/bin/slack.ts +5 -2
  11. package/bin/types.ts +0 -73
  12. package/bin/utils.ts +2 -3
  13. package/dist/bin/build-from-local.js.map +1 -1
  14. package/dist/bin/build.d.ts +25 -4
  15. package/dist/bin/build.d.ts.map +1 -1
  16. package/dist/bin/build.js +76 -47
  17. package/dist/bin/build.js.map +1 -1
  18. package/dist/bin/git.d.ts +40 -0
  19. package/dist/bin/git.d.ts.map +1 -1
  20. package/dist/bin/git.js +118 -0
  21. package/dist/bin/git.js.map +1 -1
  22. package/dist/bin/main.d.ts +1 -20
  23. package/dist/bin/main.d.ts.map +1 -1
  24. package/dist/bin/main.js +65 -62
  25. package/dist/bin/main.js.map +1 -1
  26. package/dist/bin/slack.d.ts.map +1 -1
  27. package/dist/bin/slack.js +4 -2
  28. package/dist/bin/slack.js.map +1 -1
  29. package/dist/bin/types.d.ts +0 -64
  30. package/dist/bin/types.d.ts.map +1 -1
  31. package/dist/bin/utils.d.ts.map +1 -1
  32. package/dist/bin/utils.js +2 -3
  33. package/dist/bin/utils.js.map +1 -1
  34. package/dist/test/unit/bin/build.test.d.ts +2 -0
  35. package/dist/test/unit/bin/build.test.d.ts.map +1 -0
  36. package/dist/test/unit/bin/build.test.js +46 -0
  37. package/dist/test/unit/bin/build.test.js.map +1 -0
  38. package/dist/test/unit/bin/git.test.js +104 -1
  39. package/dist/test/unit/bin/git.test.js.map +1 -1
  40. package/dist/tsconfig.tsbuildinfo +1 -1
  41. package/package.json +2 -2
  42. package/test/unit/bin/build.test.ts +68 -0
  43. package/test/unit/bin/git.test.ts +122 -0
  44. package/bin/github.ts +0 -69
  45. package/dist/bin/github.d.ts +0 -11
  46. package/dist/bin/github.d.ts.map +0 -1
  47. package/dist/bin/github.js +0 -59
  48. package/dist/bin/github.js.map +0 -1
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "apify-test-tools",
3
- "version": "0.9.1-beta.11",
3
+ "version": "0.9.1-beta.13",
4
4
  "type": "module",
5
5
  "description": "TBD",
6
6
  "repository": {
@@ -47,7 +47,7 @@
47
47
  },
48
48
  "scripts": {
49
49
  "start": "npm run dist/bin/main.js",
50
- "start:dev": "GITHUB_WORKSPACE=local-clone tsx bin/main.ts",
50
+ "start:dev": "tsx bin/main.ts --workspace local-clone",
51
51
  "build": "tsc && chmod +x ./dist/bin/main.js",
52
52
  "lint": "eslint",
53
53
  "lint:fix": "eslint --fix",
@@ -0,0 +1,68 @@
1
+ import type * as ApifyClientTypes from 'apify-client';
2
+ import { ActorSourceType } from 'apify-client';
3
+ import { describe, expect, it } from 'vitest';
4
+
5
+ import { assertRepoUrlMatchesDefaultVersion, resolveDefaultVersion } from '../../../bin/build.js';
6
+
7
+ const ACTOR = 'owner/actor';
8
+ const REPO_URL = 'git@github.com:owner/repo';
9
+
10
+ const makeActorInfo = (overrides: Partial<ApifyClientTypes.Actor> = {}): ApifyClientTypes.Actor =>
11
+ ({
12
+ defaultRunOptions: { build: 'latest' },
13
+ taggedBuilds: { latest: { buildNumber: '0.1.5' } },
14
+ versions: [
15
+ { versionNumber: '0.1', sourceType: ActorSourceType.GitRepo, gitRepoUrl: `${REPO_URL}#master:actors/a` },
16
+ ],
17
+ ...overrides,
18
+ }) as unknown as ApifyClientTypes.Actor;
19
+
20
+ describe('resolveDefaultVersion', () => {
21
+ it('should resolve the version the default build tag points to', () => {
22
+ const { defaultBuildTag, defaultBuildNumber, defaultVersionNumber, defaultVersion } = resolveDefaultVersion(
23
+ ACTOR,
24
+ makeActorInfo(),
25
+ );
26
+ expect(defaultBuildTag).toBe('latest');
27
+ expect(defaultBuildNumber).toBe('0.1.5');
28
+ expect(defaultVersionNumber).toBe('0.1');
29
+ expect(defaultVersion?.versionNumber).toBe('0.1');
30
+ });
31
+
32
+ it('should explain the setup requirement when there is no default build', () => {
33
+ expect(() => resolveDefaultVersion(ACTOR, makeActorInfo({ taggedBuilds: {} }))).toThrow(
34
+ 'every Actor needs at least one build under its default build tag',
35
+ );
36
+ });
37
+ });
38
+
39
+ describe('assertRepoUrlMatchesDefaultVersion', () => {
40
+ const { defaultVersion } = resolveDefaultVersion(ACTOR, makeActorInfo());
41
+
42
+ it('should pass when the repositories match in a different URL form', () => {
43
+ expect(() =>
44
+ assertRepoUrlMatchesDefaultVersion(ACTOR, defaultVersion, 'https://github.com/owner/repo.git'),
45
+ ).not.toThrow();
46
+ });
47
+
48
+ it('should throw when the remote points to a different repository', () => {
49
+ expect(() => assertRepoUrlMatchesDefaultVersion(ACTOR, defaultVersion, 'git@github.com:fork/repo')).toThrow(
50
+ 'Repository mismatch',
51
+ );
52
+ });
53
+
54
+ it('should throw when the default version is not built from a Git repository', () => {
55
+ const sourceFilesVersion: ApifyClientTypes.ActorVersion = {
56
+ versionNumber: '0.1',
57
+ sourceType: ActorSourceType.SourceFiles,
58
+ sourceFiles: [],
59
+ };
60
+ expect(() => assertRepoUrlMatchesDefaultVersion(ACTOR, sourceFilesVersion, REPO_URL)).toThrow(
61
+ 'has source type "SOURCE_FILES"',
62
+ );
63
+ });
64
+
65
+ it('should throw when the default version no longer exists', () => {
66
+ expect(() => assertRepoUrlMatchesDefaultVersion(ACTOR, undefined, REPO_URL)).toThrow('no longer exists');
67
+ });
68
+ });
@@ -5,8 +5,13 @@ import {
5
5
  getBranchOnlyChangedFiles,
6
6
  getChangedFiles,
7
7
  getCommits,
8
+ getCurrentBranch,
9
+ getReleaseChanges,
10
+ getRepoName,
8
11
  hasMergeFromTarget,
12
+ normalizeRepoUrl,
9
13
  parseBaseCommit,
14
+ resolveReleaseBaseCommit,
10
15
  } from '../../../bin/git.js';
11
16
  import * as Utils from '../../../bin/utils.js';
12
17
 
@@ -250,3 +255,120 @@ describe('parseBaseCommit', () => {
250
255
  expect(() => parseBaseCommit(badJson)).toThrow('Invalid base commit SHA');
251
256
  });
252
257
  });
258
+
259
+ describe('getCurrentBranch', () => {
260
+ it('should return the checked-out branch', () => {
261
+ vi.spyOn(Utils, 'spawnCommandInGhWorkspace').mockReturnValue('master');
262
+ expect(getCurrentBranch()).toBe('master');
263
+ });
264
+
265
+ it('should throw on a detached HEAD', () => {
266
+ vi.spyOn(Utils, 'spawnCommandInGhWorkspace').mockReturnValue('HEAD');
267
+ expect(() => getCurrentBranch()).toThrow('HEAD is detached');
268
+ });
269
+ });
270
+
271
+ describe('normalizeRepoUrl', () => {
272
+ it.each([
273
+ 'git@github.com:Apify-Store/google-maps.git',
274
+ 'git@github.com:apify-store/google-maps',
275
+ 'https://github.com/apify-store/google-maps',
276
+ 'https://github.com/apify-store/google-maps.git/',
277
+ 'ssh://git@github.com/apify-store/google-maps',
278
+ 'git@github.com:apify-store/google-maps#master:actors/foo',
279
+ 'https://github.com/apify-store/google-maps#master',
280
+ ])('should normalize %s', (url) => {
281
+ expect(normalizeRepoUrl(url)).toBe('github.com/apify-store/google-maps');
282
+ });
283
+
284
+ it('should tell different repositories apart', () => {
285
+ expect(normalizeRepoUrl('git@github.com:my-fork/google-maps.git')).not.toBe(
286
+ normalizeRepoUrl('git@github.com:apify-store/google-maps.git'),
287
+ );
288
+ });
289
+ });
290
+
291
+ describe('getRepoName', () => {
292
+ it('should return the repository name', () => {
293
+ expect(getRepoName('git@github.com:apify-store/google-maps.git')).toBe('google-maps');
294
+ });
295
+ });
296
+
297
+ describe('resolveReleaseBaseCommit', () => {
298
+ const baseSha = 'b'.repeat(40);
299
+
300
+ it('should return the base commit when it is an ancestor of HEAD', () => {
301
+ vi.spyOn(Utils, 'spawnCommandInGhWorkspace').mockImplementation((cmd: string) => {
302
+ if (cmd.startsWith('git rev-parse --verify')) return baseSha;
303
+ if (cmd.startsWith('git merge-base')) return baseSha;
304
+ return '';
305
+ });
306
+ expect(resolveReleaseBaseCommit(baseSha)).toBe(baseSha);
307
+ });
308
+
309
+ it('should throw on the all-zeros SHA of a newly created branch', () => {
310
+ const spy = vi.spyOn(Utils, 'spawnCommandInGhWorkspace');
311
+ expect(() => resolveReleaseBaseCommit('0'.repeat(40))).toThrow('the branch was just created');
312
+ expect(spy).not.toHaveBeenCalled();
313
+ });
314
+
315
+ it('should throw when the base commit is missing from the local history', () => {
316
+ vi.spyOn(Utils, 'spawnCommandInGhWorkspace').mockReturnValue('');
317
+ expect(() => resolveReleaseBaseCommit(baseSha)).toThrow('is not in the local git history');
318
+ });
319
+
320
+ it('should throw when the base commit is not an ancestor of HEAD (force push)', () => {
321
+ vi.spyOn(Utils, 'spawnCommandInGhWorkspace').mockImplementation((cmd: string) => {
322
+ if (cmd.startsWith('git rev-parse --verify')) return baseSha;
323
+ if (cmd.startsWith('git merge-base')) return 'c'.repeat(40);
324
+ return '';
325
+ });
326
+ expect(() => resolveReleaseBaseCommit(baseSha)).toThrow('is not an ancestor of HEAD');
327
+ });
328
+
329
+ it('should throw on an invalid SHA', () => {
330
+ expect(() => resolveReleaseBaseCommit('not-a-sha')).toThrow('Invalid base commit SHA');
331
+ });
332
+ });
333
+
334
+ describe('getReleaseChanges', () => {
335
+ const baseSha = 'b'.repeat(40);
336
+ const headSha = 'd'.repeat(40);
337
+ const mergedBranchCommit = `${'1'.repeat(40)}»¦«Dev<dev@example.com>»¦«Date1»¦«feat: branch change`;
338
+ const mergeCommit = `${headSha}»¦«Dev<dev@example.com>»¦«Date2»¦«Merge pull request #1`;
339
+
340
+ let gitCommandSpy: MockInstance;
341
+
342
+ beforeEach(() => {
343
+ gitCommandSpy = vi.spyOn(Utils, 'spawnCommandInGhWorkspace').mockImplementation((cmd: string) => {
344
+ if (cmd === 'git rev-parse HEAD') return headSha;
345
+ if (cmd.startsWith('git log')) return `${mergeCommit}\n${mergedBranchCommit}`;
346
+ if (cmd.startsWith('git diff --name-only')) return 'actors/foo/src/main.ts\nCHANGELOG.md';
347
+ if (cmd === 'git')
348
+ return 'diff --git a/CHANGELOG.md b/CHANGELOG.md\n--- a/CHANGELOG.md\n+++ b/CHANGELOG.md\n@@ -1 +1,2 @@\n+- Added foo\n # Changelog';
349
+ return '';
350
+ });
351
+ });
352
+
353
+ it('should diff the range directly and return the commits oldest first', () => {
354
+ const changes = getReleaseChanges(baseSha);
355
+
356
+ expect(changes).toStrictEqual({
357
+ commits: [
358
+ { sha: '1'.repeat(40), author: 'Dev<dev@example.com>', date: 'Date1', message: 'feat: branch change' },
359
+ { sha: headSha, author: 'Dev<dev@example.com>', date: 'Date2', message: 'Merge pull request #1' },
360
+ ],
361
+ changedFiles: ['actors/foo/src/main.ts', 'CHANGELOG.md'],
362
+ changelog: '- Added foo',
363
+ });
364
+ // Not from the parent of the oldest commit, which may predate the base commit for merge commits
365
+ expect(gitCommandSpy).toHaveBeenCalledWith(`git diff --name-only ${baseSha} HEAD`);
366
+ expect(gitCommandSpy).toHaveBeenCalledWith(
367
+ `git log --pretty=format:'%H»¦«%aN<%aE>»¦«%aD»¦«%s' ${baseSha}..HEAD`,
368
+ );
369
+ });
370
+
371
+ it('should return null when HEAD is the base commit', () => {
372
+ expect(getReleaseChanges(headSha)).toBeNull();
373
+ });
374
+ });
package/bin/github.ts DELETED
@@ -1,69 +0,0 @@
1
- import fs from 'node:fs/promises';
2
-
3
- import type { Commit, GitHubEventPush } from './types.js';
4
- import { spawnCommandInGhWorkspace } from './utils.js';
5
-
6
- export const getPushData = async (path: string) => {
7
- const event = await loadGitHubEvent(path);
8
- const branch = event.ref.replace('refs/heads/', '');
9
- const {
10
- commits: ghCommits,
11
- repository: { ssh_url: repoUrl, name },
12
- head_commit: { author },
13
- } = event;
14
- const commits: Commit[] = ghCommits.map(({ author: commitAuthor, message, id, timestamp }) => ({
15
- author: commitAuthor.username,
16
- sha: id,
17
- message,
18
- date: timestamp,
19
- }));
20
- const changedFiles = getChangedFiles(event);
21
- const changelog = getChangelogChanges(changedFiles, event);
22
- return {
23
- branch,
24
- commits,
25
- changedFiles,
26
- repoUrl,
27
- changelog,
28
- repository: name,
29
- author: author.name,
30
- };
31
- };
32
-
33
- const loadGitHubEvent = async (path: string): Promise<GitHubEventPush> => {
34
- const pushEvent = JSON.parse((await fs.readFile(path)).toString()) as GitHubEventPush;
35
- return pushEvent;
36
- };
37
-
38
- const getChangedFiles = ({ after, before }: GitHubEventPush): string[] => {
39
- const changedFiles = spawnCommandInGhWorkspace(`git diff --name-only ${after}..${before}`);
40
- return changedFiles.split('\n');
41
- };
42
-
43
- const getChangelogChanges = (changedFiles: string[], event: GitHubEventPush): string | null => {
44
- const changelogPath = 'CHANGELOG.md';
45
- if (!changedFiles.includes(changelogPath)) {
46
- return null;
47
- }
48
- const { after, before } = event;
49
- const diff = spawnCommandInGhWorkspace('git', ['diff', before, after, '--', changelogPath]);
50
-
51
- const added: string[] = [];
52
- let startedChangelog = false;
53
- for (const line of diff.split('\n')) {
54
- // We should already get only files we care about from getLastCommitDiffForFile but better to double check
55
- if (line.startsWith('+++') && line.toLowerCase().includes(changelogPath.toLowerCase())) {
56
- startedChangelog = true;
57
- continue;
58
- }
59
- if (startedChangelog) {
60
- if (line.startsWith('diff')) {
61
- break;
62
- }
63
- if (line.startsWith('+')) {
64
- added.push(line.slice(1).trim());
65
- }
66
- }
67
- }
68
- return added.join('\n').trim();
69
- };
@@ -1,11 +0,0 @@
1
- import type { Commit } from './types.js';
2
- export declare const getPushData: (path: string) => Promise<{
3
- branch: string;
4
- commits: Commit[];
5
- changedFiles: string[];
6
- repoUrl: string;
7
- changelog: string | null;
8
- repository: string;
9
- author: string;
10
- }>;
11
- //# sourceMappingURL=github.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"github.d.ts","sourceRoot":"","sources":["../../bin/github.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,MAAM,EAAmB,MAAM,YAAY,CAAC;AAG1D,eAAO,MAAM,WAAW,GAAU,MAAM,MAAM;;;;;;;;EAyB7C,CAAC"}
@@ -1,59 +0,0 @@
1
- import fs from 'node:fs/promises';
2
- import { spawnCommandInGhWorkspace } from './utils.js';
3
- export const getPushData = async (path) => {
4
- const event = await loadGitHubEvent(path);
5
- const branch = event.ref.replace('refs/heads/', '');
6
- const { commits: ghCommits, repository: { ssh_url: repoUrl, name }, head_commit: { author }, } = event;
7
- const commits = ghCommits.map(({ author: commitAuthor, message, id, timestamp }) => ({
8
- author: commitAuthor.username,
9
- sha: id,
10
- message,
11
- date: timestamp,
12
- }));
13
- const changedFiles = getChangedFiles(event);
14
- const changelog = getChangelogChanges(changedFiles, event);
15
- return {
16
- branch,
17
- commits,
18
- changedFiles,
19
- repoUrl,
20
- changelog,
21
- repository: name,
22
- author: author.name,
23
- };
24
- };
25
- const loadGitHubEvent = async (path) => {
26
- const pushEvent = JSON.parse((await fs.readFile(path)).toString());
27
- return pushEvent;
28
- };
29
- const getChangedFiles = ({ after, before }) => {
30
- const changedFiles = spawnCommandInGhWorkspace(`git diff --name-only ${after}..${before}`);
31
- return changedFiles.split('\n');
32
- };
33
- const getChangelogChanges = (changedFiles, event) => {
34
- const changelogPath = 'CHANGELOG.md';
35
- if (!changedFiles.includes(changelogPath)) {
36
- return null;
37
- }
38
- const { after, before } = event;
39
- const diff = spawnCommandInGhWorkspace('git', ['diff', before, after, '--', changelogPath]);
40
- const added = [];
41
- let startedChangelog = false;
42
- for (const line of diff.split('\n')) {
43
- // We should already get only files we care about from getLastCommitDiffForFile but better to double check
44
- if (line.startsWith('+++') && line.toLowerCase().includes(changelogPath.toLowerCase())) {
45
- startedChangelog = true;
46
- continue;
47
- }
48
- if (startedChangelog) {
49
- if (line.startsWith('diff')) {
50
- break;
51
- }
52
- if (line.startsWith('+')) {
53
- added.push(line.slice(1).trim());
54
- }
55
- }
56
- }
57
- return added.join('\n').trim();
58
- };
59
- //# sourceMappingURL=github.js.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"github.js","sourceRoot":"","sources":["../../bin/github.ts"],"names":[],"mappings":"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"}