apify-test-tools 0.10.1-beta.3 → 0.10.1-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.
Files changed (144) hide show
  1. package/.claude-plugin/marketplace.json +12 -0
  2. package/.github/workflows/public_platform-tests-claude-investigate-and-fix.yaml +22 -10
  3. package/CHANGELOG.md +7 -0
  4. package/CONTRIBUTING.md +9 -0
  5. package/README.md +6 -7
  6. package/bin/build-from-local.ts +8 -7
  7. package/bin/build.ts +22 -21
  8. package/bin/diff-changes.ts +11 -10
  9. package/bin/diff-json-schema.ts +14 -12
  10. package/bin/git.ts +90 -43
  11. package/bin/logger.ts +23 -0
  12. package/bin/main.ts +49 -35
  13. package/bin/output.ts +6 -0
  14. package/bin/slack.ts +8 -7
  15. package/bin/test-report.ts +22 -21
  16. package/bin/utils/config/actor-json.ts +84 -0
  17. package/bin/utils/config/load-config.ts +46 -46
  18. package/bin/utils/config/parser.ts +33 -28
  19. package/bin/utils/config/structures/base.ts +7 -0
  20. package/bin/utils/json-file.ts +11 -14
  21. package/bin/utils/path/repo-relative.ts +156 -0
  22. package/bin/utils/path/schema.ts +18 -0
  23. package/bin/utils.ts +61 -34
  24. package/dist/bin/build-from-local.d.ts +1 -1
  25. package/dist/bin/build-from-local.d.ts.map +1 -1
  26. package/dist/bin/build-from-local.js +8 -7
  27. package/dist/bin/build-from-local.js.map +1 -1
  28. package/dist/bin/build.d.ts.map +1 -1
  29. package/dist/bin/build.js +22 -21
  30. package/dist/bin/build.js.map +1 -1
  31. package/dist/bin/diff-changes.d.ts +1 -1
  32. package/dist/bin/diff-changes.d.ts.map +1 -1
  33. package/dist/bin/diff-changes.js +9 -8
  34. package/dist/bin/diff-changes.js.map +1 -1
  35. package/dist/bin/diff-json-schema.d.ts +1 -1
  36. package/dist/bin/diff-json-schema.d.ts.map +1 -1
  37. package/dist/bin/diff-json-schema.js +13 -12
  38. package/dist/bin/diff-json-schema.js.map +1 -1
  39. package/dist/bin/git.d.ts +10 -10
  40. package/dist/bin/git.d.ts.map +1 -1
  41. package/dist/bin/git.js +78 -37
  42. package/dist/bin/git.js.map +1 -1
  43. package/dist/bin/logger.d.ts +11 -0
  44. package/dist/bin/logger.d.ts.map +1 -0
  45. package/dist/bin/logger.js +20 -0
  46. package/dist/bin/logger.js.map +1 -0
  47. package/dist/bin/main.d.ts +1 -1
  48. package/dist/bin/main.js +48 -35
  49. package/dist/bin/main.js.map +1 -1
  50. package/dist/bin/output.d.ts +3 -0
  51. package/dist/bin/output.d.ts.map +1 -0
  52. package/dist/bin/output.js +6 -0
  53. package/dist/bin/output.js.map +1 -0
  54. package/dist/bin/slack.d.ts.map +1 -1
  55. package/dist/bin/slack.js +8 -7
  56. package/dist/bin/slack.js.map +1 -1
  57. package/dist/bin/test-report.d.ts.map +1 -1
  58. package/dist/bin/test-report.js +22 -21
  59. package/dist/bin/test-report.js.map +1 -1
  60. package/dist/bin/utils/config/actor-json.d.ts +14 -0
  61. package/dist/bin/utils/config/actor-json.d.ts.map +1 -0
  62. package/dist/bin/utils/config/actor-json.js +60 -0
  63. package/dist/bin/utils/config/actor-json.js.map +1 -0
  64. package/dist/bin/utils/config/load-config.d.ts +12 -2
  65. package/dist/bin/utils/config/load-config.d.ts.map +1 -1
  66. package/dist/bin/utils/config/load-config.js +32 -31
  67. package/dist/bin/utils/config/load-config.js.map +1 -1
  68. package/dist/bin/utils/config/parser.d.ts +4 -12
  69. package/dist/bin/utils/config/parser.d.ts.map +1 -1
  70. package/dist/bin/utils/config/parser.js +24 -23
  71. package/dist/bin/utils/config/parser.js.map +1 -1
  72. package/dist/bin/utils/config/structures/base.d.ts +5 -0
  73. package/dist/bin/utils/config/structures/base.d.ts.map +1 -1
  74. package/dist/bin/utils/config/structures/base.js.map +1 -1
  75. package/dist/bin/utils/json-file.d.ts +3 -2
  76. package/dist/bin/utils/json-file.d.ts.map +1 -1
  77. package/dist/bin/utils/json-file.js +8 -13
  78. package/dist/bin/utils/json-file.js.map +1 -1
  79. package/dist/bin/utils/path/repo-relative.d.ts +35 -0
  80. package/dist/bin/utils/path/repo-relative.d.ts.map +1 -0
  81. package/dist/bin/utils/path/repo-relative.js +137 -0
  82. package/dist/bin/utils/path/repo-relative.js.map +1 -0
  83. package/dist/bin/utils/path/schema.d.ts +7 -0
  84. package/dist/bin/utils/path/schema.d.ts.map +1 -0
  85. package/dist/bin/utils/path/schema.js +16 -0
  86. package/dist/bin/utils/path/schema.js.map +1 -0
  87. package/dist/bin/utils.d.ts +14 -3
  88. package/dist/bin/utils.d.ts.map +1 -1
  89. package/dist/bin/utils.js +52 -29
  90. package/dist/bin/utils.js.map +1 -1
  91. package/dist/test/unit/bin/build-from-local.test.js +3 -39
  92. package/dist/test/unit/bin/build-from-local.test.js.map +1 -1
  93. package/dist/test/unit/bin/diff-changes.test.js +97 -96
  94. package/dist/test/unit/bin/diff-changes.test.js.map +1 -1
  95. package/dist/test/unit/bin/diff.test.js +52 -48
  96. package/dist/test/unit/bin/diff.test.js.map +1 -1
  97. package/dist/test/unit/bin/git-command.test.d.ts +2 -0
  98. package/dist/test/unit/bin/git-command.test.d.ts.map +1 -0
  99. package/dist/test/unit/bin/git-command.test.js +54 -0
  100. package/dist/test/unit/bin/git-command.test.js.map +1 -0
  101. package/dist/test/unit/bin/git.test.js +211 -92
  102. package/dist/test/unit/bin/git.test.js.map +1 -1
  103. package/dist/test/unit/bin/logger.test.d.ts +2 -0
  104. package/dist/test/unit/bin/logger.test.d.ts.map +1 -0
  105. package/dist/test/unit/bin/logger.test.js +44 -0
  106. package/dist/test/unit/bin/logger.test.js.map +1 -0
  107. package/dist/test/unit/bin/logging.test.d.ts +2 -0
  108. package/dist/test/unit/bin/logging.test.d.ts.map +1 -0
  109. package/dist/test/unit/bin/logging.test.js +43 -0
  110. package/dist/test/unit/bin/logging.test.js.map +1 -0
  111. package/dist/test/unit/bin/utils/config/load-config.test.js +47 -25
  112. package/dist/test/unit/bin/utils/config/load-config.test.js.map +1 -1
  113. package/dist/test/unit/bin/utils/config/parser.test.js +64 -19
  114. package/dist/test/unit/bin/utils/config/parser.test.js.map +1 -1
  115. package/dist/test/unit/bin/utils/files.test.js +16 -27
  116. package/dist/test/unit/bin/utils/files.test.js.map +1 -1
  117. package/dist/test/unit/bin/utils/path/repo-relative.test.d.ts +2 -0
  118. package/dist/test/unit/bin/utils/path/repo-relative.test.d.ts.map +1 -0
  119. package/dist/test/unit/bin/utils/path/repo-relative.test.js +196 -0
  120. package/dist/test/unit/bin/utils/path/repo-relative.test.js.map +1 -0
  121. package/dist/test/unit/bin/utils/path/schema.test.d.ts +2 -0
  122. package/dist/test/unit/bin/utils/path/schema.test.d.ts.map +1 -0
  123. package/dist/test/unit/bin/utils/path/schema.test.js +52 -0
  124. package/dist/test/unit/bin/utils/path/schema.test.js.map +1 -0
  125. package/dist/test/unit/should-built-and-test.test.js +40 -38
  126. package/dist/test/unit/should-built-and-test.test.js.map +1 -1
  127. package/dist/tsconfig.tsbuildinfo +1 -1
  128. package/eslint.config.mjs +9 -1
  129. package/package.json +4 -4
  130. package/platform-tests-investigate-and-fix/.claude-plugin/plugin.json +4 -0
  131. package/platform-tests-investigate-and-fix/skills/measure-flakiness/SKILL.md +28 -0
  132. package/test/unit/bin/build-from-local.test.ts +3 -53
  133. package/test/unit/bin/diff-changes.test.ts +97 -96
  134. package/test/unit/bin/diff.test.ts +53 -48
  135. package/test/unit/bin/git-command.test.ts +71 -0
  136. package/test/unit/bin/git.test.ts +246 -89
  137. package/test/unit/bin/logger.test.ts +55 -0
  138. package/test/unit/bin/logging.test.ts +52 -0
  139. package/test/unit/bin/utils/config/load-config.test.ts +63 -26
  140. package/test/unit/bin/utils/config/parser.test.ts +81 -27
  141. package/test/unit/bin/utils/files.test.ts +19 -33
  142. package/test/unit/bin/utils/path/repo-relative.test.ts +250 -0
  143. package/test/unit/bin/utils/path/schema.test.ts +65 -0
  144. package/test/unit/should-built-and-test.test.ts +40 -38
