apify-test-tools 0.10.2-beta.1 → 0.10.2-beta.3
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/.gitattributes +2 -0
- package/.github/actions/checkout-restore-dependencies/action.yaml +13 -29
- package/.github/cli/apify-test-tools.mjs +361 -0
- package/.github/scripts/run-with-apify-tokens.mjs +2 -2
- package/.github/workflows/_check_code.yaml +25 -0
- package/.github/workflows/_move_major_tag.yaml +3 -81
- package/.github/workflows/_tests.yaml +34 -0
- package/.github/workflows/_update_release_metadata.yaml +0 -12
- package/.github/workflows/manual_move_major_tag.yaml +3 -3
- package/.github/workflows/manual_release_stable.yaml +3 -13
- package/.github/workflows/on_master.yaml +9 -5
- package/.github/workflows/on_pull_request.yaml +3 -48
- package/.github/workflows/public_platform-tests.yaml +1 -1
- package/.github/workflows/public_pr-build-test.yaml +2 -2
- package/.github/workflows/public_push-build-latest.yaml +2 -2
- package/.husky/pre-commit +5 -0
- package/.prettierignore +3 -0
- package/CHANGELOG.md +5 -0
- package/CONTRIBUTING.md +8 -18
- package/README.md +94 -44
- package/bin/affected-actors.ts +166 -0
- package/bin/build-from-local.ts +47 -13
- package/bin/build.ts +35 -19
- package/bin/diff-changes.ts +25 -17
- package/bin/git.ts +52 -41
- package/bin/main.ts +63 -78
- package/bin/slack.ts +28 -6
- package/bin/test-report.ts +6 -9
- package/bin/types.ts +9 -0
- package/dist/bin/affected-actors.d.ts +50 -0
- package/dist/bin/affected-actors.d.ts.map +1 -0
- package/dist/bin/affected-actors.js +89 -0
- package/dist/bin/affected-actors.js.map +1 -0
- package/dist/bin/build-from-local.d.ts +10 -2
- package/dist/bin/build-from-local.d.ts.map +1 -1
- package/dist/bin/build-from-local.js +29 -11
- package/dist/bin/build-from-local.js.map +1 -1
- package/dist/bin/build.d.ts +15 -5
- package/dist/bin/build.d.ts.map +1 -1
- package/dist/bin/build.js +24 -15
- package/dist/bin/build.js.map +1 -1
- package/dist/bin/diff-changes.d.ts +8 -2
- package/dist/bin/diff-changes.d.ts.map +1 -1
- package/dist/bin/diff-changes.js +16 -16
- package/dist/bin/diff-changes.js.map +1 -1
- package/dist/bin/git.d.ts +23 -13
- package/dist/bin/git.d.ts.map +1 -1
- package/dist/bin/git.js +44 -32
- package/dist/bin/git.js.map +1 -1
- package/dist/bin/main.js +57 -57
- package/dist/bin/main.js.map +1 -1
- package/dist/bin/slack.d.ts +8 -1
- package/dist/bin/slack.d.ts.map +1 -1
- package/dist/bin/slack.js +26 -5
- package/dist/bin/slack.js.map +1 -1
- package/dist/bin/test-report.d.ts.map +1 -1
- package/dist/bin/test-report.js +6 -8
- package/dist/bin/test-report.js.map +1 -1
- package/dist/bin/types.d.ts +8 -0
- package/dist/bin/types.d.ts.map +1 -1
- package/dist/test/bundle/cli-bundle.test.d.ts +2 -0
- package/dist/test/bundle/cli-bundle.test.d.ts.map +1 -0
- package/dist/test/bundle/cli-bundle.test.js +169 -0
- package/dist/test/bundle/cli-bundle.test.js.map +1 -0
- package/dist/test/e2e/cli-bundle.e2e.test.d.ts +2 -0
- package/dist/test/e2e/cli-bundle.e2e.test.d.ts.map +1 -0
- package/dist/test/e2e/cli-bundle.e2e.test.js +172 -0
- package/dist/test/e2e/cli-bundle.e2e.test.js.map +1 -0
- package/dist/test/unit/bin/affected-actors.test.d.ts +2 -0
- package/dist/test/unit/bin/affected-actors.test.d.ts.map +1 -0
- package/dist/test/unit/bin/affected-actors.test.js +160 -0
- package/dist/test/unit/bin/affected-actors.test.js.map +1 -0
- package/dist/test/unit/bin/build-from-local.test.js +53 -1
- package/dist/test/unit/bin/build-from-local.test.js.map +1 -1
- package/dist/test/unit/bin/build.test.js +113 -3
- package/dist/test/unit/bin/build.test.js.map +1 -1
- package/dist/test/unit/bin/diff-changes.test.js +78 -49
- package/dist/test/unit/bin/diff-changes.test.js.map +1 -1
- package/dist/test/unit/bin/git.test.js +50 -53
- package/dist/test/unit/bin/git.test.js.map +1 -1
- package/dist/test/unit/bin/slack.test.d.ts +2 -0
- package/dist/test/unit/bin/slack.test.d.ts.map +1 -0
- package/dist/test/unit/bin/slack.test.js +54 -0
- package/dist/test/unit/bin/slack.test.js.map +1 -0
- package/dist/test/unit/should-built-and-test.test.js +23 -17
- package/dist/test/unit/should-built-and-test.test.js.map +1 -1
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/package.json +13 -3
- package/test/bundle/cli-bundle.test.ts +199 -0
- package/test/e2e/cli-bundle.e2e.test.ts +237 -0
- package/test/unit/bin/affected-actors.test.ts +190 -0
- package/test/unit/bin/build-from-local.test.ts +55 -0
- package/test/unit/bin/build.test.ts +135 -3
- package/test/unit/bin/diff-changes.test.ts +87 -49
- package/test/unit/bin/git.test.ts +56 -53
- package/test/unit/bin/slack.test.ts +65 -0
- package/test/unit/should-built-and-test.test.ts +27 -17
- package/.github/scripts/check-package-version-bump.mjs +0 -74
- package/.github/workflows-package-version +0 -1
package/README.md
CHANGED
|
@@ -308,48 +308,35 @@ Slack credentials in scope, so a postinstall script in the dependency tree canno
|
|
|
308
308
|
|
|
309
309
|
## Versioning and releases
|
|
310
310
|
|
|
311
|
-
The workflows and the npm package live in one repo but ship on their own schedules
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
|
315
|
-
|
|
|
316
|
-
| the
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
- **A workflow that calls a new CLI feature** — the one case that can break consumers, and the only
|
|
334
|
-
one with any ceremony. Put the package change, the workflow change, and the new pin in one PR. On
|
|
335
|
-
merge the tag is **held**: the CI job reports that the pinned version isn't on npm and leaves `workflows-v0`
|
|
336
|
-
where it is, so consumers keep running the previous workflows. Cut a stable release, and the tag
|
|
337
|
-
moves on its own. Run **Move major version tag** if you don't want to wait for the next master
|
|
338
|
-
push.
|
|
311
|
+
The workflows and the npm package live in one repo but ship on their own schedules:
|
|
312
|
+
|
|
313
|
+
| What | Consumers get it through | Ships when |
|
|
314
|
+
| -------------------------------------------- | ------------------------------------------ | ----------------------- |
|
|
315
|
+
| the workflows and the CLI they call | the `@workflows-v0` tag in `uses:` | every master push |
|
|
316
|
+
| the lib (`testActor`, `describe`, matchers…) | `apify-test-tools` in their `package.json` | a stable release on npm |
|
|
317
|
+
|
|
318
|
+
The CLI is bundled with its dependencies into `.github/cli/apify-test-tools.mjs` (`npm run bundle`,
|
|
319
|
+
run by the pre-commit hook and checked in CI), and the setup action runs it from there. Nothing is
|
|
320
|
+
installed for it, so a tag is a complete statement: these workflows and this CLI, with the exact
|
|
321
|
+
dependency versions from `package-lock.json` at that commit. The consumer's own `node_modules` is
|
|
322
|
+
left alone, so their tests keep the lib version they locked.
|
|
323
|
+
|
|
324
|
+
- **Workflow or CLI change** — merge it. `workflows-v0` moves once checks and tests pass, and it goes
|
|
325
|
+
live. No release needed, even when a workflow starts using a new CLI feature: both are in the
|
|
326
|
+
same commit.
|
|
327
|
+
- **Lib change** — merge it, then cut a release when you want it out. Consumers pick it up when
|
|
328
|
+
they update `apify-test-tools`.
|
|
329
|
+
|
|
330
|
+
The CLI and the lib in one job can be different versions now, so the formats between them are a
|
|
331
|
+
contract: the `ACTOR_BUILDS` JSON that `build` writes and the lib reads, and the vitest JSON report
|
|
332
|
+
`report-tests` reads. Add fields freely; renaming or removing one needs a new major tag.
|
|
339
333
|
|
|
340
334
|
`workflows-v0` moving on every master push means `@workflows-v0` is as live as `@master` was —
|
|
341
|
-
there's no staging step
|
|
342
|
-
|
|
343
|
-
|
|
335
|
+
there's no staging step. What the tag buys you is a `workflows-v1` for breaking workflow changes,
|
|
336
|
+
so repos migrate one at a time instead of all at once, and a way to roll back by pointing the tag at
|
|
337
|
+
an earlier commit (**Move major version tag** workflow). Bump `MAJOR_TAG` in
|
|
344
338
|
`.github/workflows/_move_major_tag.yaml` to cut the next major; the old tag then freezes where it is
|
|
345
|
-
and keeps working.
|
|
346
|
-
|
|
347
|
-
Freezing is real, which is the whole reason the version is pinned rather than a floor. A frozen `workflows-v0`
|
|
348
|
-
points at a commit whose pinned version never changes, so it keeps installing the library it was
|
|
349
|
-
tested against no matter how far the package moves on. Had the workflows installed `>=<floor>`, a
|
|
350
|
-
frozen `workflows-v0` would still resolve to whatever is newest — and since the release that enables
|
|
351
|
-
`workflows-v1` is usually the same release that breaks `workflows-v0`, the migration window would
|
|
352
|
-
have been zero.
|
|
339
|
+
and keeps working, CLI included, since the bundle is part of the frozen commit.
|
|
353
340
|
|
|
354
341
|
The tag tracks the **workflows'** contract, not the npm package version. They move independently on
|
|
355
342
|
purpose, so `@workflows-v0` is expected to stay `@workflows-v0` after the package reaches 1.0 —
|
|
@@ -360,9 +347,8 @@ and those refs disagree, which is the mistake that would otherwise ship silently
|
|
|
360
347
|
### Testing workflow changes
|
|
361
348
|
|
|
362
349
|
- Point [testing-repo-for-github-actions](https://github.com/apify-store/testing-repo-for-github-actions)
|
|
363
|
-
at your branch (`uses: ...@your-branch`). It has real attached Actors and tests.
|
|
364
|
-
|
|
365
|
-
action installs that exact version — so one branch tests both halves of a change together.
|
|
350
|
+
at your branch (`uses: ...@your-branch`). It has real attached Actors and tests. The branch carries
|
|
351
|
+
its own CLI bundle, so one branch tests the workflows and the CLI together.
|
|
366
352
|
- To change the composite action itself, repoint the `uses:` refs inside the reusable workflows at
|
|
367
353
|
your branch as well, and change them back before merging.
|
|
368
354
|
- Make sure the shell code actually works on your laptop first.
|
|
@@ -580,6 +566,15 @@ describe('standby tests', () => {
|
|
|
580
566
|
|
|
581
567
|
The package includes a CLI binary used by CI workflows to build Actors, detect changes, and report test results. You can also run it locally.
|
|
582
568
|
|
|
569
|
+
### Dry runs (`--dry-run`)
|
|
570
|
+
|
|
571
|
+
Every command that changes something (`build`, `build-from-local`, `release`, `delete-old-builds`, `report-tests`) takes `--dry-run`. A dry run:
|
|
572
|
+
|
|
573
|
+
- **Changes nothing.** No versions or builds are created, nothing is uploaded or deleted, and no Slack message is posted.
|
|
574
|
+
- **Does every read and check the real run does**, so it fails wherever the real run would: change detection, the Apify token and Actor lookup, the [repository check](#repository-check), collecting and zipping local sources, and the Slack token (verified with Slack's read-only `auth.test`).
|
|
575
|
+
- **Needs the same tokens as the real run.** Read-only tokens are enough, since nothing is written. A dry run can't tell whether a token is also allowed to write, because the Apify API has no dry-run mode of its own.
|
|
576
|
+
- **Prints a plan, not results.** Logs start with `[DRY RUN] Would …`. `build` and `build-from-local` write `{ "dryRun": true, "builds": [...] }` to stdout instead of the build array, so the plan can't be passed to tests as `ACTOR_BUILDS` by mistake.
|
|
577
|
+
|
|
583
578
|
### Running locally
|
|
584
579
|
|
|
585
580
|
Running the testing library locally is useful when you only want to update the testing code in /test because you can iterate on it without pushing new code to the remote.
|
|
@@ -610,7 +605,7 @@ APIFY_TOKEN_JOHN_DOE=<token> \
|
|
|
610
605
|
--dry-run
|
|
611
606
|
```
|
|
612
607
|
|
|
613
|
-
Remove `--dry-run` to actually trigger builds and update the branch names
|
|
608
|
+
With `--dry-run`, it prints the plan (the version and Git URL each Actor would be built from). Remove `--dry-run` to actually trigger builds and update the branch names. The command outputs a JSON array of build objects to stdout:
|
|
614
609
|
|
|
615
610
|
```json
|
|
616
611
|
[{ "buildId": "...", "actorRawId": "...", "buildNumber": "...", "actorFullName": "john.doe/my-actor" }]
|
|
@@ -625,7 +620,7 @@ APIFY_TOKEN_JOHN_DOE=<token> \
|
|
|
625
620
|
npx apify-test-tools build-from-local --actors john.doe/my-actor
|
|
626
621
|
```
|
|
627
622
|
|
|
628
|
-
Pass a hardcoded actor name via `--actors` to build only that Actor (comma-separate multiple names). Omit `--actors` to build all Actors in the repo, or add `--dry-run` to
|
|
623
|
+
Pass a hardcoded actor name via `--actors` to build only that Actor (comma-separate multiple names). Omit `--actors` to build all Actors in the repo, or add `--dry-run` to check the token, the Actor and the zipped sources without uploading or building. It outputs the same JSON build array as `build`, so you run tests against it the same way as in step 5 below:
|
|
629
624
|
|
|
630
625
|
```bash
|
|
631
626
|
# Build from local source and capture output
|
|
@@ -665,6 +660,61 @@ TESTER_APIFY_TOKEN=apify_api_yyy \
|
|
|
665
660
|
npx vitest --run --maxConcurrency 20 --fileParallelism=true --maxWorkers 100 test/platform
|
|
666
661
|
```
|
|
667
662
|
|
|
663
|
+
### Finding affected Actors (`get-affected-actors`)
|
|
664
|
+
|
|
665
|
+
`get-affected-actors` lists the Actors whose context changed in a commit range, using the rules in
|
|
666
|
+
[Change detection](#change-detection). It only reads git and the config file, so it needs no tokens.
|
|
667
|
+
The range always ends at the checked-out `HEAD`, so check out the branch you want to inspect first.
|
|
668
|
+
|
|
669
|
+
```bash
|
|
670
|
+
# A PR: the branch's own commits, after its last validated commit
|
|
671
|
+
npx apify-test-tools get-affected-actors --base origin/master --since <sha>
|
|
672
|
+
|
|
673
|
+
# A release: everything after the last released commit
|
|
674
|
+
npx apify-test-tools get-affected-actors --since <sha>
|
|
675
|
+
```
|
|
676
|
+
|
|
677
|
+
| Flag | Default | Meaning |
|
|
678
|
+
| ------------------- | ------- | ------------------------------------------------------------------------------------------------------------------------------------------ |
|
|
679
|
+
| `--base` | | Only commits on `HEAD` that are not on this branch count |
|
|
680
|
+
| `--since` | | Only commits after this one count (see below) |
|
|
681
|
+
| `--test-files-glob` | | Test files (glob from the repo root) that count as changes even when `.dockerignore` lists them, see [Change detection](#change-detection) |
|
|
682
|
+
| `--actors` | all | Only report these Actors |
|
|
683
|
+
| `--ignore` | none | Never report these Actors |
|
|
684
|
+
|
|
685
|
+
Pass `--base`, `--since`, or both:
|
|
686
|
+
|
|
687
|
+
- **`--base` only** inspects the whole branch. If the branch merged `--base` in and has no functional changes
|
|
688
|
+
of its own, no Actor is reported as functional, since `--base` is already validated (and none as cosmetic
|
|
689
|
+
either, if the branch has no changes of its own at all).
|
|
690
|
+
- **`--base` and `--since`** inspects the branch after its last validated commit. If `--since` is not on the
|
|
691
|
+
branch (force-push), or it is the head (a rerun), the whole branch is inspected instead.
|
|
692
|
+
- **`--since` only** inspects everything after it up to `HEAD`. It must be an ancestor of `HEAD` and fails
|
|
693
|
+
otherwise, exactly like [`--base-commit` of `release`](#--base-commit). If `HEAD` is `--since`, nothing is
|
|
694
|
+
reported.
|
|
695
|
+
|
|
696
|
+
The output is a JSON document:
|
|
697
|
+
|
|
698
|
+
```json
|
|
699
|
+
{
|
|
700
|
+
"version": 1,
|
|
701
|
+
"ref": "my-branch",
|
|
702
|
+
"head": "<sha of HEAD>",
|
|
703
|
+
"commits": [{ "sha": "…", "author": "…", "date": "…", "message": "…" }],
|
|
704
|
+
"changedFiles": ["src/main.ts", "CHANGELOG.md"],
|
|
705
|
+
"changelog": "Lines added to CHANGELOG.md, or null",
|
|
706
|
+
"functionallyChangedActors": [{ "actorFullName": "john.doe/my-actor" }],
|
|
707
|
+
"cosmeticallyChangedActors": [{ "actorFullName": "john.doe/other-actor" }]
|
|
708
|
+
}
|
|
709
|
+
```
|
|
710
|
+
|
|
711
|
+
Each changed Actor is in exactly one of the lists. `functionallyChangedActors` have at least one functional change, so
|
|
712
|
+
they need a new build and tests. `cosmeticallyChangedActors` only have cosmetic changes (see
|
|
713
|
+
[Change detection](#change-detection)), so they need a new build only when released: `build` builds
|
|
714
|
+
`functionallyChangedActors`, `release` builds both.
|
|
715
|
+
|
|
716
|
+
`ref` is the checked-out branch, or `null` for a detached HEAD. `build` and `release` use the same detection internally.
|
|
717
|
+
|
|
668
718
|
### Releasing Actors (`release`)
|
|
669
719
|
|
|
670
720
|
`release` builds the `latest` (default) version of every Actor changed since the last release and
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
import { type ChangedActors, getChangedActors } from './diff-changes.js';
|
|
2
|
+
import {
|
|
3
|
+
getBranchOnlyChangedFiles,
|
|
4
|
+
getChangedFilesBetween,
|
|
5
|
+
getChangelogAdditions,
|
|
6
|
+
getCommits,
|
|
7
|
+
getCommitsBetween,
|
|
8
|
+
hasMergeFromTarget,
|
|
9
|
+
resolveBranchName,
|
|
10
|
+
resolveCommitSha,
|
|
11
|
+
resolveReleaseBaseCommit,
|
|
12
|
+
} from './git.js';
|
|
13
|
+
import { logger } from './logger.js';
|
|
14
|
+
import type { ActorConfig, Commit } from './types.js';
|
|
15
|
+
import { readConfigFile } from './utils/config/load-config.js';
|
|
16
|
+
|
|
17
|
+
interface AffectedActorsOptions {
|
|
18
|
+
/** End of the range. */
|
|
19
|
+
head: string;
|
|
20
|
+
/** Only the commits on head that are not on base count, i.e. the branch's own changes. */
|
|
21
|
+
base?: string;
|
|
22
|
+
/**
|
|
23
|
+
* Only changes after this commit count. With `base` it's the last validated commit of the branch and is
|
|
24
|
+
* ignored when it isn't on the branch. Without `base` it must be an ancestor of head (e.g. the last release).
|
|
25
|
+
*/
|
|
26
|
+
since?: string;
|
|
27
|
+
/** Glob relative to the repo root. Matching files count as changes even when .dockerignore lists them. */
|
|
28
|
+
testFilesGlob?: string;
|
|
29
|
+
actors: string[];
|
|
30
|
+
ignore: string[];
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
interface AffectedActors {
|
|
34
|
+
headSha: string;
|
|
35
|
+
/** Branch head points to, which is what git builds are made from. Null for a SHA or a detached HEAD. */
|
|
36
|
+
ref: string | null;
|
|
37
|
+
commits: Commit[];
|
|
38
|
+
changedFiles: string[];
|
|
39
|
+
changelog: string | null;
|
|
40
|
+
/** Actors with at least one functional change. They need a new build and tests. */
|
|
41
|
+
functionallyChangedActors: ActorConfig[];
|
|
42
|
+
/** Actors whose changes are all cosmetic (README, CHANGELOG, cosmetic .actor JSON). They only need a release build. */
|
|
43
|
+
cosmeticallyChangedActors: ActorConfig[];
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
interface CommitRange {
|
|
47
|
+
commits: Commit[];
|
|
48
|
+
fromRef: string;
|
|
49
|
+
toRef: string;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/** The branch's own commits, after `since` when it is on the branch (see getCommits). */
|
|
53
|
+
const getBranchRange = async (head: string, base: string, since: string | undefined): Promise<CommitRange> => {
|
|
54
|
+
const commits = await getCommits({ sourceBranch: head, targetBranch: base, baseCommit: since });
|
|
55
|
+
return { commits, fromRef: `${commits[0].sha}~`, toRef: commits[commits.length - 1].sha };
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
/** Everything after `since` up to head. Unlike on a branch, `since` must be an ancestor of head. */
|
|
59
|
+
const getRangeSince = async (head: string, headSha: string, since: string): Promise<CommitRange> => {
|
|
60
|
+
const sinceSha = await resolveReleaseBaseCommit(since, head);
|
|
61
|
+
const commits = sinceSha === headSha ? [] : await getCommitsBetween(sinceSha, headSha);
|
|
62
|
+
return { commits, fromRef: sinceSha, toRef: headSha };
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* This is an optimization for the common case where a branch only has cosmetic changes but had to merge in
|
|
67
|
+
* functional changes from base (being up-to-date is a CI requirement). Base is already validated, and
|
|
68
|
+
* since the branch has no functional changes of its own, there is nothing new to validate.
|
|
69
|
+
* Exception: if the branch has any functional changes alongside the merge, we must re-test — even
|
|
70
|
+
* individually validated changes can have novel interactions when combined.
|
|
71
|
+
* Returns the Actors the branch's own commits changed, or undefined when it didn't merge base in.
|
|
72
|
+
*
|
|
73
|
+
* TODO: Apply this per Actor instead of to the whole branch. Changes merged in from base only interact with the
|
|
74
|
+
* branch's own changes in Actors whose context both touch, so an Actor that only base changed is already validated
|
|
75
|
+
* and can be skipped even when the branch changes other Actors functionally. Today any functional change of the
|
|
76
|
+
* branch's own keeps every Actor, and the log messages below describe the whole branch.
|
|
77
|
+
*/
|
|
78
|
+
const getOwnChangesIfMergedFromBase = async (
|
|
79
|
+
head: string,
|
|
80
|
+
base: string,
|
|
81
|
+
actorConfigs: ActorConfig[],
|
|
82
|
+
testFilesGlob: string | undefined,
|
|
83
|
+
): Promise<ChangedActors | undefined> => {
|
|
84
|
+
if (!(await hasMergeFromTarget(head, base))) return undefined;
|
|
85
|
+
|
|
86
|
+
logger.info(
|
|
87
|
+
'[MERGE-FROM-TARGET-OPTIMIZATION]: There is merge from target branch, checking if there are no functional changes in our own branch. If so, we can skip tests',
|
|
88
|
+
);
|
|
89
|
+
const own = await getChangedActors({
|
|
90
|
+
filepathsChanged: await getBranchOnlyChangedFiles(head, base),
|
|
91
|
+
actorConfigs,
|
|
92
|
+
// Omit since to get full branch history. Validated functional commits can still interact with merged ones
|
|
93
|
+
commits: await getCommits({ sourceBranch: head, targetBranch: base }),
|
|
94
|
+
testFilesGlob,
|
|
95
|
+
});
|
|
96
|
+
logger.info(
|
|
97
|
+
own.functional.length > 0
|
|
98
|
+
? `[MERGE-FROM-TARGET-OPTIMIZATION]: Branch has ${own.functional.length} functional changes, cannot optimize, we continue with full check`
|
|
99
|
+
: '[MERGE-FROM-TARGET-OPTIMIZATION]: Branch itself has no functional changes, skipping tests',
|
|
100
|
+
);
|
|
101
|
+
return own;
|
|
102
|
+
};
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Finds the Actors whose context changed in the given range. `base` selects the branch's own commits
|
|
106
|
+
* (a PR), `since` the commits after a known one (a release, or the last validated commit of a PR).
|
|
107
|
+
*/
|
|
108
|
+
export const getAffectedActors = async ({
|
|
109
|
+
head,
|
|
110
|
+
base,
|
|
111
|
+
since: sinceOption,
|
|
112
|
+
testFilesGlob,
|
|
113
|
+
actors,
|
|
114
|
+
ignore,
|
|
115
|
+
}: AffectedActorsOptions): Promise<AffectedActors> => {
|
|
116
|
+
// An empty --since (e.g. an empty cache file) means the same as not passing it
|
|
117
|
+
const since = sinceOption || undefined;
|
|
118
|
+
if (!base && !since) {
|
|
119
|
+
throw new Error('Pass --base, --since, or both to select the commits to inspect.');
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
const headSha = await resolveCommitSha(head);
|
|
123
|
+
const { commits, fromRef, toRef } = base
|
|
124
|
+
? await getBranchRange(head, base, since)
|
|
125
|
+
: await getRangeSince(head, headSha, since!);
|
|
126
|
+
const actorConfigs = await readConfigFile({ actors, ignore });
|
|
127
|
+
|
|
128
|
+
// What the branch only merged in from base is already validated. Without functional changes of its own
|
|
129
|
+
// the branch has nothing to test, and without any changes of its own nothing to release either.
|
|
130
|
+
const own = base ? await getOwnChangesIfMergedFromBase(head, base, actorConfigs, testFilesGlob) : undefined;
|
|
131
|
+
const changedFiles = await getChangedFilesBetween(fromRef, toRef);
|
|
132
|
+
const changed = await getChangedActors({ filepathsChanged: changedFiles, actorConfigs, commits, testFilesGlob });
|
|
133
|
+
|
|
134
|
+
return {
|
|
135
|
+
headSha,
|
|
136
|
+
ref: await resolveBranchName(head),
|
|
137
|
+
commits,
|
|
138
|
+
changedFiles,
|
|
139
|
+
changelog: await getChangelogAdditions(fromRef, toRef, changedFiles),
|
|
140
|
+
functionallyChangedActors: own && own.functional.length === 0 ? [] : changed.functional,
|
|
141
|
+
cosmeticallyChangedActors: own && own.functional.length + own.cosmetic.length === 0 ? [] : changed.cosmetic,
|
|
142
|
+
};
|
|
143
|
+
};
|
|
144
|
+
|
|
145
|
+
/** The JSON document `get-affected-actors` prints. Versioned, since other commands and workflows consume it. */
|
|
146
|
+
export const toAffectedActorsDocument = ({
|
|
147
|
+
headSha,
|
|
148
|
+
ref,
|
|
149
|
+
commits,
|
|
150
|
+
changedFiles,
|
|
151
|
+
changelog,
|
|
152
|
+
functionallyChangedActors,
|
|
153
|
+
cosmeticallyChangedActors,
|
|
154
|
+
}: AffectedActors) => {
|
|
155
|
+
const names = (configs: ActorConfig[]) => configs.map(({ actorFullName }) => ({ actorFullName }));
|
|
156
|
+
return {
|
|
157
|
+
version: 1,
|
|
158
|
+
ref,
|
|
159
|
+
head: headSha,
|
|
160
|
+
commits,
|
|
161
|
+
changedFiles,
|
|
162
|
+
changelog,
|
|
163
|
+
functionallyChangedActors: names(functionallyChangedActors),
|
|
164
|
+
cosmeticallyChangedActors: names(cosmeticallyChangedActors),
|
|
165
|
+
};
|
|
166
|
+
};
|
package/bin/build-from-local.ts
CHANGED
|
@@ -6,11 +6,11 @@ import AdmZip from 'adm-zip';
|
|
|
6
6
|
import type * as ApifyClientTypes from 'apify-client';
|
|
7
7
|
import { ActorSourceType } from 'apify-client';
|
|
8
8
|
|
|
9
|
-
import {
|
|
9
|
+
import { ApifyBuilder, LOCAL_SOURCE_VERSION_NUMBER, runAndSummarizeBuilds } from './build.js';
|
|
10
10
|
import { loadDockerIgnore } from './dockerignore.js';
|
|
11
11
|
import { logger } from './logger.js';
|
|
12
12
|
import { isPathWithinScope } from './path-utils.js';
|
|
13
|
-
import type { ActorConfig, BuildData } from './types.js';
|
|
13
|
+
import type { ActorConfig, BuildData, DryRunPlan } from './types.js';
|
|
14
14
|
import type { SourceFile } from './utils.js';
|
|
15
15
|
import { getGitignoredPaths, isOutsideDir, listRepoFilePaths, readSourceFile } from './utils.js';
|
|
16
16
|
import type { RelativeDir } from './utils/path/repo-relative.js';
|
|
@@ -179,32 +179,66 @@ export const rewriteActorJsonPaths = async (
|
|
|
179
179
|
await fs.writeFile(path.join(newActorDir, 'actor.json'), JSON.stringify(rewritten, null, 4));
|
|
180
180
|
};
|
|
181
181
|
|
|
182
|
+
type LocalBuildPlan = {
|
|
183
|
+
actorFullName: string;
|
|
184
|
+
versionNumber: string;
|
|
185
|
+
folder: string;
|
|
186
|
+
fileCount: number;
|
|
187
|
+
zipSizeBytes: number;
|
|
188
|
+
};
|
|
189
|
+
|
|
182
190
|
export const runBuildsFromLocal = async ({
|
|
183
191
|
actorConfigs,
|
|
184
192
|
dryRun,
|
|
185
193
|
}: {
|
|
186
194
|
actorConfigs: ActorConfig[];
|
|
187
195
|
dryRun: boolean;
|
|
188
|
-
}): Promise<BuildData[]
|
|
196
|
+
}): Promise<BuildData[] | DryRunPlan<LocalBuildPlan>> => {
|
|
197
|
+
// Read-only preparation shared with the real build, so a dry run fails on a missing token, an unknown
|
|
198
|
+
// Actor or unreadable sources just like the build would
|
|
199
|
+
const preparedBuilds = await Promise.all(
|
|
200
|
+
actorConfigs.map(async (actorConfig) => {
|
|
201
|
+
const actorInfo = await ApifyBuilder.fromActorConfig(actorConfig).getActorInfo();
|
|
202
|
+
// Zipped and uploaded like `apify push` does, since inline SOURCE_FILES are capped at ~9 MB.
|
|
203
|
+
const zip = new AdmZip();
|
|
204
|
+
const sourceFiles = await collectSourceFiles(actorConfig);
|
|
205
|
+
for (const { name, content } of sourceFiles) {
|
|
206
|
+
zip.addFile(name, content);
|
|
207
|
+
}
|
|
208
|
+
return { actorConfig, actorInfo, fileCount: sourceFiles.length, zipBuffer: zip.toBuffer() };
|
|
209
|
+
}),
|
|
210
|
+
);
|
|
211
|
+
|
|
189
212
|
if (dryRun) {
|
|
190
213
|
logger.info('[DRY RUN] Would build from local source:');
|
|
191
|
-
for (const {
|
|
192
|
-
logger.info(
|
|
214
|
+
for (const { actorConfig, fileCount, zipBuffer } of preparedBuilds) {
|
|
215
|
+
logger.info(
|
|
216
|
+
` ${actorConfig.actorFullName} (${actorConfig.folder}): ${fileCount} files, ${zipBuffer.length} bytes zipped`,
|
|
217
|
+
);
|
|
193
218
|
}
|
|
194
|
-
return
|
|
219
|
+
return {
|
|
220
|
+
dryRun: true,
|
|
221
|
+
builds: preparedBuilds.map(({ actorConfig, fileCount, zipBuffer }) => ({
|
|
222
|
+
actorFullName: actorConfig.actorFullName,
|
|
223
|
+
versionNumber: LOCAL_SOURCE_VERSION_NUMBER,
|
|
224
|
+
folder: actorConfig.folder.path,
|
|
225
|
+
fileCount,
|
|
226
|
+
zipSizeBytes: zipBuffer.length,
|
|
227
|
+
})),
|
|
228
|
+
};
|
|
195
229
|
}
|
|
196
230
|
|
|
231
|
+
const preparedByActorFullName = new Map(
|
|
232
|
+
preparedBuilds.map((prepared) => [prepared.actorConfig.actorFullName, prepared]),
|
|
233
|
+
);
|
|
234
|
+
|
|
197
235
|
return runAndSummarizeBuilds(actorConfigs, 'LOCAL BUILDS', async (actorConfig, builder) => {
|
|
198
|
-
|
|
199
|
-
const zip = new AdmZip();
|
|
200
|
-
for (const { name, content } of await collectSourceFiles(actorConfig)) {
|
|
201
|
-
zip.addFile(name, content);
|
|
202
|
-
}
|
|
236
|
+
const { actorInfo, zipBuffer } = preparedByActorFullName.get(actorConfig.actorFullName)!;
|
|
203
237
|
const actorVersion: ApifyClientTypes.ActorVersion = {
|
|
204
238
|
versionNumber: LOCAL_SOURCE_VERSION_NUMBER,
|
|
205
|
-
tarballUrl: await builder.uploadSourceZip(LOCAL_SOURCE_VERSION_NUMBER,
|
|
239
|
+
tarballUrl: await builder.uploadSourceZip(LOCAL_SOURCE_VERSION_NUMBER, zipBuffer),
|
|
206
240
|
sourceType: ActorSourceType.Tarball,
|
|
207
241
|
};
|
|
208
|
-
return builder.createVersionAndBuild(LOCAL_SOURCE_VERSION_NUMBER, actorVersion, false);
|
|
242
|
+
return builder.createVersionAndBuild(LOCAL_SOURCE_VERSION_NUMBER, actorVersion, false, actorInfo);
|
|
209
243
|
});
|
|
210
244
|
};
|
package/bin/build.ts
CHANGED
|
@@ -3,7 +3,7 @@ import { ActorSourceType, ApifyClient } from 'apify-client';
|
|
|
3
3
|
|
|
4
4
|
import { normalizeRepoUrl } from './git.js';
|
|
5
5
|
import { logger } from './logger.js';
|
|
6
|
-
import type { ActorConfig, BuildData } from './types.js';
|
|
6
|
+
import type { ActorConfig, BuildData, DryRunPlan } from './types.js';
|
|
7
7
|
|
|
8
8
|
type BuildPrActorOptions = {
|
|
9
9
|
buildTag?: string;
|
|
@@ -179,7 +179,7 @@ export class ApifyBuilder {
|
|
|
179
179
|
* Deletes build of all versions. Apify API doesn't allow to delete default build and we explicitly skip it
|
|
180
180
|
* We delete devel builds faster because we used the for every PR until recently so just to get rid of them faster
|
|
181
181
|
*/
|
|
182
|
-
async deleteOldBuilds(): Promise<void> {
|
|
182
|
+
async deleteOldBuilds({ dryRun }: { dryRun: boolean }): Promise<void> {
|
|
183
183
|
// Even though we don't version our current Actors, if we ever such Actors to GitHub CI, we would accidentally delete old supported versions
|
|
184
184
|
// This hardcoded solution is not ideal, but it should prevent most imaginable cases
|
|
185
185
|
// All currently popular versioned Actors use `version-${number}` format
|
|
@@ -258,9 +258,15 @@ export class ApifyBuilder {
|
|
|
258
258
|
});
|
|
259
259
|
|
|
260
260
|
logger.info(
|
|
261
|
-
`[DELETE OLD BUILDS][${this.actorFullName}]: Deleting ${buildsToDelete.length}
|
|
262
|
-
`older than 30 days from total ${items.length}`,
|
|
261
|
+
`[DELETE OLD BUILDS][${this.actorFullName}]: ${dryRun ? '[DRY RUN] Would delete' : 'Deleting'} ${buildsToDelete.length} ` +
|
|
262
|
+
`old builds that are non-default and older than 30 days from total ${items.length}`,
|
|
263
263
|
);
|
|
264
|
+
if (dryRun) {
|
|
265
|
+
for (const build of buildsToDelete) {
|
|
266
|
+
logger.info(` ${build.buildNumber} (${build.id}, started ${build.startedAt.toISOString()})`);
|
|
267
|
+
}
|
|
268
|
+
return;
|
|
269
|
+
}
|
|
264
270
|
for (const build of buildsToDelete) {
|
|
265
271
|
await this.apifyClient.build(build.id).delete();
|
|
266
272
|
}
|
|
@@ -310,13 +316,13 @@ export const runAndSummarizeBuilds = async (
|
|
|
310
316
|
return waitAndSummarizeBuilds(startedBuilds, buildersByActorFullName, label);
|
|
311
317
|
};
|
|
312
318
|
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
}
|
|
319
|
+
type GitBuildPlan = {
|
|
320
|
+
actorFullName: string;
|
|
321
|
+
versionNumber: string;
|
|
322
|
+
buildTag?: string;
|
|
323
|
+
gitRepoUrl: string;
|
|
324
|
+
useDockerCache: boolean;
|
|
325
|
+
};
|
|
320
326
|
|
|
321
327
|
type RunBuildsOptions = {
|
|
322
328
|
actorConfigs: ActorConfig[];
|
|
@@ -337,7 +343,7 @@ export const runBuilds = async ({
|
|
|
337
343
|
isLatest = false,
|
|
338
344
|
dryRun,
|
|
339
345
|
useDockerCache,
|
|
340
|
-
}: RunBuildsOptions): Promise<BuildData[]
|
|
346
|
+
}: RunBuildsOptions): Promise<BuildData[] | DryRunPlan<GitBuildPlan>> => {
|
|
341
347
|
// Fetched once per Actor and reused for the checks, the version numbers and the build itself
|
|
342
348
|
const buildConfigs: BuildPrActorOptions[] = await Promise.all(
|
|
343
349
|
actorConfigs.map(async (actorConfig) => {
|
|
@@ -366,14 +372,24 @@ export const runBuilds = async ({
|
|
|
366
372
|
}),
|
|
367
373
|
);
|
|
368
374
|
|
|
375
|
+
// Everything above only reads, so a dry run has checked what the real run would, short of writing
|
|
369
376
|
if (dryRun) {
|
|
370
377
|
logger.info('[DRY RUN] Would build:');
|
|
371
|
-
for (const { actorConfig, versionNumber } of buildConfigs) {
|
|
372
|
-
logger.info(` ${actorConfig.actorFullName} (${versionNumber})`);
|
|
378
|
+
for (const { actorConfig, versionNumber, gitRepoUrl } of buildConfigs) {
|
|
379
|
+
logger.info(` ${actorConfig.actorFullName} (${versionNumber}) from ${gitRepoUrl}`);
|
|
373
380
|
}
|
|
374
|
-
return
|
|
375
|
-
|
|
376
|
-
|
|
381
|
+
return {
|
|
382
|
+
dryRun: true,
|
|
383
|
+
builds: buildConfigs.map(
|
|
384
|
+
({ actorConfig, versionNumber, buildTag, gitRepoUrl, useDockerCache: useCache }) => ({
|
|
385
|
+
actorFullName: actorConfig.actorFullName,
|
|
386
|
+
versionNumber,
|
|
387
|
+
buildTag,
|
|
388
|
+
gitRepoUrl,
|
|
389
|
+
useDockerCache: useCache,
|
|
390
|
+
}),
|
|
391
|
+
),
|
|
392
|
+
};
|
|
377
393
|
}
|
|
378
394
|
|
|
379
395
|
const buildConfigsByActorFullName = new Map(
|
|
@@ -398,8 +414,8 @@ export const runBuilds = async ({
|
|
|
398
414
|
});
|
|
399
415
|
};
|
|
400
416
|
|
|
401
|
-
export const deleteOldBuilds = async (actorConfigs: ActorConfig[]) => {
|
|
417
|
+
export const deleteOldBuilds = async (actorConfigs: ActorConfig[], { dryRun }: { dryRun: boolean }) => {
|
|
402
418
|
for (const actorConfig of actorConfigs) {
|
|
403
|
-
await ApifyBuilder.fromActorConfig(actorConfig).deleteOldBuilds();
|
|
419
|
+
await ApifyBuilder.fromActorConfig(actorConfig).deleteOldBuilds({ dryRun });
|
|
404
420
|
}
|
|
405
421
|
};
|
package/bin/diff-changes.ts
CHANGED
|
@@ -9,7 +9,6 @@ import { type RelativeDir, RelativeFile } from './utils/path/repo-relative.js';
|
|
|
9
9
|
interface ShouldBuildAndTestOptions {
|
|
10
10
|
filepathsChanged: string[];
|
|
11
11
|
actorConfigs: ActorConfig[];
|
|
12
|
-
isLatest?: boolean;
|
|
13
12
|
commits: Commit[];
|
|
14
13
|
// Glob relative to the repo root. Matching files count as changes even when .dockerignore lists them.
|
|
15
14
|
testFilesGlob?: string;
|
|
@@ -112,6 +111,7 @@ const isExcludedBySibling = (filePath: RelativeFile, actor: ActorConfig, allActo
|
|
|
112
111
|
type ActorChangeEntry = {
|
|
113
112
|
actorConfig: ActorConfig;
|
|
114
113
|
files: string[];
|
|
114
|
+
hasFunctionalChange: boolean;
|
|
115
115
|
};
|
|
116
116
|
|
|
117
117
|
type ChangeGroup = { actors: string[]; files: string[] };
|
|
@@ -164,13 +164,20 @@ const logChangeGroups = (groups: ChangeGroup[]): void => {
|
|
|
164
164
|
}
|
|
165
165
|
};
|
|
166
166
|
|
|
167
|
+
export interface ChangedActors {
|
|
168
|
+
/** Actors with at least one functional change. They need a new build and tests. */
|
|
169
|
+
functional: ActorConfig[];
|
|
170
|
+
/** Actors whose changes are all cosmetic. They only need a new build when released. */
|
|
171
|
+
cosmetic: ActorConfig[];
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/** Finds the Actors the changed files belong to, split by the most impactful change each Actor has. */
|
|
167
175
|
export const getChangedActors = async ({
|
|
168
176
|
filepathsChanged,
|
|
169
177
|
actorConfigs,
|
|
170
|
-
isLatest = false,
|
|
171
178
|
commits,
|
|
172
179
|
testFilesGlob,
|
|
173
|
-
}: ShouldBuildAndTestOptions): Promise<
|
|
180
|
+
}: ShouldBuildAndTestOptions): Promise<ChangedActors> => {
|
|
174
181
|
const actorsChangedMap = new Map<string, ActorChangeEntry>();
|
|
175
182
|
const testFilesInContext = new Set<string>();
|
|
176
183
|
const isTestFile = testFilesGlob ? picomatch(testFilesGlob, { nocase: true, dot: true }) : () => false;
|
|
@@ -191,21 +198,21 @@ export const getChangedActors = async ({
|
|
|
191
198
|
}
|
|
192
199
|
|
|
193
200
|
if (change.impact === 'ignored' || change.impact === 'outside-context') continue;
|
|
194
|
-
if (change.impact === 'cosmetic' && !isLatest) continue;
|
|
195
201
|
|
|
196
|
-
const entry = actorsChangedMap.get(actorConfig.folder.path) ?? {
|
|
202
|
+
const entry = actorsChangedMap.get(actorConfig.folder.path) ?? {
|
|
203
|
+
actorConfig,
|
|
204
|
+
files: [],
|
|
205
|
+
hasFunctionalChange: false,
|
|
206
|
+
};
|
|
197
207
|
entry.files.push(originalFilePath);
|
|
208
|
+
entry.hasFunctionalChange ||= change.impact === 'functional';
|
|
198
209
|
actorsChangedMap.set(actorConfig.folder.path, entry);
|
|
199
210
|
}
|
|
200
211
|
}
|
|
201
212
|
|
|
202
|
-
const actorsChanged = Array.from(actorsChangedMap.values()).map((entry) => entry.actorConfig);
|
|
203
|
-
|
|
204
213
|
// Log changes grouped by actor set, so changes shared across actors are logged once
|
|
205
214
|
// instead of being repeated per actor.
|
|
206
|
-
|
|
207
|
-
const groups = groupFilesByActorSet(fileToActors);
|
|
208
|
-
logChangeGroups(groups);
|
|
215
|
+
logChangeGroups(groupFilesByActorSet(buildFileToActorsMap(actorsChangedMap)));
|
|
209
216
|
|
|
210
217
|
for (const testFile of filepathsChanged.filter(
|
|
211
218
|
(filePath) => isTestFile(filePath) && !testFilesInContext.has(filePath),
|
|
@@ -216,12 +223,13 @@ export const getChangedActors = async ({
|
|
|
216
223
|
);
|
|
217
224
|
}
|
|
218
225
|
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
}
|
|
226
|
+
const entries = Array.from(actorsChangedMap.values());
|
|
227
|
+
const functional = entries.filter((entry) => entry.hasFunctionalChange).map((entry) => entry.actorConfig);
|
|
228
|
+
const cosmetic = entries.filter((entry) => !entry.hasFunctionalChange).map((entry) => entry.actorConfig);
|
|
229
|
+
|
|
230
|
+
const names = (configs: ActorConfig[]) => configs.map((config) => config.actorFullName).join(', ') || 'none';
|
|
231
|
+
logger.info(`[DIFF]: Actors with functional changes: ${names(functional)}`);
|
|
232
|
+
logger.info(`[DIFF]: Actors with only cosmetic changes: ${names(cosmetic)}`);
|
|
225
233
|
|
|
226
|
-
return
|
|
234
|
+
return { functional, cosmetic };
|
|
227
235
|
};
|