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

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 (275) hide show
  1. package/.gitattributes +1 -0
  2. package/.github/actionlint.yaml +11 -0
  3. package/.github/actions/checkout-restore-dependencies/action.yaml +58 -6
  4. package/.github/cli/apify-test-tools.mjs +135 -142
  5. package/.github/scripts/mask-config-secrets.sh +17 -0
  6. package/.github/scripts/run-with-apify-tokens.mjs +63 -103
  7. package/.github/workflows/_check_code.yaml +5 -21
  8. package/.github/workflows/_move_major_tag.yaml +2 -2
  9. package/.github/workflows/public_platform-tests-claude-investigate-and-fix.yaml +89 -8
  10. package/.github/workflows/public_platform-tests.yaml +68 -30
  11. package/.github/workflows/public_pr-build-test.yaml +77 -38
  12. package/.github/workflows/public_push-build-latest.yaml +49 -11
  13. package/.github/workflows/public_review.yaml +5 -4
  14. package/.github/workflows/public_stale-issues.yaml +51 -0
  15. package/.husky/pre-commit +4 -4
  16. package/.prettierignore +2 -1
  17. package/CHANGELOG.md +22 -0
  18. package/CONTRIBUTING.md +117 -17
  19. package/README.md +221 -595
  20. package/bin/actor-ids.ts +41 -0
  21. package/bin/affected-actors.ts +211 -0
  22. package/bin/build-from-local.ts +19 -1
  23. package/bin/build.ts +105 -39
  24. package/bin/diff-changes.ts +69 -36
  25. package/bin/diff-json-schema.ts +5 -6
  26. package/bin/git.ts +73 -175
  27. package/bin/main.ts +142 -189
  28. package/bin/notify.ts +198 -0
  29. package/bin/run-tests.ts +167 -0
  30. package/bin/slack.ts +16 -76
  31. package/bin/stdin-input.ts +156 -0
  32. package/bin/test-files-glob.ts +126 -0
  33. package/bin/test-report.ts +141 -140
  34. package/bin/types.ts +4 -7
  35. package/bin/utils/config/load-config.ts +20 -2
  36. package/bin/utils/config/parser.ts +40 -8
  37. package/bin/utils/config/structures/grouped.ts +2 -2
  38. package/bin/utils/config/structures/legacy.ts +1 -1
  39. package/dist/bin/actor-ids.d.ts +15 -0
  40. package/dist/bin/actor-ids.d.ts.map +1 -0
  41. package/dist/bin/actor-ids.js +30 -0
  42. package/dist/bin/actor-ids.js.map +1 -0
  43. package/dist/bin/affected-actors.d.ts +55 -0
  44. package/dist/bin/affected-actors.d.ts.map +1 -0
  45. package/dist/bin/affected-actors.js +142 -0
  46. package/dist/bin/affected-actors.js.map +1 -0
  47. package/dist/bin/build-from-local.d.ts +2 -1
  48. package/dist/bin/build-from-local.d.ts.map +1 -1
  49. package/dist/bin/build-from-local.js +6 -2
  50. package/dist/bin/build-from-local.js.map +1 -1
  51. package/dist/bin/build.d.ts +18 -5
  52. package/dist/bin/build.d.ts.map +1 -1
  53. package/dist/bin/build.js +74 -24
  54. package/dist/bin/build.js.map +1 -1
  55. package/dist/bin/diff-changes.d.ts +21 -5
  56. package/dist/bin/diff-changes.d.ts.map +1 -1
  57. package/dist/bin/diff-changes.js +41 -28
  58. package/dist/bin/diff-changes.js.map +1 -1
  59. package/dist/bin/diff-json-schema.d.ts +2 -2
  60. package/dist/bin/diff-json-schema.d.ts.map +1 -1
  61. package/dist/bin/diff-json-schema.js +1 -4
  62. package/dist/bin/diff-json-schema.js.map +1 -1
  63. package/dist/bin/git.d.ts +26 -34
  64. package/dist/bin/git.d.ts.map +1 -1
  65. package/dist/bin/git.js +60 -143
  66. package/dist/bin/git.js.map +1 -1
  67. package/dist/bin/main.js +126 -140
  68. package/dist/bin/main.js.map +1 -1
  69. package/dist/bin/notify.d.ts +35 -0
  70. package/dist/bin/notify.d.ts.map +1 -0
  71. package/dist/bin/notify.js +131 -0
  72. package/dist/bin/notify.js.map +1 -0
  73. package/dist/bin/run-tests.d.ts +21 -0
  74. package/dist/bin/run-tests.d.ts.map +1 -0
  75. package/dist/bin/run-tests.js +138 -0
  76. package/dist/bin/run-tests.js.map +1 -0
  77. package/dist/bin/slack.d.ts +2 -14
  78. package/dist/bin/slack.d.ts.map +1 -1
  79. package/dist/bin/slack.js +17 -46
  80. package/dist/bin/slack.js.map +1 -1
  81. package/dist/bin/stdin-input.d.ts +79 -0
  82. package/dist/bin/stdin-input.d.ts.map +1 -0
  83. package/dist/bin/stdin-input.js +124 -0
  84. package/dist/bin/stdin-input.js.map +1 -0
  85. package/dist/bin/test-files-glob.d.ts +18 -0
  86. package/dist/bin/test-files-glob.d.ts.map +1 -0
  87. package/dist/bin/test-files-glob.js +108 -0
  88. package/dist/bin/test-files-glob.js.map +1 -0
  89. package/dist/bin/test-report.d.ts +69 -8
  90. package/dist/bin/test-report.d.ts.map +1 -1
  91. package/dist/bin/test-report.js +76 -67
  92. package/dist/bin/test-report.js.map +1 -1
  93. package/dist/bin/types.d.ts +4 -7
  94. package/dist/bin/types.d.ts.map +1 -1
  95. package/dist/bin/utils/config/load-config.d.ts +8 -0
  96. package/dist/bin/utils/config/load-config.d.ts.map +1 -1
  97. package/dist/bin/utils/config/load-config.js +14 -1
  98. package/dist/bin/utils/config/load-config.js.map +1 -1
  99. package/dist/bin/utils/config/parser.d.ts +15 -2
  100. package/dist/bin/utils/config/parser.d.ts.map +1 -1
  101. package/dist/bin/utils/config/parser.js +29 -8
  102. package/dist/bin/utils/config/parser.js.map +1 -1
  103. package/dist/bin/utils/config/structures/grouped.js +2 -2
  104. package/dist/bin/utils/config/structures/grouped.js.map +1 -1
  105. package/dist/bin/utils/config/structures/legacy.js +1 -1
  106. package/dist/bin/utils/config/structures/legacy.js.map +1 -1
  107. package/dist/lib/extend-expect.d.ts.map +1 -1
  108. package/dist/lib/extend-expect.js +37 -218
  109. package/dist/lib/extend-expect.js.map +1 -1
  110. package/dist/lib/lib.d.ts +1 -5
  111. package/dist/lib/lib.d.ts.map +1 -1
  112. package/dist/lib/lib.js +19 -31
  113. package/dist/lib/lib.js.map +1 -1
  114. package/dist/lib/run-checks.d.ts +14 -0
  115. package/dist/lib/run-checks.d.ts.map +1 -0
  116. package/dist/lib/run-checks.js +82 -0
  117. package/dist/lib/run-checks.js.map +1 -0
  118. package/dist/lib/run-test-result.d.ts +3 -6
  119. package/dist/lib/run-test-result.d.ts.map +1 -1
  120. package/dist/lib/run-test-result.js +41 -54
  121. package/dist/lib/run-test-result.js.map +1 -1
  122. package/dist/test/bundle/cli-bundle.test.js +76 -56
  123. package/dist/test/bundle/cli-bundle.test.js.map +1 -1
  124. package/dist/test/bundle/token-wrapper.test.d.ts +2 -0
  125. package/dist/test/bundle/token-wrapper.test.d.ts.map +1 -0
  126. package/dist/test/bundle/token-wrapper.test.js +80 -0
  127. package/dist/test/bundle/token-wrapper.test.js.map +1 -0
  128. package/dist/test/e2e/cli-bundle.e2e.test.js +33 -68
  129. package/dist/test/e2e/cli-bundle.e2e.test.js.map +1 -1
  130. package/dist/test/e2e/git-build-release.e2e.test.d.ts +2 -0
  131. package/dist/test/e2e/git-build-release.e2e.test.d.ts.map +1 -0
  132. package/dist/test/e2e/git-build-release.e2e.test.js +87 -0
  133. package/dist/test/e2e/git-build-release.e2e.test.js.map +1 -0
  134. package/dist/test/e2e/helpers.d.ts +37 -0
  135. package/dist/test/e2e/helpers.d.ts.map +1 -0
  136. package/dist/test/e2e/helpers.js +57 -0
  137. package/dist/test/e2e/helpers.js.map +1 -0
  138. package/dist/test/unit/bin/actor-filtering.test.js +7 -0
  139. package/dist/test/unit/bin/actor-filtering.test.js.map +1 -1
  140. package/dist/test/unit/bin/actor-ids.test.d.ts +2 -0
  141. package/dist/test/unit/bin/actor-ids.test.d.ts.map +1 -0
  142. package/dist/test/unit/bin/actor-ids.test.js +40 -0
  143. package/dist/test/unit/bin/actor-ids.test.js.map +1 -0
  144. package/dist/test/unit/bin/affected-actors-cli.test.d.ts +2 -0
  145. package/dist/test/unit/bin/affected-actors-cli.test.d.ts.map +1 -0
  146. package/dist/test/unit/bin/affected-actors-cli.test.js +224 -0
  147. package/dist/test/unit/bin/affected-actors-cli.test.js.map +1 -0
  148. package/dist/test/unit/bin/affected-actors.test.d.ts +2 -0
  149. package/dist/test/unit/bin/affected-actors.test.d.ts.map +1 -0
  150. package/dist/test/unit/bin/affected-actors.test.js +153 -0
  151. package/dist/test/unit/bin/affected-actors.test.js.map +1 -0
  152. package/dist/test/unit/bin/build-cli.test.d.ts +2 -0
  153. package/dist/test/unit/bin/build-cli.test.d.ts.map +1 -0
  154. package/dist/test/unit/bin/build-cli.test.js +49 -0
  155. package/dist/test/unit/bin/build-cli.test.js.map +1 -0
  156. package/dist/test/unit/bin/build-from-local.test.js +12 -0
  157. package/dist/test/unit/bin/build-from-local.test.js.map +1 -1
  158. package/dist/test/unit/bin/build.test.js +116 -6
  159. package/dist/test/unit/bin/build.test.js.map +1 -1
  160. package/dist/test/unit/bin/compiled-cli.d.ts +6 -0
  161. package/dist/test/unit/bin/compiled-cli.d.ts.map +1 -0
  162. package/dist/test/unit/bin/compiled-cli.js +23 -0
  163. package/dist/test/unit/bin/compiled-cli.js.map +1 -0
  164. package/dist/test/unit/bin/diff-changes.test.js +168 -114
  165. package/dist/test/unit/bin/diff-changes.test.js.map +1 -1
  166. package/dist/test/unit/bin/diff.test.js +25 -28
  167. package/dist/test/unit/bin/diff.test.js.map +1 -1
  168. package/dist/test/unit/bin/git.test.js +67 -288
  169. package/dist/test/unit/bin/git.test.js.map +1 -1
  170. package/dist/test/unit/bin/notify.test.d.ts +2 -0
  171. package/dist/test/unit/bin/notify.test.d.ts.map +1 -0
  172. package/dist/test/unit/bin/notify.test.js +222 -0
  173. package/dist/test/unit/bin/notify.test.js.map +1 -0
  174. package/dist/test/unit/bin/stdin-input.test.d.ts +2 -0
  175. package/dist/test/unit/bin/stdin-input.test.d.ts.map +1 -0
  176. package/dist/test/unit/bin/stdin-input.test.js +92 -0
  177. package/dist/test/unit/bin/stdin-input.test.js.map +1 -0
  178. package/dist/test/unit/bin/test-cli.test.d.ts +2 -0
  179. package/dist/test/unit/bin/test-cli.test.d.ts.map +1 -0
  180. package/dist/test/unit/bin/test-cli.test.js +146 -0
  181. package/dist/test/unit/bin/test-cli.test.js.map +1 -0
  182. package/dist/test/unit/bin/test-files-glob.test.d.ts +2 -0
  183. package/dist/test/unit/bin/test-files-glob.test.d.ts.map +1 -0
  184. package/dist/test/unit/bin/test-files-glob.test.js +102 -0
  185. package/dist/test/unit/bin/test-files-glob.test.js.map +1 -0
  186. package/dist/test/unit/bin/test-report.test.d.ts +2 -0
  187. package/dist/test/unit/bin/test-report.test.d.ts.map +1 -0
  188. package/dist/test/unit/bin/test-report.test.js +100 -0
  189. package/dist/test/unit/bin/test-report.test.js.map +1 -0
  190. package/dist/test/unit/bin/utils/config/load-config.test.js +19 -1
  191. package/dist/test/unit/bin/utils/config/load-config.test.js.map +1 -1
  192. package/dist/test/unit/bin/utils/config/parser.test.js +36 -4
  193. package/dist/test/unit/bin/utils/config/parser.test.js.map +1 -1
  194. package/dist/test/unit/bin/utils/config/structures/grouped.test.js +6 -0
  195. package/dist/test/unit/bin/utils/config/structures/grouped.test.js.map +1 -1
  196. package/dist/test/unit/bin/utils/config/structures/legacy.test.js +1 -0
  197. package/dist/test/unit/bin/utils/config/structures/legacy.test.js.map +1 -1
  198. package/dist/test/unit/github-scripts/mask-config-secrets.test.d.ts +2 -0
  199. package/dist/test/unit/github-scripts/mask-config-secrets.test.d.ts.map +1 -0
  200. package/dist/test/unit/github-scripts/mask-config-secrets.test.js +50 -0
  201. package/dist/test/unit/github-scripts/mask-config-secrets.test.js.map +1 -0
  202. package/dist/test/unit/github-scripts/run-with-apify-tokens.test.d.ts +2 -0
  203. package/dist/test/unit/github-scripts/run-with-apify-tokens.test.d.ts.map +1 -0
  204. package/dist/test/unit/github-scripts/run-with-apify-tokens.test.js +102 -0
  205. package/dist/test/unit/github-scripts/run-with-apify-tokens.test.js.map +1 -0
  206. package/dist/test/unit/run-checks.test.d.ts +2 -0
  207. package/dist/test/unit/run-checks.test.d.ts.map +1 -0
  208. package/dist/test/unit/run-checks.test.js +62 -0
  209. package/dist/test/unit/run-checks.test.js.map +1 -0
  210. package/dist/test/unit/run-test-result.test.d.ts +2 -0
  211. package/dist/test/unit/run-test-result.test.d.ts.map +1 -0
  212. package/dist/test/unit/run-test-result.test.js +34 -0
  213. package/dist/test/unit/run-test-result.test.js.map +1 -0
  214. package/dist/test/unit/should-built-and-test.test.js +41 -38
  215. package/dist/test/unit/should-built-and-test.test.js.map +1 -1
  216. package/dist/tsconfig.tsbuildinfo +1 -1
  217. package/dist/workflow-scripts-src/run-with-apify-tokens.d.ts +3 -0
  218. package/dist/workflow-scripts-src/run-with-apify-tokens.d.ts.map +1 -0
  219. package/dist/workflow-scripts-src/run-with-apify-tokens.js +100 -0
  220. package/dist/workflow-scripts-src/run-with-apify-tokens.js.map +1 -0
  221. package/docs/cli.md +469 -0
  222. package/docs/lib.md +276 -0
  223. package/docs/workflows.md +455 -0
  224. package/eslint.config.mjs +10 -1
  225. package/lib/extend-expect.ts +48 -258
  226. package/lib/lib.ts +25 -34
  227. package/lib/run-checks.ts +101 -0
  228. package/lib/run-test-result.ts +45 -55
  229. package/package.json +4 -2
  230. package/platform-tests-investigate-and-fix/skills/assess-impact/SKILL.md +42 -0
  231. package/platform-tests-investigate-and-fix/skills/measure-flakiness/SKILL.md +4 -2
  232. package/test/bundle/cli-bundle.test.ts +97 -67
  233. package/test/bundle/token-wrapper.test.ts +110 -0
  234. package/test/e2e/cli-bundle.e2e.test.ts +60 -93
  235. package/test/e2e/git-build-release.e2e.test.ts +113 -0
  236. package/test/e2e/helpers.ts +82 -0
  237. package/test/unit/bin/actor-filtering.test.ts +8 -0
  238. package/test/unit/bin/actor-ids.test.ts +47 -0
  239. package/test/unit/bin/affected-actors-cli.test.ts +271 -0
  240. package/test/unit/bin/affected-actors.test.ts +221 -0
  241. package/test/unit/bin/build-cli.test.ts +61 -0
  242. package/test/unit/bin/build-from-local.test.ts +16 -0
  243. package/test/unit/bin/build.test.ts +149 -5
  244. package/test/unit/bin/compiled-cli.ts +23 -0
  245. package/test/unit/bin/diff-changes.test.ts +181 -114
  246. package/test/unit/bin/diff.test.ts +25 -28
  247. package/test/unit/bin/git.test.ts +76 -361
  248. package/test/unit/bin/notify.test.ts +299 -0
  249. package/test/unit/bin/stdin-input.test.ts +120 -0
  250. package/test/unit/bin/test-cli.test.ts +175 -0
  251. package/test/unit/bin/test-files-glob.test.ts +118 -0
  252. package/test/unit/bin/test-report.test.ts +118 -0
  253. package/test/unit/bin/utils/config/load-config.test.ts +26 -1
  254. package/test/unit/bin/utils/config/parser.test.ts +46 -5
  255. package/test/unit/bin/utils/config/structures/grouped.test.ts +12 -0
  256. package/test/unit/bin/utils/config/structures/legacy.test.ts +1 -0
  257. package/test/unit/github-scripts/mask-config-secrets.test.ts +61 -0
  258. package/test/unit/github-scripts/run-with-apify-tokens.test.ts +115 -0
  259. package/test/unit/run-checks.test.ts +76 -0
  260. package/test/unit/run-test-result.test.ts +44 -0
  261. package/test/unit/should-built-and-test.test.ts +46 -39
  262. package/tsconfig.json +1 -1
  263. package/vitest.config.ts +9 -0
  264. package/workflow-scripts-src/run-with-apify-tokens.ts +112 -0
  265. package/.github/scripts/check-major-tag-refs.mjs +0 -72
  266. package/dist/test/unit/bin/logging.test.d.ts +0 -2
  267. package/dist/test/unit/bin/logging.test.d.ts.map +0 -1
  268. package/dist/test/unit/bin/logging.test.js +0 -43
  269. package/dist/test/unit/bin/logging.test.js.map +0 -1
  270. package/dist/test/unit/bin/slack.test.d.ts +0 -2
  271. package/dist/test/unit/bin/slack.test.d.ts.map +0 -1
  272. package/dist/test/unit/bin/slack.test.js +0 -54
  273. package/dist/test/unit/bin/slack.test.js.map +0 -1
  274. package/test/unit/bin/logging.test.ts +0 -52
  275. package/test/unit/bin/slack.test.ts +0 -65