@@ -0,0 +1,12 @@
1
+ {
2
+ "name": "apify-test-tools",
3
+ "owner": {
4
+ "name": "Apify"
5
+ },
6
+ "plugins": [
7
+ {
8
+ "name": "platform-tests-investigate-and-fix",
9
+ "source": "./platform-tests-investigate-and-fix"
10
+ }
11
+ ]
12
+ }
@@ -39,16 +39,17 @@ on:
39
39
  type: string
40
40
  default: sonnet
41
41
  description: Claude model to use for investigation and fix
42
+ max_flakiness_runs:
43
+ required: false
44
+ type: number
45
+ default: 200
46
+ description: The most times Claude may re-run the failing Actor run to measure how often the failure happens, when that isn't obvious. Claude decides how many of them it needs.
42
47
  investigate_prompt:
43
48
  required: false
44
49
  type: string
45
50
  default: |
46
51
  Investigate the failing E2E platform test and open a GitHub issue documenting your findings. Start by reviewing the test logs, error messages, and recent workflow run history to determine the root cause.
47
52
 
48
- The failure will typically fall into one of these categories:
49
- - Flaky test: The test is non-deterministic or depends on timing/external state and fails intermittently. Check past workflow runs to see if this test has failed before.
50
- - Real bug: Genuine broken functionality — either a pre-existing issue or an environment/dependency change outside the codebase (e.g. a website changed its structure, an external API changed behavior).
51
-
52
53
  Use available tools to check previous workflow runs, git history, and existing GitHub issues to gather context and avoid creating duplicate issues. For deeper context,
