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.
- package/.claude-plugin/marketplace.json +12 -0
- package/.github/workflows/public_platform-tests-claude-investigate-and-fix.yaml +22 -10
- package/CHANGELOG.md +7 -0
- package/CONTRIBUTING.md +9 -0
- package/README.md +6 -7
- package/bin/build-from-local.ts +8 -7
- package/bin/build.ts +22 -21
- package/bin/diff-changes.ts +11 -10
- package/bin/diff-json-schema.ts +14 -12
- package/bin/git.ts +90 -43
- package/bin/logger.ts +23 -0
- package/bin/main.ts +49 -35
- package/bin/output.ts +6 -0
- package/bin/slack.ts +8 -7
- package/bin/test-report.ts +22 -21
- package/bin/utils/config/actor-json.ts +84 -0
- package/bin/utils/config/load-config.ts +46 -46
- package/bin/utils/config/parser.ts +33 -28
- package/bin/utils/config/structures/base.ts +7 -0
- package/bin/utils/json-file.ts +11 -14
- package/bin/utils/path/repo-relative.ts +156 -0
- package/bin/utils/path/schema.ts +18 -0
- package/bin/utils.ts +61 -34
- package/dist/bin/build-from-local.d.ts +1 -1
- package/dist/bin/build-from-local.d.ts.map +1 -1
- package/dist/bin/build-from-local.js +8 -7
- package/dist/bin/build-from-local.js.map +1 -1
- package/dist/bin/build.d.ts.map +1 -1
- package/dist/bin/build.js +22 -21
- package/dist/bin/build.js.map +1 -1
- package/dist/bin/diff-changes.d.ts +1 -1
- package/dist/bin/diff-changes.d.ts.map +1 -1
- package/dist/bin/diff-changes.js +9 -8
- package/dist/bin/diff-changes.js.map +1 -1
- package/dist/bin/diff-json-schema.d.ts +1 -1
- package/dist/bin/diff-json-schema.d.ts.map +1 -1
- package/dist/bin/diff-json-schema.js +13 -12
- package/dist/bin/diff-json-schema.js.map +1 -1
- package/dist/bin/git.d.ts +10 -10
- package/dist/bin/git.d.ts.map +1 -1
- package/dist/bin/git.js +78 -37
- package/dist/bin/git.js.map +1 -1
- package/dist/bin/logger.d.ts +11 -0
- package/dist/bin/logger.d.ts.map +1 -0
- package/dist/bin/logger.js +20 -0
- package/dist/bin/logger.js.map +1 -0
- package/dist/bin/main.d.ts +1 -1
- package/dist/bin/main.js +48 -35
- package/dist/bin/main.js.map +1 -1
- package/dist/bin/output.d.ts +3 -0
- package/dist/bin/output.d.ts.map +1 -0
- package/dist/bin/output.js +6 -0
- package/dist/bin/output.js.map +1 -0
- package/dist/bin/slack.d.ts.map +1 -1
- package/dist/bin/slack.js +8 -7
- package/dist/bin/slack.js.map +1 -1
- package/dist/bin/test-report.d.ts.map +1 -1
- package/dist/bin/test-report.js +22 -21
- package/dist/bin/test-report.js.map +1 -1
- package/dist/bin/utils/config/actor-json.d.ts +14 -0
- package/dist/bin/utils/config/actor-json.d.ts.map +1 -0
- package/dist/bin/utils/config/actor-json.js +60 -0
- package/dist/bin/utils/config/actor-json.js.map +1 -0
- package/dist/bin/utils/config/load-config.d.ts +12 -2
- package/dist/bin/utils/config/load-config.d.ts.map +1 -1
- package/dist/bin/utils/config/load-config.js +32 -31
- package/dist/bin/utils/config/load-config.js.map +1 -1
- package/dist/bin/utils/config/parser.d.ts +4 -12
- package/dist/bin/utils/config/parser.d.ts.map +1 -1
- package/dist/bin/utils/config/parser.js +24 -23
- package/dist/bin/utils/config/parser.js.map +1 -1
- package/dist/bin/utils/config/structures/base.d.ts +5 -0
- package/dist/bin/utils/config/structures/base.d.ts.map +1 -1
- package/dist/bin/utils/config/structures/base.js.map +1 -1
- package/dist/bin/utils/json-file.d.ts +3 -2
- package/dist/bin/utils/json-file.d.ts.map +1 -1
- package/dist/bin/utils/json-file.js +8 -13
- package/dist/bin/utils/json-file.js.map +1 -1
- package/dist/bin/utils/path/repo-relative.d.ts +35 -0
- package/dist/bin/utils/path/repo-relative.d.ts.map +1 -0
- package/dist/bin/utils/path/repo-relative.js +137 -0
- package/dist/bin/utils/path/repo-relative.js.map +1 -0
- package/dist/bin/utils/path/schema.d.ts +7 -0
- package/dist/bin/utils/path/schema.d.ts.map +1 -0
- package/dist/bin/utils/path/schema.js +16 -0
- package/dist/bin/utils/path/schema.js.map +1 -0
- package/dist/bin/utils.d.ts +14 -3
- package/dist/bin/utils.d.ts.map +1 -1
- package/dist/bin/utils.js +52 -29
- package/dist/bin/utils.js.map +1 -1
- package/dist/test/unit/bin/build-from-local.test.js +3 -39
- package/dist/test/unit/bin/build-from-local.test.js.map +1 -1
- package/dist/test/unit/bin/diff-changes.test.js +97 -96
- package/dist/test/unit/bin/diff-changes.test.js.map +1 -1
- package/dist/test/unit/bin/diff.test.js +52 -48
- package/dist/test/unit/bin/diff.test.js.map +1 -1
- package/dist/test/unit/bin/git-command.test.d.ts +2 -0
- package/dist/test/unit/bin/git-command.test.d.ts.map +1 -0
- package/dist/test/unit/bin/git-command.test.js +54 -0
- package/dist/test/unit/bin/git-command.test.js.map +1 -0
- package/dist/test/unit/bin/git.test.js +211 -92
- package/dist/test/unit/bin/git.test.js.map +1 -1
- package/dist/test/unit/bin/logger.test.d.ts +2 -0
- package/dist/test/unit/bin/logger.test.d.ts.map +1 -0
- package/dist/test/unit/bin/logger.test.js +44 -0
- package/dist/test/unit/bin/logger.test.js.map +1 -0
- package/dist/test/unit/bin/logging.test.d.ts +2 -0
- package/dist/test/unit/bin/logging.test.d.ts.map +1 -0
- package/dist/test/unit/bin/logging.test.js +43 -0
- package/dist/test/unit/bin/logging.test.js.map +1 -0
- package/dist/test/unit/bin/utils/config/load-config.test.js +47 -25
- package/dist/test/unit/bin/utils/config/load-config.test.js.map +1 -1
- package/dist/test/unit/bin/utils/config/parser.test.js +64 -19
- package/dist/test/unit/bin/utils/config/parser.test.js.map +1 -1
- package/dist/test/unit/bin/utils/files.test.js +16 -27
- package/dist/test/unit/bin/utils/files.test.js.map +1 -1
- package/dist/test/unit/bin/utils/path/repo-relative.test.d.ts +2 -0
- package/dist/test/unit/bin/utils/path/repo-relative.test.d.ts.map +1 -0
- package/dist/test/unit/bin/utils/path/repo-relative.test.js +196 -0
- package/dist/test/unit/bin/utils/path/repo-relative.test.js.map +1 -0
- package/dist/test/unit/bin/utils/path/schema.test.d.ts +2 -0
- package/dist/test/unit/bin/utils/path/schema.test.d.ts.map +1 -0
- package/dist/test/unit/bin/utils/path/schema.test.js +52 -0
- package/dist/test/unit/bin/utils/path/schema.test.js.map +1 -0
- package/dist/test/unit/should-built-and-test.test.js +40 -38
- package/dist/test/unit/should-built-and-test.test.js.map +1 -1
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/eslint.config.mjs +9 -1
- package/package.json +4 -4
- package/platform-tests-investigate-and-fix/.claude-plugin/plugin.json +4 -0
- package/platform-tests-investigate-and-fix/skills/measure-flakiness/SKILL.md +28 -0
- package/test/unit/bin/build-from-local.test.ts +3 -53
- package/test/unit/bin/diff-changes.test.ts +97 -96
- package/test/unit/bin/diff.test.ts +53 -48
- package/test/unit/bin/git-command.test.ts +71 -0
- package/test/unit/bin/git.test.ts +246 -89
- package/test/unit/bin/logger.test.ts +55 -0
- package/test/unit/bin/logging.test.ts +52 -0
- package/test/unit/bin/utils/config/load-config.test.ts +63 -26
- package/test/unit/bin/utils/config/parser.test.ts +81 -27
- package/test/unit/bin/utils/files.test.ts +19 -33
- package/test/unit/bin/utils/path/repo-relative.test.ts +250 -0
- package/test/unit/bin/utils/path/schema.test.ts +65 -0
- package/test/unit/should-built-and-test.test.ts +40 -38
|
@@ -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
|
-
|
|
74
|
+
TESTER_APIFY_TOKEN:
|
|
71
75
|
required: true
|
|
72
|
-
description: Apify token
|
|
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
|
-
|
|
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 ${
|
|
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:
|
|
170
|
-
uses: actions/checkout@
|
|
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'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
|
|
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
|
package/bin/build-from-local.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
198
|
+
logger.info('[DRY RUN] Would build from local source:');
|
|
198
199
|
for (const { actorFullName, folder } of actorConfigs) {
|
|
199
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
155
|
+
logger.error(`\n--- BUILD LOG (last 40 lines) ---\n${logTail}\n---`);
|
|
155
156
|
} catch (err) {
|
|
156
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
275
|
-
|
|
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
|
-
|
|
284
|
-
|
|
284
|
+
logger.info('=========================================');
|
|
285
|
+
logger.info('SUMMARY:');
|
|
285
286
|
for (const buildData of startedBuilds.sort((a, b) => a.actorFullName.localeCompare(b.actorFullName))) {
|
|
286
|
-
|
|
287
|
+
logger.info(`[${buildData.actorFullName}]: ${buildData.buildNumber}`);
|
|
287
288
|
}
|
|
288
|
-
|
|
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
|
-
|
|
302
|
-
|
|
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
|
-
|
|
370
|
+
logger.info('[DRY RUN] Would build:');
|
|
370
371
|
for (const { actorConfig, versionNumber } of buildConfigs) {
|
|
371
|
-
|
|
372
|
+
logger.info(` ${actorConfig.actorFullName} (${versionNumber})`);
|
|
372
373
|
}
|
|
373
374
|
return buildConfigs.map(({ actorConfig, versionNumber }) =>
|
|
374
375
|
dryRunBuildData(actorConfig.actorFullName, versionNumber),
|
package/bin/diff-changes.ts
CHANGED
|
@@ -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
|
-
|
|
163
|
+
logger.info(`[DIFF]: Shared changes for actors ${actors.join(', ')}: ${files.join(', ')}`);
|
|
163
164
|
} else {
|
|
164
|
-
|
|
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
|
-
|
|
209
|
+
logger.info(`[DIFF]: Actors to be built and tested: ${actors.join(', ')}`);
|
|
209
210
|
} else {
|
|
210
|
-
|
|
211
|
+
logger.info(`[DIFF]: No relevant files changed, skipping builds and tests`);
|
|
211
212
|
}
|
|
212
213
|
|
|
213
214
|
return actorsChanged;
|
package/bin/diff-json-schema.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
|
+
import { logger } from './logger.js';
|
|
1
2
|
import type { Commit } from './types.js';
|
|
2
|
-
import {
|
|
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
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
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
|
}
|