@@ -1,18 +1,20 @@
1
- import picomatch from 'picomatch';
2
-
3
1
  import { isCosmeticOnlyJsonSchemaChange } from './diff-json-schema.js';
4
2
  import { type DockerIgnoreMatcher, loadDockerIgnore } from './dockerignore.js';
5
3
  import { logger } from './logger.js';
6
- import type { ActorConfig, Commit } from './types.js';
4
+ import { createTestFilesMatcher } from './test-files-glob.js';
5
+ import type { ActorConfig, GitRange } from './types.js';
7
6
  import { type RelativeDir, RelativeFile } from './utils/path/repo-relative.js';
8
7
 
9
8
  interface ShouldBuildAndTestOptions {
10
9
  filepathsChanged: string[];
11
10
  actorConfigs: ActorConfig[];
12
- isLatest?: boolean;
13
- commits: Commit[];
14
- // Glob relative to the repo root. Matching files count as changes even when .dockerignore lists them.
15
- testFilesGlob?: string;
11
+ /** The range the files changed in, to compare `.actor` JSON files across. */
12
+ range: GitRange;
13
+ /**
14
+ * Glob of the platform test files, from the repo root (`platformTestsGlob` in the config). A change to a matching
15
+ * file is a test change, even when .dockerignore lists it.
16
+ */
17
+ platformTestsGlob?: string;
16
18
  }