53
54
  use tools to investigate the run on Apify platform, review its logs, datasets, key-value stores.
54
55
 
@@ -56,9 +57,12 @@ on:
56
57
  - Debug snapshots: often stored as a field on dataset items (e.g. `_debugSnapshots`) or as records in a key-value store, keyed so they can be matched to the log line for the failing request.
57
58
  - Any other debug artifacts the Actor produced (extra logs, error/state fields on output items) that narrow down the failing request.
58
59
 
60
+ Before you create a new issue, check whether the `platform-tests-investigate-and-fix:measure-flakiness` skill applies, and follow it if it does.
61
+
59
62
  When you create the issue, write it like an incident postmortem: a short summary and impact up top for anyone skimming, full technical detail below for whoever fixes it. Don't water down the engineering detail to make the top readable.
60
63
  - Title: short and clean, describing the actual problem in plain terms — no dates, run numbers, or other noise. Prefix it with a conventional-commit-style type, e.g. "fix: blocking issue on detail endpoint".
61
64
  - Body, section 1 ("## Summary"): one line stating what's broken plus its impact, in plain language a PM can understand at a glance — no jargon, internal field names, or stack traces (e.g. "Scraping product detail pages fails when a listing has no reviews, affecting N% of runs").
65
+ - If you measured flakiness, a "## Flakiness" section right after the Summary, as the `measure-flakiness` skill describes.
62
66
  - A `---` divider, then section 2 ("## Details"): the engineering bug report — root cause, relevant log lines, links to the failing run, and any other internals an engineer needs. If a single item/request failed among otherwise-good ones, show or link that specific one (e.g. the dataset item index/id, or its matching debug snapshot) rather than just describing the run as failed.
63
67
 
64
68
  If you create a new issue, you MUST write its issue number (just the number, nothing else) to $GITHUB_WORKSPACE/issue-number.txt as the very last thing you do. If you skip creating one because a duplicate already exists, do not create the file.
@@ -67,9 +71,12 @@ on:
67
71
  ANTHROPIC_API_KEY:
68
72
  required: true
69
73
  description: Claude API key
70
- TESTER_APIFY_TOKEN_READ_ONLY:
74
+ TESTER_APIFY_TOKEN:
71
75
  required: true
72
- description: Apify token used only for read-only investigation — reading the failing run, its log and its storages.
76
+ description: Apify token for investigating the failing run — reading it, its log and its storages — and re-running it to measure how often the failure happens.
77
+ NPM_TOKEN:
78
+ required: false
79
+ description: Read-only token for installing private npm packages before Claude runs.
73
80
 
74
81
  jobs:
75
82
  investigate:
@@ -89,7 +96,8 @@ jobs:
89
96
  id: claude
90
97
  uses: anthropics/claude-code-action@8251c103ac8c1d761882c86aba1412c7f583c844 # v1.0.213
91
98
  env:
92
- TESTER_APIFY_TOKEN_READ_ONLY: ${{ secrets.TESTER_APIFY_TOKEN_READ_ONLY }}
99
+ TESTER_APIFY_TOKEN: ${{ secrets.TESTER_APIFY_TOKEN }}
100
+ MAX_FLAKINESS_RUNS: ${{ inputs.max_flakiness_runs }}
93
101
  with:
94
102
  prompt: |
95
103
  The failing E2E platform test ran in workflow run ${{ github.run_id }} of this repo.
@@ -97,13 +105,15 @@ jobs:
97
105
  ${{ inputs.investigate_prompt }}
98
106
  github_token: ${{ secrets.GITHUB_TOKEN }}
99
107
  anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }}
108
+ plugin_marketplaces: https://github.com/apify/apify-test-tools.git
109
+ plugins: platform-tests-investigate-and-fix@apify-test-tools
100
110
  # The `tools` query parameter below selects which tools the Apify MCP server
101
111
  # exposes: reading an Actor run — its status, exit code, storage ids and log — and
102
112
  # reading that run's dataset items and its key-value store keys and records, e.g.
103
113
  # INPUT, OUTPUT and error snapshots. WARN: the server adds `abort-actor-run` on top
104
114
  # even when not listed here, see https://github.com/apify/apify-mcp-server
105
115
  claude_args: |
106
- --mcp-config '{"mcpServers":{"apify":{"type":"http","url":"https://mcp.apify.com/?tools=get-actor-run,get-actor-log,get-dataset-list,get-dataset,get-dataset-items,get-key-value-store-list,get-key-value-store,get-key-value-store-keys,get-key-value-store-record","headers":{"Authorization":"Bearer ${TESTER_APIFY_TOKEN_READ_ONLY}"}}}}'
116
+ --mcp-config '{"mcpServers":{"apify":{"type":"http","url":"https://mcp.apify.com/?tools=get-actor-run,get-actor-run-log,get-dataset-list,get-dataset,get-dataset-items,get-key-value-store-list,get-key-value-store,get-key-value-store-keys,get-key-value-store-record","headers":{"Authorization":"Bearer ${TESTER_APIFY_TOKEN}"}}}}'
107
117
  --model ${{ inputs.model }}
108
118
  --dangerously-skip-permissions
109
119
 
@@ -166,8 +176,10 @@ jobs:
166
176
  id-token: write
167
177
  actions: read
168
178
  steps:
169
- - name: Checkout repository
170
- uses: actions/checkout@v5
179
+ - name: Setup repository and dependencies
180
+ uses: apify/apify-test-tools/.github/actions/checkout-restore-dependencies@workflows-v0
181
+ with:
182
+ npm-token: ${{ secrets.NPM_TOKEN }}
171
183
 
172
184
  - name: Run Claude Code
173
185
  id: claude
package/CHANGELOG.md CHANGED
@@ -5,10 +5,17 @@ All notable changes to this project will be documented in this file.
5
5
  <!-- git-cliff-unreleased-start -->
6
6
  ## 0.10.1 - **not yet released**
7
7
 