17
19
 
18
20
  const IGNORED_TOP_LEVEL_FILES = [
@@ -34,6 +36,7 @@ type FileChangeForActor =
34
36
  | { impact: 'ignored' }
35
37
  | { impact: 'outside-context' }
36
38
  | { impact: 'cosmetic'; semanticallyVerified: boolean }
39
+ | { impact: 'test' }
37
40
  | { impact: 'functional' };
38
41
 
39
42
  /**
@@ -44,16 +47,18 @@ type FileChangeForActor =
44
47
  * not one per actor, so it applies to every actor regardless of context/folder.
45
48
  * 2. Context matching (actorConfig.contextPaths) → outside-context if no match
46
49
  * 3. Hardcoded ignore list, checked against the path hoisted relative to the matched context entry → ignored
47
- * 4. .dockerignore filtering (patterns relative to dockerContextDir), skipped for the actor's own `.actor/`
48
- * dir and for test files → ignored if matched
49
- * 5. README.md by filename → cosmetic if inside the actor's own folder, otherwise ignored
50
- * 6. .json inside the actor's own `.actor/` dir with only cosmetic schema diffs → cosmetic (semantically verified)
51
- * 7. Everything else → functional
50
+ * 4. Platform test files (`platformTestsGlob`) → test, even when .dockerignore lists them. A test change needs the
51
+ * tests to run, but no release.
52
+ * 5. .dockerignore filtering (patterns relative to dockerContextDir), skipped for the actor's own `.actor/`
53
+ * dir → ignored if matched
54
+ * 6. README.md by filename → cosmetic if inside the actor's own folder, otherwise ignored
55
+ * 7. .json inside the actor's own `.actor/` dir with only cosmetic schema diffs → cosmetic (semantically verified)
56
+ * 8. Everything else → functional
52
57
  */
53
58
  const classifyFileChange = async (
54
59
  changedFile: RelativeFile,
55
60
  actorConfig: ActorConfig,
56
- commits: Commit[],
61
+ range: GitRange,
57
62
  dockerIgnoreMatcher: DockerIgnoreMatcher,
58
63
  isTestFile: (filePath: string) => boolean,
59
64
  ): Promise<FileChangeForActor> => {
@@ -75,13 +80,17 @@ const classifyFileChange = async (
75
80
  return { impact: 'ignored' };
76
81
  }
77
82
 
83
+ // Test files are often in .dockerignore, but a change to them must still run the tests
84
+ if (isTestFile(changedFile.path)) {
85
+ return { impact: 'test' };
86
+ }
87
+
78
88
  const isUnderActorDotDir = actorConfig.actorJson.file.parent.contains(changedFile);
79
89
 
80
90
  // .actor/ can legitimately be listed in .dockerignore (the Apify platform evaluates it before
81
91
  // the Docker build, so excluding it from the build context is a valid caching optimization) —
82
92
  // that shouldn't cause changes to .actor/ itself to be ignored here.
83
- // Test files are often in .dockerignore too, but a change to them must still build and test.
84
- if (!isUnderActorDotDir && !isTestFile(changedFile.path) && dockerIgnoreMatcher(changedFile.path)) {
93
+ if (!isUnderActorDotDir && dockerIgnoreMatcher(changedFile.path)) {
85
94
  return { impact: 'ignored' };
86
95
  }
87
96
 
@@ -92,7 +101,7 @@ const classifyFileChange = async (
92
101
  }
93
102
 
94
103
  if (lowercaseFilePath.endsWith('.json') && isUnderActorDotDir) {
95
- const isCosmetic = await isCosmeticOnlyJsonSchemaChange(commits, changedFile.path);
104
+ const isCosmetic = await isCosmeticOnlyJsonSchemaChange(range, changedFile.path);
96
105
  if (isCosmetic) return { impact: 'cosmetic', semanticallyVerified: true };
97
106
  }
98
107
 
@@ -112,6 +121,9 @@ const isExcludedBySibling = (filePath: RelativeFile, actor: ActorConfig, allActo
112
121
  type ActorChangeEntry = {
113
122
  actorConfig: ActorConfig;
114
123
  files: string[];
124
+ hasFunctionalChange: boolean;
125
+ hasCosmeticChange: boolean;
126
+ hasTestChange: boolean;
115
127
  };
116
128
 
117
129
  type ChangeGroup = { actors: string[]; files: string[] };
@@ -164,16 +176,28 @@ const logChangeGroups = (groups: ChangeGroup[]): void => {
164
176
  }
165
177
  };
166
178
 
179
+ export interface ChangedActors {
180
+ /** Actors with at least one functional change. They need a new build and tests. */
181
+ functional: ActorConfig[];
182
+ /** Actors with cosmetic changes and no functional one. They only need a new build when released. */
183
+ cosmetic: ActorConfig[];
184
+ /**
185
+ * Actors with at least one platform test change, whatever else changed. Their tests need to run, but test changes
186
+ * alone need no release. An Actor can be in this list and in one of the others.
187
+ */
188
+ test: ActorConfig[];
189
+ }
190
+
191
+ /** Finds the Actors the changed files belong to, split by the most impactful change each Actor has. */
167
192
  export const getChangedActors = async ({
168
193
  filepathsChanged,
169
194
  actorConfigs,
170
- isLatest = false,
171
- commits,
172
- testFilesGlob,
173
- }: ShouldBuildAndTestOptions): Promise<ActorConfig[]> => {
195
+ range,
196
+ platformTestsGlob,
197
+ }: ShouldBuildAndTestOptions): Promise<ChangedActors> => {
174
198
  const actorsChangedMap = new Map<string, ActorChangeEntry>();
175
199
  const testFilesInContext = new Set<string>();
176
- const isTestFile = testFilesGlob ? picomatch(testFilesGlob, { nocase: true, dot: true }) : () => false;
200
+ const isTestFile = createTestFilesMatcher(platformTestsGlob);
177
201
 
178
202
  for (const actorConfig of actorConfigs) {
179
203
  const dockerIgnoreMatcher = loadDockerIgnore(actorConfig.dockerContextDir);
@@ -185,27 +209,31 @@ export const getChangedActors = async ({
185
209
  continue;
186
210
  }
187
211
 
188
- const change = await classifyFileChange(filePath, actorConfig, commits, dockerIgnoreMatcher, isTestFile);
212
+ const change = await classifyFileChange(filePath, actorConfig, range, dockerIgnoreMatcher, isTestFile);
189
213
  if (change.impact !== 'outside-context' && isTestFile(originalFilePath)) {
190
214
  testFilesInContext.add(originalFilePath);
191
215
  }
192
216
 
193
217
  if (change.impact === 'ignored' || change.impact === 'outside-context') continue;
194
- if (change.impact === 'cosmetic' && !isLatest) continue;
195
218
 
196
- const entry = actorsChangedMap.get(actorConfig.folder.path) ?? { actorConfig, files: [] };
219
+ const entry = actorsChangedMap.get(actorConfig.folder.path) ?? {
220
+ actorConfig,
221
+ files: [],
222
+ hasFunctionalChange: false,
223
+ hasCosmeticChange: false,
224
+ hasTestChange: false,
225
+ };
197
226
  entry.files.push(originalFilePath);
227
+ entry.hasFunctionalChange ||= change.impact === 'functional';
228
+ entry.hasCosmeticChange ||= change.impact === 'cosmetic';
229
+ entry.hasTestChange ||= change.impact === 'test';
198
230
  actorsChangedMap.set(actorConfig.folder.path, entry);
199
231
  }
200
232
  }
201
233
 
202
- const actorsChanged = Array.from(actorsChangedMap.values()).map((entry) => entry.actorConfig);
203
-
204
234
  // Log changes grouped by actor set, so changes shared across actors are logged once
205
235
  // instead of being repeated per actor.
206
- const fileToActors = buildFileToActorsMap(actorsChangedMap);
207
- const groups = groupFilesByActorSet(fileToActors);
208
- logChangeGroups(groups);
236
+ logChangeGroups(groupFilesByActorSet(buildFileToActorsMap(actorsChangedMap)));
209
237
 
210
238
  for (const testFile of filepathsChanged.filter(
211
239
  (filePath) => isTestFile(filePath) && !testFilesInContext.has(filePath),
@@ -216,12 +244,17 @@ export const getChangedActors = async ({
216
244
  );
217
245
  }
218
246
 
219
- if (actorsChanged.length > 0) {
220
- const actors = actorsChanged.map((config) => config.actorFullName);
221
- logger.info(`[DIFF]: Actors to be built and tested: ${actors.join(', ')}`);
222
- } else {
223
- logger.info(`[DIFF]: No relevant files changed, skipping builds and tests`);
224
- }
247
+ const entries = Array.from(actorsChangedMap.values());
248
+ const functional = entries.filter((entry) => entry.hasFunctionalChange).map((entry) => entry.actorConfig);
249
+ const cosmetic = entries
250
+ .filter((entry) => !entry.hasFunctionalChange && entry.hasCosmeticChange)
251
+ .map((entry) => entry.actorConfig);
252
+ const test = entries.filter((entry) => entry.hasTestChange).map((entry) => entry.actorConfig);
253
+
254
+ const names = (configs: ActorConfig[]) => configs.map((config) => config.actorFullName).join(', ') || 'none';
255
+ logger.info(`[DIFF]: Actors with functional changes: ${names(functional)}`);
256
+ logger.info(`[DIFF]: Actors with only cosmetic changes: ${names(cosmetic)}`);
257
+ logger.info(`[DIFF]: Actors with platform test changes: ${names(test)}`);
225
258
 
226
- return actorsChanged;
259
+ return { functional, cosmetic, test };
227
260
  };
@@ -1,5 +1,5 @@
1
1
  import { logger } from './logger.js';
2
- import type { Commit } from './types.js';
2
+ import type { GitRange } from './types.js';
3
3
  import { runGitCommand } from './utils.js';
4
4
 
5
5
  const COSMETIC_JSON_FIELD_NAMES = new Set([
@@ -29,11 +29,10 @@ const isCosmeticObjectChange = (oldVal: unknown, newVal: unknown, currentKey?: s
29
29
  * Returns true if the two JSON strings differ only in cosmetic fields
30
30
  * (title, description, example, enumTitles, sectionCaption, sectionDescription).
31
31
  */
32
- export const isCosmeticOnlyJsonSchemaChange = async (commits: Commit[], changedFilepath: string): Promise<boolean> => {
33
- // TODO: validate this is the right commit range
34
- const oldRef = `${commits[0].sha}~`;
35
- const newRef = commits[commits.length - 1].sha;
36
-
32
+ export const isCosmeticOnlyJsonSchemaChange = async (
33
+ { fromRef: oldRef, toRef: newRef }: GitRange,
34
+ changedFilepath: string,
35
+ ): Promise<boolean> => {
37
36
  const [oldJson, newJson] = await Promise.all([
38
37
  runGitCommand(['show', `${oldRef}:${changedFilepath}`])
39
38
  .then(JSON.parse)
package/bin/git.ts CHANGED
@@ -1,24 +1,24 @@
1
1
  import { logger } from './logger.js';
2
- import type { Commit, Config } from './types.js';
2
+ import type { Commit } from './types.js';
3
3
  import { GitCommandError, runGitCommand } from './utils.js';
4
4
 
5
5
  export const GIT_FORMAT_SEPARATOR = '»¦«';
6
6
  const GIT_LOG_FORMAT = ['%H', '%aN<%aE>', '%aD', '%s'].join(GIT_FORMAT_SEPARATOR);
7
7
 
8
+ // Lists the changed paths. With rename detection (on by default) a moved file is listed only under its new path,
9
+ // so the Actor whose context it left wouldn't count as changed. --no-renames lists it as deleted and added instead.
10
+ const LIST_CHANGED_PATHS = ['--name-only', '--no-renames'];
11
+
8
12
  /**
9
- * Gets the list of changed files between the given commits (inclusive).
13
+ * Files that differ between two commits, i.e. changed by the commits in the range (fromRef, toRef]. fromRef's own
14
+ * changes are not included: to include them, pass its parent, `${sha}~` (as getChangedFiles does).
15
+ * Diffing the endpoints, rather than walking a commit list, is what keeps merges right: the oldest commit of a
16
+ * merged branch can predate fromRef, and its changes are already in fromRef.
10
17
  */
11
- export const getChangedFiles = async (commits: Commit[]) => {
12
- // getCommits never returns an empty list (the rerun check returns all commits when the base commit
13
- // is the branch HEAD, and an empty git range throws when parsing), so this signals a programmer error
14
- if (commits.length === 0) {
15
- throw new Error('Cannot get changed files: the commit list is empty. This should never happen.');
16
- }
17
-
18
- const changedFilesString = await runGitCommand(
19
- ['diff', '--name-only', `${commits[0].sha}~..${commits[commits.length - 1].sha}`],
20
- { operation: 'get the list of changed files between commits' },
21
- );
18
+ export const getChangedFilesBetween = async (fromRef: string, toRef: string) => {
19
+ const changedFilesString = await runGitCommand(['diff', ...LIST_CHANGED_PATHS, `${fromRef}..${toRef}`], {
20
+ operation: 'get the list of changed files between commits',
21
+ });
22
22
 
23
23
  const changedFiles = changedFilesString.split('\n').filter(Boolean);
24
24
  logger.info(`Changed files (up to 50): ${changedFiles.slice(0, 50).join(', ')}`);
@@ -26,134 +26,37 @@ export const getChangedFiles = async (commits: Commit[]) => {
26
26
  };
27
27
 
28
28
  /**
29
- * Returns true if the branch contains a merge commit whose parent is reachable from targetBranch
30
- * (i.e. a genuine "merge from target" commit, not a merge of some unrelated branch).
31
- * Uses the full targetBranch..sourceBranch range, ignoring baseCommit.
29
+ * The commit `git diff base...head` compares against: where `head` forked from `base`, or the last commit of `base`
30
+ * that `head` merged in. The diff from it holds only what `head` itself changed, like GitHub's "Files changed" tab.
31
+ * With criss-cross merges there are several such commits and git picks one.
32
32
  */
33
- export const hasMergeFromTarget = async (sourceBranch: string, targetBranch: string): Promise<boolean> => {
34
- const mergeShas = (
35
- await runGitCommand(['log', '--merges', '--pretty=format:%H', `${targetBranch}..${sourceBranch}`], {
36
- operation: 'find merge commits on the source branch that are absent from the target branch',
37
- })
38
- )
39
- .split('\n')
40
- .filter(Boolean);
33
+ export const getMergeBase = async (base: string, head: string): Promise<string> =>
34
+ runGitCommand(['merge-base', base, head], { operation: `find where ${head} forked from ${base}` });
41
35
 
42
- for (const sha of mergeShas) {
43
- const parents = (
44
- await runGitCommand(['log', '-1', '--pretty=format:%P', sha], {
45
- operation: 'get parents of a merge commit',
46
- })
47
- ).split(' ');
48
- for (const parent of parents) {
49
- // git merge-base A B outputs the common ancestor.
50
- // If that equals A, then A is an ancestor of B (i.e. parent is reachable from targetBranch).
51
- let mergeBase: string;
52
- try {
53
- mergeBase = await runGitCommand(['merge-base', parent, targetBranch], {
54
- operation: 'check whether a merge parent is reachable from the target branch',
55
- });
56
- } catch (error) {
57
- // Exit code 1 on merge-base means the histories have no common ancestor.
58
- if (error instanceof GitCommandError && error.exitCode === 1) {
59
- continue;
60
- }
61
- throw error;
62
- }
63
- if (mergeBase === parent) {
64
- return true;
65
- }
66
- }
67
- }
68
- return false;
69
- };
70
-
71
- /**
72
- * Returns all files touched by non-merge commits on the branch (full history, ignoring baseCommit).
73
- * Used to check whether the branch itself has any functional changes, independent of what master merged in.
74
- */
75
- export const getBranchOnlyChangedFiles = async (sourceBranch: string, targetBranch: string): Promise<string[]> => {
76
- const output = await runGitCommand(
77
- ['log', '--no-merges', '--name-only', '--pretty=format:', `${targetBranch}..${sourceBranch}`],
78
- { operation: 'list files changed by non-merge commits on the source branch' },
79
- );
80
- return output.split('\n').filter(Boolean);
81
- };
82
-
83
- const SHA_REGEX = /^[0-9a-f]{40}$/i;
36
+ /** Resolves a ref to a commit, or undefined when it isn't in the local history (e.g. it was force-pushed away). */
37
+ export const findCommitSha = async (ref: string): Promise<string | undefined> =>
38
+ runGitCommand(['rev-parse', '--verify', '--quiet', `${ref}^{commit}`], {
39
+ operation: `look up ${ref} in local Git history`,
40
+ }).catch((error: unknown) => {
41
+ // --quiet suppresses Git's error text for a missing commit, but Git still exits with code 1
42
+ if (error instanceof GitCommandError && error.exitCode === 1) return undefined;
43
+ throw error;
44
+ });
84
45
 
85
46
  /**
86
- *
87
- * @param shaOrCommit Supports both a SHA string or a Commit object in JSON format. Can be empty.
88
- * @returns The SHA string if valid, otherwise throws an error.
47
+ * Commits reachable from toRef but not from fromRef, oldest first: the range (fromRef, toRef], so fromRef itself is
48
+ * not included. To include it, pass its parent, `${sha}~`.
89
49
  */
90
- export const parseBaseCommit = (shaOrCommit: string | undefined): string | undefined => {
91
- if (!shaOrCommit) return undefined;
92
- let sha: string;
93
- if (shaOrCommit.startsWith('{')) {
94
- sha = (JSON.parse(shaOrCommit) as Commit).sha;
95
- } else {
96
- sha = shaOrCommit;
97
- }
98
- if (!SHA_REGEX.test(sha)) {
99
- throw new Error(
100
- `Invalid base commit SHA: "${sha}". It should be a 40-character hexadecimal string, instead got input: "${shaOrCommit}".`,
101
- );
102
- }
103
- return sha;
104
- };
105
-
106
- const fetchAllBranchCommits = async (sourceBranch: string, targetBranch: string): Promise<Commit[]> => {
107
- const output = await runGitCommand(
108
- ['log', `--pretty=format:${GIT_LOG_FORMAT}`, `${targetBranch}..${sourceBranch}`],
109
- { operation: 'list commits reachable from the source ref but absent from the target ref' },
110
- );
111
- const commitsStrings = output.split('\n');
112
- const commits = commitsStrings.map((commitString) => parseCommit(commitString));
50
+ export const getCommitsBetween = async (fromRef: string, toRef: string): Promise<Commit[]> => {
51
+ const output = await runGitCommand(['log', `--pretty=format:${GIT_LOG_FORMAT}`, `${fromRef}..${toRef}`], {
52
+ operation: 'list commits reachable from the source ref but absent from the target ref',
53
+ });
54
+ // An empty range (e.g. `x..x`) prints nothing
55
+ const commits = output.split('\n').filter(Boolean).map(parseCommit);
113
56
  commits.reverse();
114
57
  return commits;
115
58
  };
116
59
 
117
- /**
118
- * Gets the commits between sourceBranch and targetBranch (exclusive).
119
- * - If baseCommit is provided, only returns commits after the baseCommit.
120
- */
121
- export const getCommits = async ({
122
- sourceBranch,
123
- targetBranch,
124
- baseCommit,
125
- }: Pick<Config, 'sourceBranch' | 'targetBranch' | 'baseCommit'>): Promise<Commit[]> => {
126
- const baseCommitSha = parseBaseCommit(baseCommit);
127
- const commits = await fetchAllBranchCommits(sourceBranch, targetBranch);
128
-
129
- // The last validated (base) commit being the branch HEAD means nothing new was pushed since the last
130
- // validation — the dev reran the workflow (or force-pushed to the same state) to trigger a clean test
131
- const headSha = commits[commits.length - 1]?.sha;
132
- if (baseCommitSha !== undefined && baseCommitSha === headSha) {
133
- logger.info(
134
- `Detected rerun with the same commit that we already validated. This usually means the user wants to rerun the Action from scratch, ignoring last validated commit ${baseCommitSha} and returning all commits`,
135
- );
136
- logger.info(`Commits being returned: ${commits.map((c) => c.sha).join(', ')}`);
137
- return commits;
138
- }
139
-
140
- const baseCommitIndex = commits.findIndex((commit) => commit.sha === baseCommitSha);
141
-
142
- const hasBaseCommit = baseCommitIndex !== -1;
143
- if (hasBaseCommit) {
144
- const commitsUpToBaseCommit = commits.slice(baseCommitIndex + 1);
145
- logger.info(
146
- `Found base commit ${baseCommitSha} at index ${baseCommitIndex}, returning ${commitsUpToBaseCommit.length} commits after it`,
147
- );
148
- logger.info(`Commits being returned: ${commitsUpToBaseCommit.map((c) => c.sha).join(', ')}`);
149
- return commitsUpToBaseCommit;
150
- }
151
-
152
- logger.info(`Base commit ${baseCommitSha} not found in the commit range, returning all ${commits.length} commits`);
153
- logger.info(`Commits being returned: ${commits.map((c) => c.sha).join(', ')}`);
154
- return commits;
155
- };
156
-
157
60
  export const parseCommit = (commitString: string): Commit => {
158
61
  const splits = commitString.split(GIT_FORMAT_SEPARATOR);
159
62
  if (splits.length !== 4) {
@@ -169,21 +72,25 @@ export const parseCommit = (commitString: string): Commit => {
169
72
  };
170
73
 
171
74
  /**
172
- * Returns the currently checked-out branch. Release builds point the Actor version at this branch,
75
+ * Returns the currently checked-out branch. Builds point the Actor version at this branch,
173
76
  * so a detached HEAD (no branch to point at) is an error rather than a guess.
174
77
  */
175
78
  export const getCurrentBranch = async (): Promise<string> => {
176
79
  const branch = await runGitCommand(['rev-parse', '--abbrev-ref', 'HEAD'], {
177
- operation: 'determine the current branch for release',
80
+ operation: 'determine the checked-out branch',
178
81
  });
179
82
  if (branch === 'HEAD') {
180
83
  throw new Error(
181
- 'Cannot determine the branch to release: HEAD is detached. Check out the branch you want to release.',
84
+ 'Cannot determine the branch to build from: HEAD is detached. Check out the branch you want to build.',
182
85
  );
183
86
  }
184
87
  return branch;
185
88
  };
186
89
 
90
+ /** Resolves a ref (branch, remote branch, SHA, HEAD, ...) to the full SHA of the commit it points to. */
91
+ export const resolveCommitSha = async (ref: string): Promise<string> =>
92
+ runGitCommand(['rev-parse', '--verify', `${ref}^{commit}`], { operation: `resolve ${ref} to a commit` });
93
+
187
94
  /**
188
95
  * Reads the repository URL from the `origin` remote, rewritten to the SSH form the Apify platform
189
96
  * uses for Git repo sources, e.g. git@github.com:apify-store/google-maps
@@ -223,11 +130,22 @@ export const normalizeRepoUrl = (repoUrl: string): string => {
223
130
  .toLowerCase();
224
131
  };
225
132
 
133
+ /**
134
+ * Web link to a commit, e.g. https://github.com/apify-store/google-maps/commit/<sha>. Null for hosts other than
135
+ * GitHub, whose commit pages live under other paths.
136
+ */
137
+ export const getCommitUrl = (repoUrl: string, sha: string): string | null => {
138
+ const repository = normalizeRepoUrl(repoUrl);
139
+ return repository.startsWith('github.com/') ? `https://${repository}/commit/${sha}` : null;
140
+ };
141
+
226
142
  /** Repository name from its URL, e.g. `google-maps` for git@github.com:apify-store/google-maps.git */
227
143
  export const getRepoName = (repoUrl: string): string => {
228
144
  return normalizeRepoUrl(repoUrl).split('/').pop()!;
229
145
  };
230
146
 
147
+ const SHA_REGEX = /^[0-9a-f]{40}$/i;
148
+
231
149
  // Sent by e.g. GitHub as the "before" commit of a push that created the branch
232
150
  const ZERO_SHA_REGEX = /^0{40}$/;
233
151
 
@@ -236,41 +154,37 @@ const ZERO_SHA_REGEX = /^0{40}$/;
236
154
  * Unlike the PR path (getCommits), there is no lenient fallback. Falling back to "everything"
237
155
  * would rebuild the latest build of every Actor and post it to Slack, so every problem is an error.
238
156
  */
239
- export const resolveReleaseBaseCommit = async (baseCommit: string): Promise<string> => {
240
- const sha = parseBaseCommit(baseCommit);
241
- if (!sha) {
242
- throw new Error('--base-commit is required for release. See the README section "Releasing Actors".');
157
+ export const resolveReleaseBaseCommit = async (baseCommit: string, head = 'HEAD'): Promise<string> => {
158
+ if (!SHA_REGEX.test(baseCommit)) {
159
+ throw new Error(
160
+ `Invalid base commit SHA: "${baseCommit}". It should be a 40-character hexadecimal string. ` +
161
+ 'See the "Releasing Actors" section of docs/cli.md.',
162
+ );
243
163
  }
164
+ const sha = baseCommit;
244
165
  if (ZERO_SHA_REGEX.test(sha)) {
245
166
  throw new Error(
246
167
  `Base commit is ${sha}, which means the branch was just created and there is no previous release to diff against. ` +
247
- `Rerun with --base-commit set to the last commit before the changes you want to release.`,
168
+ `Rerun with the base commit set to the last commit before the changes you want to release.`,
248
169
  );
249
170
  }
250
- // --quiet suppresses Git's error text for a missing commit, but Git still exits with code 1.
251
- const verifiedSha = await runGitCommand(['rev-parse', '--verify', '--quiet', `${sha}^{commit}`], {
252
- operation: 'verify the release base commit in local Git history',
253
- }).catch((error: unknown) => {
254
- if (error instanceof GitCommandError && error.exitCode === 1) return '';
255
- throw error;
256
- });
257
- if (!verifiedSha) {
171
+ if (!(await findCommitSha(sha))) {
258
172
  throw new Error(
259
173
  `Base commit ${sha} is not in the local git history. Either the checkout is shallow ` +
260
174
  `(fetch the full history, e.g. fetch-depth: 0 in actions/checkout) or the branch was force-pushed.`,
261
175
  );
262
176
  }
263
177
  // git merge-base A B outputs the common ancestor. If that equals A, then A is an ancestor of B.
264
- const mergeBase = await runGitCommand(['merge-base', verifiedSha, 'HEAD'], {
265
- operation: 'check whether the release base commit is an ancestor of HEAD',
178
+ const mergeBase = await runGitCommand(['merge-base', sha, head], {
179
+ operation: `check whether the release base commit is an ancestor of ${head}`,
266
180
  }).catch((error: unknown) => {
267
181
  if (error instanceof GitCommandError && error.exitCode === 1) return '';
268
182
  throw error;
269
183
  });
270
184
  if (mergeBase !== sha) {
271
185
  throw new Error(
272
- `Base commit ${sha} is not an ancestor of HEAD, most likely because the branch was force-pushed. ` +
273
- `The changed files cannot be determined reliably. Rerun with --base-commit set to an ancestor of HEAD ` +
186
+ `Base commit ${sha} is not an ancestor of ${head}, most likely because the branch was force-pushed. ` +
187
+ `The changed files cannot be determined reliably. Rerun with the base commit set to an ancestor of ${head} ` +
274
188
  `from before the changes you want to release.`,
275
189
  );
276
190
  }
@@ -279,12 +193,16 @@ export const resolveReleaseBaseCommit = async (baseCommit: string): Promise<stri
279
193
 
280
194
  const CHANGELOG_PATH = 'CHANGELOG.md';
281
195
 
282
- /** Returns the lines added to the root CHANGELOG.md between baseSha and HEAD, or null if it didn't change. */
283
- const getChangelogAdditions = async (baseSha: string, changedFiles: string[]): Promise<string | null> => {
196
+ /** Returns the lines added to the root CHANGELOG.md between fromRef and toRef, or null if it didn't change. */
197
+ export const getChangelogAdditions = async (
198
+ fromRef: string,
199
+ toRef: string,
200
+ changedFiles: string[],
201
+ ): Promise<string | null> => {
284
202
  if (!changedFiles.includes(CHANGELOG_PATH)) {
285
203
  return null;
286
204
  }
287
- const diff = await runGitCommand(['diff', baseSha, 'HEAD', '--', CHANGELOG_PATH], {
205
+ const diff = await runGitCommand(['diff', fromRef, toRef, '--', CHANGELOG_PATH], {
288
206
  operation: 'read changelog changes since the last release',
289
207
  });
290
208
 
@@ -307,23 +225,3 @@ const getChangelogAdditions = async (baseSha: string, changedFiles: string[]): P
307
225
  }
308
226
  return added.join('\n').trim();
309
227
  };
310
-
311
- /**
312
- * Everything that changed since the last release (baseSha, exclusive) up to HEAD, or null when there
313
- * are no new commits. Changed files come from diffing the range directly instead of from the commit
314
- * list: with a merge commit, the oldest commit of the merged branch can be older than baseSha, and
315
- * diffing from its parent would pull in already-released changes.
316
- */
317
- export const getReleaseChanges = async (baseSha: string) => {
318
- if (
319
- (await runGitCommand(['rev-parse', 'HEAD'], { operation: 'identify the current release commit' })) === baseSha
320
- ) {
321
- return null;
322
- }
323
- const commits = await fetchAllBranchCommits('HEAD', baseSha);
324
- const changedFiles = await runGitCommand(['diff', '--name-only', baseSha, 'HEAD'], {
325
- operation: 'list files changed since the last release',
326
- }).then((output) => output.split('\n').filter(Boolean));
327
- const changelog = await getChangelogAdditions(baseSha, changedFiles);
328
- return { commits, changedFiles, changelog };
329
- };