8
+ ### 🚀 Features
9
+
10
+ - **relative-path:** Path safety and manipulation lib ([#154](https://github.com/apify/apify-test-tools/pull/154)) ([c151a1c](https://github.com/apify/apify-test-tools/commit/c151a1c2755fef98b335d48a3461f5042b90def9)) by [@JuanGalilea](https://github.com/JuanGalilea)
11
+ - **logger:** Add logger and ban console statements ([#170](https://github.com/apify/apify-test-tools/pull/170)) ([b295d36](https://github.com/apify/apify-test-tools/commit/b295d363276c92fc6c30dd34d7a90acef5d380bf)) by [@JuanGalilea](https://github.com/JuanGalilea), closes [#156](https://github.com/apify/apify-test-tools/issues/156)
12
+ - Give claude investigator a skill to test flakiness ([#184](https://github.com/apify/apify-test-tools/pull/184)) ([b490298](https://github.com/apify/apify-test-tools/commit/b4902987dc2ae55882463ff8223e1e9f25b8b497)) by [@foxt451](https://github.com/foxt451), closes [#162](https://github.com/apify/apify-test-tools/issues/162)
13
+
8
14
  ### 🐛 Bug Fixes
9
15
 
10
16
  - **internal-workflows:** Wait out npm&#x27;s async publish before holding the major tag ([#160](https://github.com/apify/apify-test-tools/pull/160)) ([78434f0](https://github.com/apify/apify-test-tools/commit/78434f03d288ecbd383d55d56a1610f06ded298f)) by [@metalwarrior665](https://github.com/metalwarrior665)
11
17
  - Skip builds and tests when the net diff is empty ([#171](https://github.com/apify/apify-test-tools/pull/171)) ([874d70c](https://github.com/apify/apify-test-tools/commit/874d70ca5506f7ec742a1808c9bf6c670f08552c)) by [@artogahr](https://github.com/artogahr)
18
+ - **workflows:** Add npm install step to Claude fix workflow ([#161](https://github.com/apify/apify-test-tools/pull/161)) ([d8884e0](https://github.com/apify/apify-test-tools/commit/d8884e07f778de14c8d9d7f0ec9e0a1672c224ad)) by [@foxt451](https://github.com/foxt451), closes [#39](https://github.com/apify/apify-test-tools/issues/39)
12
19
 
13
20
 
14
21
  <!-- git-cliff-unreleased-end -->
package/CONTRIBUTING.md CHANGED
@@ -14,6 +14,15 @@ The package consists of three parts:
14
14
  - `bin/slack.ts` sending notifications to slack
15
15
  - `bin/test-report.ts` processing vitest's test reports
16
16
 
17
+ CLI command results use `writeJson` from `bin/output.ts` to write newline-terminated JSON to stdout.
18
+ Use the CLI `logger` from `bin/logger.ts` for diagnostics; every level writes to stderr.
19
+ ESLint rejects `console.log` throughout the codebase and all direct `console` calls in `bin/`.
20
+ The importable library in `lib/` keeps its own diagnostics; CLI log levels apply only to `bin/`.
21
+
22
+ The CLI's global `--log-level` option accepts `debug`, `info` (default), `warn`, `error`, or `silent`.
23
+ It filters diagnostics without affecting command results. Use `logger.debug` for detailed troubleshooting,
24
+ `logger.info` for progress, `logger.warn` for recoverable problems, and `logger.error` for failures.
25
+
17
26
  ## Test library
18
27
 
19
28
  - `lib/extend-expect.ts` - custom matchers
package/README.md CHANGED
@@ -258,13 +258,12 @@ it at a branch only to test a prompt change.
258
258
  Callers pass `secrets: inherit`. The workflows do not turn every inherited secret into job-wide
259
259
  environment variables, so a secret is only visible to the step that needs it:
260
260
 
261
- | Secret | Reaches |
262
- | ------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
263
- | `NPM_TOKEN` | dependency install steps only, as both `NPM_TOKEN` and `NODE_AUTH_TOKEN`. Use a read-only token: npm granular tokens can be read-only, classic automation tokens can publish. |
264
- | the Actor tokens named by `tokenEnvVar` in `apify-test-tools.config.json` | the `build`, `release`, and `delete-old-builds` steps only |
265
- | `TESTER_APIFY_TOKEN` | the vitest step only |
266
- | `SLACK_TOKEN_TESTS_BOT` / `SLACK_TOKEN_RELEASES_BOT` | the reporting and release steps only |
267
- | `TESTER_APIFY_TOKEN_READ_ONLY` | the Claude investigation step only, as the Apify MCP server's bearer token. Required by `public_platform-tests-claude-investigate-and-fix`. Use a read-only token: it reads the failing run, its log and its storages. |
261
+ | Secret | Reaches |
262
+ | ------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
263
+ | `NPM_TOKEN` | dependency install steps only, as both `NPM_TOKEN` and `NODE_AUTH_TOKEN`. Use a read-only token: npm granular tokens can be read-only, classic automation tokens can publish. |
264
+ | the Actor tokens named by `tokenEnvVar` in `apify-test-tools.config.json` | the `build`, `release`, and `delete-old-builds` steps only |
265
+ | `TESTER_APIFY_TOKEN` | the vitest step, and the Claude investigation step: the Apify MCP server's bearer token for reading the failing run, its log and its storages, and Claude's token for re-running it to measure how often the failure happens |
266
+ | `SLACK_TOKEN_TESTS_BOT` / `SLACK_TOKEN_RELEASES_BOT` | the reporting and release steps only |
268
267
 
269
268
  The Actor tokens are the one set that cannot be listed in the workflow, because each Actor names its
270
269
  own token via `tokenEnvVar` in `apify-test-tools.config.json`. Those steps pass
@@ -8,6 +8,7 @@ import { ActorSourceType } from 'apify-client';
8
8
 
9
9
  import { dryRunBuildData, LOCAL_SOURCE_VERSION_NUMBER, runAndSummarizeBuilds } from './build.js';
10
10
  import { buildDockerIgnoreMatcher } from './dockerignore.js';
11
+ import { logger } from './logger.js';
11
12
  import { isPathWithinScope } from './path-utils.js';
12
13
  import type { ActorConfig, BuildData } from './types.js';
13
14
  import type { SourceFile } from './utils.js';
@@ -34,7 +35,7 @@ export const collectSourceFiles = async (actorName: string, actorDir: string): P
34
35
  const isMonorepoActor = !!contextAbsDir && isOutsideDir(contextAbsDir, absActorDir);
35
36
 
36
37
  const dockerContextDirAbs = isMonorepoActor ? contextAbsDir! : absActorDir;
37
- const keptFilePaths = collectNonIgnoredFiles(dockerContextDirAbs, repoRoot);
38
+ const keptFilePaths = await collectNonIgnoredFiles(dockerContextDirAbs, repoRoot);
38
39
 
39
40
  if (!isMonorepoActor) {
40
41
  return Promise.all(keptFilePaths.map(async (filePath) => readSourceFile(filePath, dockerContextDirAbs)));
@@ -64,9 +65,9 @@ export const collectSourceFiles = async (actorName: string, actorDir: string): P
64
65
  // regardless of .gitignore/.dockerignore, matching Apify CLI's own behavior. Files matching the
65
66
  // hardcoded secret-pattern backstop (keys, certs, .env variants) are dropped unconditionally,
66
67
  // .actor/ included, since those should never ship regardless of what the ignore files say.
67
- export const collectNonIgnoredFiles = (dockerContextDir: string, repoRoot: string): string[] => {
68
- const relativePaths = listRepoFilePaths(repoRoot, dockerContextDir);
69
- const ignoredPaths = getGitignoredPaths(relativePaths);
68
+ export const collectNonIgnoredFiles = async (dockerContextDir: string, repoRoot: string): Promise<string[]> => {
69
+ const relativePaths = await listRepoFilePaths(repoRoot, dockerContextDir);
70
+ const ignoredPaths = await getGitignoredPaths(relativePaths, repoRoot);
70
71
  const rootRelativePaths = new Map(
71
72
  relativePaths.map((relPath) => [
72
73
  relPath,
@@ -104,7 +105,7 @@ export const flattenMonorepoContext = async (
104
105
  actorJson: Record<string, unknown>,
105
106
  keptContextFiles: string[],
106
107
  ): Promise<{ tempDir: string; filePaths: string[] }> => {
107
- console.error(`[${actorName}]: monorepo actor detected — flattening from Docker context`);
108
+ logger.info(`[${actorName}]: monorepo actor detected — flattening from Docker context`);
108
109
 
109
110
  const tempDir = await fs.mkdtemp(path.join(os.tmpdir(), `apify-build-${actorName.replace('/', '_')}-`));
110
111
  const filePaths: string[] = [];
@@ -194,9 +195,9 @@ export const runBuildsFromLocal = async ({
194
195
  dryRun: boolean;
195
196
  }): Promise<BuildData[]> => {
196
197
  if (dryRun) {
197
- console.error('[DRY RUN] Would build from local source:');
198
+ logger.info('[DRY RUN] Would build from local source:');
198
199
  for (const { actorFullName, folder } of actorConfigs) {
199
- console.error(` ${actorFullName} (${folder})`);
200
+ logger.info(` ${actorFullName} (${folder})`);
200
201
  }
201
202
  return actorConfigs.map(({ actorFullName }) => dryRunBuildData(actorFullName, LOCAL_SOURCE_VERSION_NUMBER));
202
203
  }
package/bin/build.ts CHANGED
@@ -2,6 +2,7 @@ import type * as ApifyClientTypes from 'apify-client';
2
2
  import { ActorSourceType, ApifyClient } from 'apify-client';
3
3
 
4
4
  import { normalizeRepoUrl } from './git.js';
5
+ import { logger } from './logger.js';
5
6
  import type { ActorConfig, BuildData } from './types.js';
6
7
 
7
8
  type BuildPrActorOptions = {
@@ -28,7 +29,7 @@ const ACTOR_SETUP_REQUIREMENT =
28
29
  */
29
30
  export const resolveDefaultVersion = (actorFullName: string, actorInfo: ApifyClientTypes.Actor) => {
30
31
  const defaultBuildTag = actorInfo.defaultRunOptions.build;
31
- console.error(`Default build tag for ${actorFullName} is ${defaultBuildTag}`);
32
+ logger.info(`Default build tag for ${actorFullName} is ${defaultBuildTag}`);
32
33
 
33
34
  // We could technically allow this but in most cases this is accidentally set wrongly and there is a workaround
34
35
  if (defaultBuildTag.match(/\d+\.\d+\.\d+/)) {
@@ -43,7 +44,7 @@ export const resolveDefaultVersion = (actorFullName: string, actorInfo: ApifyCli
43
44
  throw new Error(`[${actorFullName}] No build found for tag "${defaultBuildTag}". ${ACTOR_SETUP_REQUIREMENT}`);
44
45
  }
45
46
  const defaultVersionNumber = defaultBuildNumber.match(/(\d+\.\d+)\.\d+/)![1];
46
- console.error(`Default version for ${actorFullName} is ${defaultVersionNumber}`);
47
+ logger.info(`Default version for ${actorFullName} is ${defaultVersionNumber}`);
47
48
 
48
49
  const defaultVersion = actorInfo.versions.find((version) => version.versionNumber === defaultVersionNumber);
49
50
 
@@ -139,7 +140,7 @@ export class ApifyBuilder {
139
140
  // We also get back actId so the testing actor can both match by actor ID and name
140
141
  const { id, actId, buildNumber } = await actorClient.build(versionNumber, { useCache });
141
142
 
142
- console.error(`[${this.actorFullName}]: ${id} (${buildNumber})`);
143
+ logger.info(`[${this.actorFullName}]: ${id} (${buildNumber})`);
143
144
  return { buildId: id, actorRawId: actId, buildNumber, actorFullName: this.actorFullName };
144
145
  };
145
146
 
@@ -147,20 +148,20 @@ export class ApifyBuilder {
147
148
  const build = await this.apifyClient.build(buildId).waitForFinish();
148
149
  const versionNumber = build.buildNumber;
149
150
  if (build.status === 'FAILED' || build.status === 'TIMED-OUT') {
150
- console.error(`[${this.actorFullName}]: ${versionNumber}`);
151
+ logger.error(`[${this.actorFullName}]: ${versionNumber}`);
151
152
  try {
152
153
  const log = await this.apifyClient.build(buildId).log().get();
153
154
  const logTail = log?.split('\n').slice(-40).join('\n');
154
- console.error(`\n--- BUILD LOG (last 40 lines) ---\n${logTail}\n---`);
155
+ logger.error(`\n--- BUILD LOG (last 40 lines) ---\n${logTail}\n---`);
155
156
  } catch (err) {
156
- console.error(`[${this.actorFullName}]: Failed to fetch build log: ${err}`);
157
+ logger.error(`[${this.actorFullName}]: Failed to fetch build log: ${err}`);
157
158
  }
158
159
  throw new Error(
159
160
  `[BUILD][${this.actorFullName}]: Build ${buildId} (${versionNumber}) failed. ` +
160
161
  `Not continuing with other builds and tests.`,
161
162
  );
162
163
  }
163
- console.error(`[${this.actorFullName}]: ${versionNumber}`);
164
+ logger.info(`[${this.actorFullName}]: ${versionNumber}`);
164
165
  return build;
165
166
  };
166
167
 
@@ -227,7 +228,7 @@ export class ApifyBuilder {
227
228
  type CorrectBuildColletionItem = (typeof items)[0] & { buildNumber: string };
228
229
  const buildsToDelete = (items as CorrectBuildColletionItem[]).filter((build) => {
229
230
  if (build.buildNumber === defaultBuildNumber) {
230
- console.error(
231
+ logger.info(
231
232
  `[DELETE OLD BUILDS][${this.actorFullName}]: Skipping default build ${defaultBuildNumber} (${defaultBuildTag}). ` +
232
233
  `We never delete default builds`,
233
234
  );
@@ -238,7 +239,7 @@ export class ApifyBuilder {
238
239
  (protectedBuildNumber) => protectedBuildNumber.buildNumber === build.buildNumber,
239
240
  );
240
241
  if (protectedTagFound) {
241
- console.error(
242
+ logger.info(
242
243
  `[DELETE OLD BUILDS][${this.actorFullName}]: Skipping protected build ${protectedTagFound.buildNumber} (${protectedTagFound.tag}).`,
243
244
  );
244
245
  return false;
@@ -247,7 +248,7 @@ export class ApifyBuilder {
247
248
  if (taggedDevelBuildNumber && build.buildNumber === taggedDevelBuildNumber) {
248
249
  const shouldDeleteDevelBuild = build.startedAt.getTime() < daysAgoUnixDevel;
249
250
  if (shouldDeleteDevelBuild) {
250
- console.error(
251
+ logger.info(
251
252
  `[DELETE OLD BUILDS][${this.actorFullName}]: Removing olf devel build ${taggedDevelBuildNumber}.`,
252
253
  );
253
254
  }
@@ -256,7 +257,7 @@ export class ApifyBuilder {
256
257
  return build.startedAt.getTime() < daysAgoUnixProd;
257
258
  });
258
259
 
259
- console.error(
260
+ logger.info(
260
261
  `[DELETE OLD BUILDS][${this.actorFullName}]: Deleting ${buildsToDelete.length} old builds that are non-default and ` +
261
262
  `older than 30 days from total ${items.length}`,
262
263
  );
@@ -271,8 +272,8 @@ export const waitAndSummarizeBuilds = async (
271
272
  buildersMap: Map<string, ApifyBuilder>,
272
273
  label: string,
273
274
  ): Promise<BuildData[]> => {
274
- console.error('=========================================');
275
- console.error(`FINISHED ${label}:`);
275
+ logger.info('=========================================');
276
+ logger.info(`FINISHED ${label}:`);
276
277
  await Promise.all(
277
278
  startedBuilds.map(async (buildData) => {
278
279
  const builder = buildersMap.get(buildData.actorFullName)!;
@@ -280,12 +281,12 @@ export const waitAndSummarizeBuilds = async (
280
281
  }),
281
282
  );
282
283
 
283
- console.error('=========================================');
284
- console.error('SUMMARY:');
284
+ logger.info('=========================================');
285
+ logger.info('SUMMARY:');
285
286
  for (const buildData of startedBuilds.sort((a, b) => a.actorFullName.localeCompare(b.actorFullName))) {
286
- console.error(`[${buildData.actorFullName}]: ${buildData.buildNumber}`);
287
+ logger.info(`[${buildData.actorFullName}]: ${buildData.buildNumber}`);
287
288
  }
288
- console.error('=========================================');
289
+ logger.info('=========================================');
289
290
 
290
291
  return startedBuilds;
291
292
  };
@@ -298,8 +299,8 @@ export const runAndSummarizeBuilds = async (
298
299
  const buildersByActorFullName = new Map<string, ApifyBuilder>(
299
300
  actorConfigs.map((actorConfig) => [actorConfig.actorFullName, ApifyBuilder.fromActorConfig(actorConfig)]),
300
301
  );
301
- console.error('=========================================');
302
- console.error(`STARTED ${label}:`);
302
+ logger.info('=========================================');
303
+ logger.info(`STARTED ${label}:`);
303
304
  const startedBuilds = await Promise.all(
304
305
  actorConfigs.map(async (actorConfig) =>
305
306
  buildOneActor(actorConfig, buildersByActorFullName.get(actorConfig.actorFullName)!),
@@ -366,9 +367,9 @@ export const runBuilds = async ({
366
367
  );
367
368
 
368
369
  if (dryRun) {
369
- console.error('[DRY RUN] Would build:');
370
+ logger.info('[DRY RUN] Would build:');
370
371
  for (const { actorConfig, versionNumber } of buildConfigs) {
371
- console.error(` ${actorConfig.actorFullName} (${versionNumber})`);
372
+ logger.info(` ${actorConfig.actorFullName} (${versionNumber})`);
372
373
  }
373
374
  return buildConfigs.map(({ actorConfig, versionNumber }) =>
374
375
  dryRunBuildData(actorConfig.actorFullName, versionNumber),
@@ -1,5 +1,6 @@
1
1
  import { isCosmeticOnlyJsonSchemaChange } from './diff-json-schema.js';
2
2
  import { type DockerIgnoreMatcher, loadDockerIgnore } from './dockerignore.js';
3
+ import { logger } from './logger.js';
3
4
  import { findContainingScope, hoistPath, isPathWithinScope } from './path-utils.js';
4
5
  import type { ActorConfig, Commit } from './types.js';
5
6
 
@@ -44,12 +45,12 @@ type FileChangeForActor =
44
45
  * 6. .json inside the actor's own `.actor/` dir with only cosmetic schema diffs → cosmetic (semantically verified)
45
46
  * 7. Everything else → functional
46
47
  */
47
- const classifyFileChange = (
48
+ const classifyFileChange = async (
48
49
  originalFilePath: string,
49
50
  actorConfig: ActorConfig,
50
51
  commits: Commit[],
51
52
  dockerIgnoreMatcher: DockerIgnoreMatcher,
52
- ): FileChangeForActor => {
53
+ ): Promise<FileChangeForActor> => {
53
54
  const lowercaseFilePath = originalFilePath.toLowerCase();
54
55
 
55
56
  // TODO: hardcodes that there's a single repo-wide changelog belonging to every actor. Should instead
@@ -89,7 +90,7 @@ const classifyFileChange = (
89
90
  }
90
91
 
91
92
  if (lowercaseFilePath.endsWith('.json') && isUnderActorDotDir) {
92
- const isCosmetic = isCosmeticOnlyJsonSchemaChange(commits, originalFilePath);
93
+ const isCosmetic = await isCosmeticOnlyJsonSchemaChange(commits, originalFilePath);
93
94
  if (isCosmetic) {
94
95
  return { impact: 'cosmetic', semanticallyVerified: true };
95
96
  }
@@ -159,19 +160,19 @@ const groupFilesByActorSet = (fileToActors: Map<string, Set<string>>): ChangeGro
159
160
  const logChangeGroups = (groups: ChangeGroup[]): void => {
160
161
  for (const { actors, files } of groups) {
161
162
  if (actors.length > 1) {
162
- console.error(`[DIFF]: Shared changes for actors ${actors.join(', ')}: ${files.join(', ')}`);
163
+ logger.info(`[DIFF]: Shared changes for actors ${actors.join(', ')}: ${files.join(', ')}`);
163
164
  } else {
164
- console.error(`[DIFF]: Changes specific to actor ${actors[0]}: ${files.join(', ')}`);
165
+ logger.info(`[DIFF]: Changes specific to actor ${actors[0]}: ${files.join(', ')}`);
165
166
  }
166
167
  }
167
168
  };
168
169
 
169
- export const getChangedActors = ({
170
+ export const getChangedActors = async ({
170
171
  filepathsChanged,
171
172
  actorConfigs,
172
173
  isLatest = false,
173
174
  commits,
174
- }: ShouldBuildAndTestOptions): ActorConfig[] => {
175
+ }: ShouldBuildAndTestOptions): Promise<ActorConfig[]> => {
175
176
  const actorsChangedMap = new Map<string, ActorChangeEntry>();
176
177
 
177
178
  for (const actorConfig of actorConfigs) {
@@ -184,7 +185,7 @@ export const getChangedActors = ({
184
185
  continue;
185
186
  }
186
187
 
187
- const change = classifyFileChange(originalFilePath, actorConfig, commits, dockerIgnoreMatcher);
188
+ const change = await classifyFileChange(originalFilePath, actorConfig, commits, dockerIgnoreMatcher);
188
189
 
189
190
  if (change.impact === 'ignored' || change.impact === 'outside-context') continue;
190
191
  if (change.impact === 'cosmetic' && !isLatest) continue;
@@ -205,9 +206,9 @@ export const getChangedActors = ({
205
206
 
206
207
  if (actorsChanged.length > 0) {
207
208
  const actors = actorsChanged.map((config) => config.actorFullName);
208
- console.error(`[DIFF]: Actors to be built and tested: ${actors.join(', ')}`);
209
+ logger.info(`[DIFF]: Actors to be built and tested: ${actors.join(', ')}`);
209
210
  } else {
210
- console.error(`[DIFF]: No relevant files changed, skipping builds and tests`);
211
+ logger.info(`[DIFF]: No relevant files changed, skipping builds and tests`);
211
212
  }
212
213
 
213
214
  return actorsChanged;
@@ -1,5 +1,6 @@
1
+ import { logger } from './logger.js';
1
2
  import type { Commit } from './types.js';
2
- import { spawnCommand } from './utils.js';
3
+ import { runGitCommand } from './utils.js';
3
4
 
4
5
  const COSMETIC_JSON_FIELD_NAMES = new Set([
5
6
  'title',
@@ -28,21 +29,22 @@ const isCosmeticObjectChange = (oldVal: unknown, newVal: unknown, currentKey?: s
28
29
  * Returns true if the two JSON strings differ only in cosmetic fields
29
30
  * (title, description, example, enumTitles, sectionCaption, sectionDescription).
30
31
  */
31
- export const isCosmeticOnlyJsonSchemaChange = (commits: Commit[], changedFilepath: string): boolean => {
32
+ export const isCosmeticOnlyJsonSchemaChange = async (commits: Commit[], changedFilepath: string): Promise<boolean> => {
32
33
  // TODO: validate this is the right commit range
33
34
  const oldRef = `${commits[0].sha}~`;
34
35
  const newRef = commits[commits.length - 1].sha;
35
- let oldJson: unknown;
36
- let newJson: unknown;
37
- try {
38
- const oldContent = spawnCommand(`git show ${oldRef}:${changedFilepath}`);
39
- const newContent = spawnCommand(`git show ${newRef}:${changedFilepath}`);
40
36
 
41
- oldJson = JSON.parse(oldContent);
42
- newJson = JSON.parse(newContent);
43
- } catch {
44
- console.error(
45
- `Failed to get or parse JSON content for ${changedFilepath} at refs ${oldRef} and ${newRef}, maybe it is new file or deleted? Treating it as a non-cosmetic change.`,
37
+ const [oldJson, newJson] = await Promise.all([
38
+ runGitCommand(['show', `${oldRef}:${changedFilepath}`])
39
+ .then(JSON.parse)
40
+ .catch(() => undefined),
41
+ runGitCommand(['show', `${newRef}:${changedFilepath}`])
42
+ .then(JSON.parse)
43
+ .catch(() => undefined),
44
+ ]);
45
+ if (!oldJson || !newJson) {
46
+ logger.info(
47
+ `Failed to get JSON content for ${changedFilepath} at refs ${oldRef} and ${newRef}, maybe it is new file or deleted? Treating it as a non-cosmetic change.`,
46
48
  );
47
49
  return false;
48
50
  }