apify-test-tools 0.10.1-beta.6 → 0.10.1
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/CHANGELOG.md +1 -3
- package/bin/build-from-local.ts +19 -27
- package/bin/build.ts +1 -1
- package/bin/diff-changes.ts +26 -35
- package/bin/dockerignore.ts +24 -17
- package/bin/types.ts +7 -3
- package/bin/utils/config/actor-json.ts +49 -8
- package/bin/utils/config/load-config.ts +4 -24
- package/bin/utils/path/repo-relative.ts +27 -4
- package/dist/bin/build-from-local.d.ts +3 -2
- package/dist/bin/build-from-local.d.ts.map +1 -1
- package/dist/bin/build-from-local.js +16 -23
- package/dist/bin/build-from-local.js.map +1 -1
- package/dist/bin/build.js +1 -1
- package/dist/bin/build.js.map +1 -1
- package/dist/bin/diff-changes.d.ts.map +1 -1
- package/dist/bin/diff-changes.js +26 -29
- package/dist/bin/diff-changes.js.map +1 -1
- package/dist/bin/dockerignore.d.ts +11 -10
- package/dist/bin/dockerignore.d.ts.map +1 -1
- package/dist/bin/dockerignore.js +24 -15
- package/dist/bin/dockerignore.js.map +1 -1
- package/dist/bin/types.d.ts +6 -3
- package/dist/bin/types.d.ts.map +1 -1
- package/dist/bin/utils/config/actor-json.d.ts.map +1 -1
- package/dist/bin/utils/config/actor-json.js +35 -4
- package/dist/bin/utils/config/actor-json.js.map +1 -1
- package/dist/bin/utils/config/load-config.d.ts +1 -11
- package/dist/bin/utils/config/load-config.d.ts.map +1 -1
- package/dist/bin/utils/config/load-config.js +1 -10
- package/dist/bin/utils/config/load-config.js.map +1 -1
- package/dist/bin/utils/path/repo-relative.d.ts +4 -0
- package/dist/bin/utils/path/repo-relative.d.ts.map +1 -1
- package/dist/bin/utils/path/repo-relative.js +24 -5
- package/dist/bin/utils/path/repo-relative.js.map +1 -1
- package/dist/test/unit/bin/actor-filtering.test.js +8 -2
- package/dist/test/unit/bin/actor-filtering.test.js.map +1 -1
- package/dist/test/unit/bin/build-from-local.test.js +53 -5
- package/dist/test/unit/bin/build-from-local.test.js.map +1 -1
- package/dist/test/unit/bin/diff-changes.test.js +98 -132
- package/dist/test/unit/bin/diff-changes.test.js.map +1 -1
- package/dist/test/unit/bin/dockerignore.test.js +27 -24
- package/dist/test/unit/bin/dockerignore.test.js.map +1 -1
- package/dist/test/unit/bin/utils/config/load-config.test.js +40 -4
- package/dist/test/unit/bin/utils/config/load-config.test.js.map +1 -1
- package/dist/test/unit/bin/utils/path/repo-relative.test.js +5 -0
- package/dist/test/unit/bin/utils/path/repo-relative.test.js.map +1 -1
- package/dist/test/unit/should-built-and-test.test.js +55 -116
- package/dist/test/unit/should-built-and-test.test.js.map +1 -1
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/package.json +65 -65
- package/test/unit/bin/actor-filtering.test.ts +8 -2
- package/test/unit/bin/build-from-local.test.ts +54 -8
- package/test/unit/bin/diff-changes.test.ts +133 -133
- package/test/unit/bin/dockerignore.test.ts +28 -28
- package/test/unit/bin/utils/config/load-config.test.ts +54 -4
- package/test/unit/bin/utils/path/repo-relative.test.ts +6 -0
- package/test/unit/should-built-and-test.test.ts +98 -121
package/CHANGELOG.md
CHANGED
|
@@ -2,8 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to this project will be documented in this file.
|
|
4
4
|
|
|
5
|
-
|
|
6
|
-
## 0.10.1 - **not yet released**
|
|
5
|
+
## [0.10.1](https://github.com/apify/apify-test-tools/releases/tag/v0.10.1) (2026-10-13)
|
|
7
6
|
|
|
8
7
|
### 🚀 Features
|
|
9
8
|
|
|
@@ -19,7 +18,6 @@ All notable changes to this project will be documented in this file.
|
|
|
19
18
|
- **worktree:** Support worktree usage in tests ([#188](https://github.com/apify/apify-test-tools/pull/188)) ([ffc7e7b](https://github.com/apify/apify-test-tools/commit/ffc7e7bf1466af00eed671137feff0cbda712003)) by [@JuanGalilea](https://github.com/JuanGalilea)
|
|
20
19
|
|
|
21
20
|
|
|
22
|
-
<!-- git-cliff-unreleased-end -->
|
|
23
21
|
## [0.10.0](https://github.com/apify/apify-test-tools/releases/tag/v0.10.0) (2026-09-30)
|
|
24
22
|
|
|
25
23
|
### 🚀 Features
|
package/bin/build-from-local.ts
CHANGED
|
@@ -7,12 +7,13 @@ import type * as ApifyClientTypes from 'apify-client';
|
|
|
7
7
|
import { ActorSourceType } from 'apify-client';
|
|
8
8
|
|
|
9
9
|
import { dryRunBuildData, LOCAL_SOURCE_VERSION_NUMBER, runAndSummarizeBuilds } from './build.js';
|
|
10
|
-
import {
|
|
10
|
+
import { loadDockerIgnore } from './dockerignore.js';
|
|
11
11
|
import { logger } from './logger.js';
|
|
12
12
|
import { isPathWithinScope } from './path-utils.js';
|
|
13
13
|
import type { ActorConfig, BuildData } from './types.js';
|
|
14
14
|
import type { SourceFile } from './utils.js';
|
|
15
15
|
import { getGitignoredPaths, isOutsideDir, listRepoFilePaths, readSourceFile } from './utils.js';
|
|
16
|
+
import type { RelativeDir } from './utils/path/repo-relative.js';
|
|
16
17
|
|
|
17
18
|
// JUST IN CASE. File patterns that commonly hold credentials — never ship these into a build, regardless
|
|
18
19
|
// of sourceType or of whether the repo's .gitignore happens to list them. Everything else that should be
|
|
@@ -21,30 +22,27 @@ import { getGitignoredPaths, isOutsideDir, listRepoFilePaths, readSourceFile } f
|
|
|
21
22
|
const SKIP_FILE_PATTERNS = [/^\.env(\..+)?$/, /\.pem$/, /\.key$/, /\.pfx$/, /\.p12$/];
|
|
22
23
|
const isSecretFile = (fileName: string): boolean => SKIP_FILE_PATTERNS.some((pattern) => pattern.test(fileName));
|
|
23
24
|
|
|
24
|
-
export const collectSourceFiles = async (
|
|
25
|
+
export const collectSourceFiles = async (actorConfig: ActorConfig): Promise<SourceFile[]> => {
|
|
25
26
|
const repoRoot = process.cwd();
|
|
26
|
-
const absActorDir = path.resolve(
|
|
27
|
+
const absActorDir = path.resolve(actorConfig.folder.path);
|
|
27
28
|
|
|
28
|
-
//
|
|
29
|
-
//
|
|
30
|
-
|
|
31
|
-
const
|
|
32
|
-
const actorJson = JSON.parse(await fs.readFile(actorJsonPath, 'utf8')) as Record<string, unknown>;
|
|
33
|
-
const rawContextDir = actorJson.dockerContextDir as string | undefined;
|
|
34
|
-
const contextAbsDir = rawContextDir ? path.resolve(absActorDir, '.actor', rawContextDir) : undefined;
|
|
35
|
-
const isMonorepoActor = !!contextAbsDir && isOutsideDir(contextAbsDir, absActorDir);
|
|
29
|
+
// Config loading has already resolved dockerContextDir. Keep the raw actor.json contents
|
|
30
|
+
// for rewriting its path fields if this actor needs a flattened build context.
|
|
31
|
+
const actorJson = JSON.parse(await fs.readFile(actorConfig.actorJson.file.path, 'utf8')) as Record<string, unknown>;
|
|
32
|
+
const isMonorepoActor = !actorConfig.folder.contains(actorConfig.dockerContextDir);
|
|
36
33
|
|
|
37
|
-
const
|
|
38
|
-
const
|
|
34
|
+
const sourceRoot = isMonorepoActor ? actorConfig.dockerContextDir : actorConfig.folder;
|
|
35
|
+
const sourceRootAbs = path.resolve(sourceRoot.path);
|
|
36
|
+
const keptFilePaths = await collectNonIgnoredFiles(sourceRoot, repoRoot);
|
|
39
37
|
|
|
40
38
|
if (!isMonorepoActor) {
|
|
41
|
-
return Promise.all(keptFilePaths.map(async (filePath) => readSourceFile(filePath,
|
|
39
|
+
return Promise.all(keptFilePaths.map(async (filePath) => readSourceFile(filePath, sourceRootAbs)));
|
|
42
40
|
}
|
|
43
41
|
|
|
44
42
|
const { tempDir, filePaths } = await flattenMonorepoContext(
|
|
45
|
-
|
|
43
|
+
actorConfig.actorFullName,
|
|
46
44
|
absActorDir,
|
|
47
|
-
|
|
45
|
+
sourceRootAbs,
|
|
48
46
|
actorJson,
|
|
49
47
|
keptFilePaths,
|
|
50
48
|
);
|
|
@@ -65,16 +63,10 @@ export const collectSourceFiles = async (actorName: string, actorDir: string): P
|
|
|
65
63
|
// regardless of .gitignore/.dockerignore, matching Apify CLI's own behavior. Files matching the
|
|
66
64
|
// hardcoded secret-pattern backstop (keys, certs, .env variants) are dropped unconditionally,
|
|
67
65
|
// .actor/ included, since those should never ship regardless of what the ignore files say.
|
|
68
|
-
export const collectNonIgnoredFiles = async (dockerContextDir:
|
|
69
|
-
const relativePaths = await listRepoFilePaths(repoRoot, dockerContextDir);
|
|
66
|
+
export const collectNonIgnoredFiles = async (dockerContextDir: RelativeDir, repoRoot: string): Promise<string[]> => {
|
|
67
|
+
const relativePaths = await listRepoFilePaths(repoRoot, path.resolve(repoRoot, dockerContextDir.path));
|
|
70
68
|
const ignoredPaths = await getGitignoredPaths(relativePaths, repoRoot);
|
|
71
|
-
const
|
|
72
|
-
relativePaths.map((relPath) => [
|
|
73
|
-
relPath,
|
|
74
|
-
path.relative(dockerContextDir, path.join(repoRoot, relPath)).split(path.sep).join('/'),
|
|
75
|
-
]),
|
|
76
|
-
);
|
|
77
|
-
const isDockerIgnored = buildDockerIgnoreMatcher(dockerContextDir);
|
|
69
|
+
const isDockerIgnored = loadDockerIgnore(dockerContextDir);
|
|
78
70
|
|
|
79
71
|
return relativePaths
|
|
80
72
|
.filter((relPath) => {
|
|
@@ -82,7 +74,7 @@ export const collectNonIgnoredFiles = async (dockerContextDir: string, repoRoot:
|
|
|
82
74
|
const isUnderActorDir = relPath.split('/').includes('.actor');
|
|
83
75
|
if (isUnderActorDir) return true;
|
|
84
76
|
if (ignoredPaths.has(relPath)) return false;
|
|
85
|
-
return !isDockerIgnored(
|
|
77
|
+
return !isDockerIgnored(relPath);
|
|
86
78
|
})
|
|
87
79
|
.map((relPath) => path.join(repoRoot, relPath));
|
|
88
80
|
};
|
|
@@ -205,7 +197,7 @@ export const runBuildsFromLocal = async ({
|
|
|
205
197
|
return runAndSummarizeBuilds(actorConfigs, 'LOCAL BUILDS', async (actorConfig, builder) => {
|
|
206
198
|
// Zipped and uploaded like `apify push` does, since inline SOURCE_FILES are capped at ~9 MB.
|
|
207
199
|
const zip = new AdmZip();
|
|
208
|
-
for (const { name, content } of await collectSourceFiles(actorConfig
|
|
200
|
+
for (const { name, content } of await collectSourceFiles(actorConfig)) {
|
|
209
201
|
zip.addFile(name, content);
|
|
210
202
|
}
|
|
211
203
|
const actorVersion: ApifyClientTypes.ActorVersion = {
|
package/bin/build.ts
CHANGED
|
@@ -352,7 +352,7 @@ export const runBuilds = async ({
|
|
|
352
352
|
|
|
353
353
|
// Depending on if these are miniactors or standaloneActors
|
|
354
354
|
let gitRepoUrl = `${repoUrl}#${branch}`;
|
|
355
|
-
if (actorConfig.folder) {
|
|
355
|
+
if (!actorConfig.folder.isRoot()) {
|
|
356
356
|
gitRepoUrl = `${gitRepoUrl}:${actorConfig.folder}`;
|
|
357
357
|
}
|
|
358
358
|
return {
|
package/bin/diff-changes.ts
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { isCosmeticOnlyJsonSchemaChange } from './diff-json-schema.js';
|
|
2
2
|
import { type DockerIgnoreMatcher, loadDockerIgnore } from './dockerignore.js';
|
|
3
3
|
import { logger } from './logger.js';
|
|
4
|
-
import { findContainingScope, hoistPath, isPathWithinScope } from './path-utils.js';
|
|
5
4
|
import type { ActorConfig, Commit } from './types.js';
|
|
5
|
+
import { type RelativeDir, RelativeFile } from './utils/path/repo-relative.js';
|
|
6
6
|
|
|
7
7
|
interface ShouldBuildAndTestOptions {
|
|
8
8
|
filepathsChanged: string[];
|
|
@@ -21,9 +21,10 @@ const IGNORED_TOP_LEVEL_FILES = [
|
|
|
21
21
|
'.editorconfig',
|
|
22
22
|
];
|
|
23
23
|
|
|
24
|
-
|
|
25
|
-
const
|
|
26
|
-
IGNORED_TOP_LEVEL_FILES.some((pattern) =>
|
|
24
|
+
const isIgnoredTopLevelFile = (filePath: RelativeFile, contextPath: RelativeDir): boolean => {
|
|
25
|
+
const pathWithinContext = contextPath.relativePathTo(filePath.path).toLowerCase();
|
|
26
|
+
return IGNORED_TOP_LEVEL_FILES.some((pattern) => pathWithinContext.startsWith(pattern));
|
|
27
|
+
};
|
|
27
28
|
|
|
28
29
|
type FileChangeForActor =
|
|
29
30
|
| { impact: 'ignored' }
|
|
@@ -46,12 +47,12 @@ type FileChangeForActor =
|
|
|
46
47
|
* 7. Everything else → functional
|
|
47
48
|
*/
|
|
48
49
|
const classifyFileChange = async (
|
|
49
|
-
|
|
50
|
+
changedFile: RelativeFile,
|
|
50
51
|
actorConfig: ActorConfig,
|
|
51
52
|
commits: Commit[],
|
|
52
53
|
dockerIgnoreMatcher: DockerIgnoreMatcher,
|
|
53
54
|
): Promise<FileChangeForActor> => {
|
|
54
|
-
const lowercaseFilePath =
|
|
55
|
+
const lowercaseFilePath = changedFile.path.toLowerCase();
|
|
55
56
|
|
|
56
57
|
// TODO: hardcodes that there's a single repo-wide changelog belonging to every actor. Should instead
|
|
57
58
|
// be derived from parsing actor.json (readme, changelog, schema paths), see
|
|
@@ -60,56 +61,46 @@ const classifyFileChange = async (
|
|
|
60
61
|
return { impact: 'cosmetic', semanticallyVerified: false };
|
|
61
62
|
}
|
|
62
63
|
|
|
63
|
-
const
|
|
64
|
-
|
|
65
|
-
const matchedContext = findContainingScope(lowercaseFilePath, lowercaseContextPaths);
|
|
66
|
-
if (matchedContext === undefined) {
|
|
64
|
+
const matchedContext = actorConfig.contextPaths.find((contextPath) => contextPath.contains(changedFile));
|
|
65
|
+
if (!matchedContext) {
|
|
67
66
|
return { impact: 'outside-context' };
|
|
68
67
|
}
|
|
69
68
|
|
|
70
|
-
|
|
71
|
-
if (isIgnoredTopLevelFile(hoistedFilePath)) {
|
|
69
|
+
if (isIgnoredTopLevelFile(changedFile, matchedContext)) {
|
|
72
70
|
return { impact: 'ignored' };
|
|
73
71
|
}
|
|
74
72
|
|
|
75
|
-
const
|
|
76
|
-
const actorDotDir = lowercaseFolder ? `${lowercaseFolder}/.actor` : '.actor';
|
|
77
|
-
const isUnderActorDotDir = isPathWithinScope(lowercaseFilePath, actorDotDir);
|
|
73
|
+
const isUnderActorDotDir = actorConfig.actorJson.file.parent.contains(changedFile);
|
|
78
74
|
|
|
79
75
|
// .actor/ can legitimately be listed in .dockerignore (the Apify platform evaluates it before
|
|
80
76
|
// the Docker build, so excluding it from the build context is a valid caching optimization) —
|
|
81
77
|
// that shouldn't cause changes to .actor/ itself to be ignored here.
|
|
82
|
-
if (!isUnderActorDotDir && dockerIgnoreMatcher(
|
|
78
|
+
if (!isUnderActorDotDir && dockerIgnoreMatcher(changedFile.path)) {
|
|
83
79
|
return { impact: 'ignored' };
|
|
84
80
|
}
|
|
85
81
|
|
|
86
|
-
const isInActorFolder =
|
|
82
|
+
const isInActorFolder = changedFile.isWithin(actorConfig.folder);
|
|
87
83
|
|
|
88
84
|
if (lowercaseFilePath.endsWith('readme.md')) {
|
|
89
85
|
return isInActorFolder ? { impact: 'cosmetic', semanticallyVerified: false } : { impact: 'ignored' };
|
|
90
86
|
}
|
|
91
87
|
|
|
92
88
|
if (lowercaseFilePath.endsWith('.json') && isUnderActorDotDir) {
|
|
93
|
-
const isCosmetic = await isCosmeticOnlyJsonSchemaChange(commits,
|
|
94
|
-
if (isCosmetic) {
|
|
95
|
-
return { impact: 'cosmetic', semanticallyVerified: true };
|
|
96
|
-
}
|
|
89
|
+
const isCosmetic = await isCosmeticOnlyJsonSchemaChange(commits, changedFile.path);
|
|
90
|
+
if (isCosmetic) return { impact: 'cosmetic', semanticallyVerified: true };
|
|
97
91
|
}
|
|
98
92
|
|
|
99
93
|
return { impact: 'functional' };
|
|
100
94
|
};
|
|
101
95
|
|
|
102
96
|
/**
|
|
103
|
-
*
|
|
104
|
-
* Root actors (folder === "") never exclude files from siblings.
|
|
97
|
+
* Exclude files inside another actor's non-root folder, including for an actor at the repo root.
|
|
105
98
|
*/
|
|
106
|
-
const isExcludedBySibling = (
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
isPathWithinScope(lowercaseFilePath, other.folder.toLowerCase()),
|
|
112
|
-
);
|
|
99
|
+
const isExcludedBySibling = (filePath: RelativeFile, actor: ActorConfig, allActors: ActorConfig[]): boolean => {
|
|
100
|
+
const isSibling = (other: ActorConfig) => !other.folder.isEqualTo(actor.folder);
|
|
101
|
+
const isNonRoot = (other: ActorConfig) => !other.folder.isRoot();
|
|
102
|
+
const nonRootSiblings = allActors.filter(isSibling).filter(isNonRoot);
|
|
103
|
+
return nonRootSiblings.some((other) => filePath.isWithin(other.folder));
|
|
113
104
|
};
|
|
114
105
|
|
|
115
106
|
type ActorChangeEntry = {
|
|
@@ -179,20 +170,20 @@ export const getChangedActors = async ({
|
|
|
179
170
|
const dockerIgnoreMatcher = loadDockerIgnore(actorConfig.dockerContextDir);
|
|
180
171
|
|
|
181
172
|
for (const originalFilePath of filepathsChanged) {
|
|
182
|
-
const
|
|
173
|
+
const filePath = new RelativeFile(originalFilePath);
|
|
183
174
|
|
|
184
|
-
if (isExcludedBySibling(
|
|
175
|
+
if (isExcludedBySibling(filePath, actorConfig, actorConfigs)) {
|
|
185
176
|
continue;
|
|
186
177
|
}
|
|
187
178
|
|
|
188
|
-
const change = await classifyFileChange(
|
|
179
|
+
const change = await classifyFileChange(filePath, actorConfig, commits, dockerIgnoreMatcher);
|
|
189
180
|
|
|
190
181
|
if (change.impact === 'ignored' || change.impact === 'outside-context') continue;
|
|
191
182
|
if (change.impact === 'cosmetic' && !isLatest) continue;
|
|
192
183
|
|
|
193
|
-
const entry = actorsChangedMap.get(actorConfig.folder) ?? { actorConfig, files: [] };
|
|
184
|
+
const entry = actorsChangedMap.get(actorConfig.folder.path) ?? { actorConfig, files: [] };
|
|
194
185
|
entry.files.push(originalFilePath);
|
|
195
|
-
actorsChangedMap.set(actorConfig.folder, entry);
|
|
186
|
+
actorsChangedMap.set(actorConfig.folder.path, entry);
|
|
196
187
|
}
|
|
197
188
|
}
|
|
198
189
|
|
package/bin/dockerignore.ts
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import fs from 'node:fs';
|
|
2
|
-
import path from 'node:path';
|
|
3
2
|
|
|
4
3
|
import ignore from 'ignore';
|
|
5
4
|
|
|
6
|
-
import {
|
|
5
|
+
import { logger } from './logger.js';
|
|
6
|
+
import type { RelativeDir, RelativeFile } from './utils/path/repo-relative.js';
|
|
7
7
|
|
|
8
8
|
export type DockerIgnoreMatcher = (repoRelativePath: string) => boolean;
|
|
9
9
|
|
|
@@ -15,36 +15,43 @@ export type DockerIgnoreMatcher = (repoRelativePath: string) => boolean;
|
|
|
15
15
|
const normalizeDockerignorePattern = (line: string): string => line.replace(/^(!?)(?:\.\/)+/, '$1');
|
|
16
16
|
|
|
17
17
|
/**
|
|
18
|
-
* Reads `.dockerignore`
|
|
19
|
-
*
|
|
20
|
-
* `
|
|
21
|
-
*
|
|
18
|
+
* Reads a `.dockerignore` file and returns a matcher for repo-relative paths within its parent
|
|
19
|
+
* directory. Paths outside that directory never match. Matching is performed on paths relative to
|
|
20
|
+
* the `.dockerignore` file's parent, and leading `./` prefixes are removed from patterns to match
|
|
21
|
+
* Docker's normalization behavior.
|
|
22
22
|
*
|
|
23
|
-
* Returns a
|
|
23
|
+
* Returns a matcher that always returns `false` if the file cannot be read.
|
|
24
24
|
*/
|
|
25
|
-
export const buildDockerIgnoreMatcher = (
|
|
25
|
+
export const buildDockerIgnoreMatcher = (dockerIgnore: RelativeFile): DockerIgnoreMatcher => {
|
|
26
26
|
let content: string;
|
|
27
27
|
try {
|
|
28
|
-
content = fs.readFileSync(path
|
|
28
|
+
content = fs.readFileSync(dockerIgnore.path, 'utf-8');
|
|
29
29
|
} catch {
|
|
30
30
|
return () => false;
|
|
31
31
|
}
|
|
32
32
|
|
|
33
33
|
const matcher = ignore().add(content.split('\n').map(normalizeDockerignorePattern).join('\n'));
|
|
34
|
-
|
|
34
|
+
const rootDir = dockerIgnore.parent;
|
|
35
35
|
return (filePath: string): boolean => {
|
|
36
|
-
if (!
|
|
36
|
+
if (!rootDir.containsPath(filePath)) {
|
|
37
37
|
return false;
|
|
38
38
|
}
|
|
39
39
|
|
|
40
|
-
return matcher.ignores(
|
|
40
|
+
return matcher.ignores(rootDir.relativePathTo(filePath));
|
|
41
41
|
};
|
|
42
42
|
};
|
|
43
43
|
|
|
44
44
|
/**
|
|
45
|
-
*
|
|
46
|
-
*
|
|
47
|
-
*
|
|
45
|
+
* Loads `.dockerignore` from the root of `dockerContextDir` and returns a matcher that accepts
|
|
46
|
+
* repo-relative file paths. Patterns are matched relative to `dockerContextDir`, consistent with
|
|
47
|
+
* Docker's build-context behavior. If the file cannot be read, the matcher always returns `false`.
|
|
48
48
|
*/
|
|
49
|
-
export const loadDockerIgnore = (dockerContextDir:
|
|
50
|
-
|
|
49
|
+
export const loadDockerIgnore = (dockerContextDir: RelativeDir): DockerIgnoreMatcher => {
|
|
50
|
+
try {
|
|
51
|
+
const dockerIgnorePath = dockerContextDir.joinFile('.dockerignore');
|
|
52
|
+
return buildDockerIgnoreMatcher(dockerIgnorePath);
|
|
53
|
+
} catch {
|
|
54
|
+
logger.warn(`[.dockerignore] not found in "${dockerContextDir}".`);
|
|
55
|
+
return () => false;
|
|
56
|
+
}
|
|
57
|
+
};
|
package/bin/types.ts
CHANGED
|
@@ -1,3 +1,6 @@
|
|
|
1
|
+
import type { ActorJsonPaths } from './utils/config/actor-json.js';
|
|
2
|
+
import type { ExistingDir, RelativeDir } from './utils/path/repo-relative.js';
|
|
3
|
+
|
|
1
4
|
export interface Config {
|
|
2
5
|
targetBranch: string;
|
|
3
6
|
sourceBranch: string;
|
|
@@ -22,8 +25,9 @@ export interface BuildData {
|
|
|
22
25
|
|
|
23
26
|
export interface ActorConfig {
|
|
24
27
|
actorFullName: string;
|
|
25
|
-
folder:
|
|
28
|
+
folder: ExistingDir;
|
|
26
29
|
tokenEnvVar: string;
|
|
27
|
-
|
|
28
|
-
|
|
30
|
+
actorJson: ActorJsonPaths;
|
|
31
|
+
dockerContextDir: RelativeDir;
|
|
32
|
+
contextPaths: RelativeDir[];
|
|
29
33
|
}
|
|
@@ -1,3 +1,6 @@
|
|
|
1
|
+
import { readdirSync } from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
|
|
1
4
|
import z from 'zod';
|
|
2
5
|
|
|
3
6
|
import { safeReadJsonObjectFile } from '../json-file.js';
|
|
@@ -38,6 +41,31 @@ const resolveFile = (actorJsonFile: ExistingFile, field: string, value: unknown)
|
|
|
38
41
|
}
|
|
39
42
|
};
|
|
40
43
|
|
|
44
|
+
/**
|
|
45
|
+
* Looks for fallback locations of config files in the provided order, matches insensitively.
|
|
46
|
+
*
|
|
47
|
+
* Pretty inefficient way to do it, but it was either this bs or adding a matching library just for the insensitive matching of fallback files.
|
|
48
|
+
* So if this ever becomes a performance problem for anyone, they can explicitly state their files in the config and live happily ever after.
|
|
49
|
+
*/
|
|
50
|
+
const resolveDefaultFileLocations = (
|
|
51
|
+
actorJsonFile: ExistingFile,
|
|
52
|
+
fallbackPaths: readonly string[],
|
|
53
|
+
): RelativeFile | undefined => {
|
|
54
|
+
for (const fallbackCandidate of fallbackPaths) {
|
|
55
|
+
const file = actorJsonFile.joinFile(fallbackCandidate);
|
|
56
|
+
const expectedName = path.posix.basename(file.path);
|
|
57
|
+
const entries = readdirSync(file.parent.path);
|
|
58
|
+
const actualName =
|
|
59
|
+
entries.find((entry) => entry === expectedName) ??
|
|
60
|
+
entries.find((entry) => entry.toLowerCase() === expectedName.toLowerCase());
|
|
61
|
+
if (actualName) {
|
|
62
|
+
const actualFile = file.parent.joinFile(actualName);
|
|
63
|
+
if (actualFile.exists()) return actualFile;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
return undefined;
|
|
67
|
+
};
|
|
68
|
+
|
|
41
69
|
export function readActorJson(config: ValidatedActorConfig): ActorJsonPaths {
|
|
42
70
|
const actorJsonPath = config.folder.joinFile('.actor/actor.json');
|
|
43
71
|
let actorJsonFile: ExistingFile;
|
|
@@ -66,14 +94,27 @@ export function readActorJson(config: ValidatedActorConfig): ActorJsonPaths {
|
|
|
66
94
|
return {
|
|
67
95
|
file: actorJsonFile,
|
|
68
96
|
dockerContextDir,
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
97
|
+
// all these with `resolveDefaultFileLocations` is because the platform has lookup locations as fallback
|
|
98
|
+
// if not present in actor.json, this is behavior from before .actor folder was a thing
|
|
99
|
+
// with this we replicate exactly what the platform does for resolution of these files
|
|
100
|
+
// so we resolve the same way the platform does during builds.
|
|
101
|
+
dockerfile:
|
|
102
|
+
resolveFile(actorJsonFile, 'dockerfile', fields.dockerfile) ??
|
|
103
|
+
resolveDefaultFileLocations(actorJsonFile, ['./Dockerfile', '../Dockerfile']),
|
|
104
|
+
readme:
|
|
105
|
+
resolveFile(actorJsonFile, 'readme', fields.readme) ??
|
|
106
|
+
resolveDefaultFileLocations(actorJsonFile, ['./README.md', '../README.md']),
|
|
107
|
+
changelog:
|
|
108
|
+
resolveFile(actorJsonFile, 'changelog', fields.changelog) ??
|
|
109
|
+
resolveDefaultFileLocations(actorJsonFile, ['./CHANGELOG.md', '../CHANGELOG.md']),
|
|
110
|
+
inputSchema:
|
|
111
|
+
resolveFile(
|
|
112
|
+
actorJsonFile,
|
|
113
|
+
fields.input != null ? 'input' : 'inputSchema',
|
|
114
|
+
fields.input ?? fields.inputSchema,
|
|
115
|
+
) ?? resolveDefaultFileLocations(actorJsonFile, ['./INPUT_SCHEMA.json', '../INPUT_SCHEMA.json']),
|
|
116
|
+
|
|
117
|
+
// these other schemas came after .actor was invented, so they have no fallbacks in platform
|
|
77
118
|
outputSchema: resolveFile(
|
|
78
119
|
actorJsonFile,
|
|
79
120
|
fields.output != null ? 'output' : 'outputSchema',
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { selectActors } from '../../actor-filtering.js';
|
|
2
2
|
import type { ActorConfig } from '../../types.js';
|
|
3
3
|
import { safeReadJsonObjectFile } from '../json-file.js';
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
4
|
+
import { ExistingFile, type RelativeDir } from '../path/repo-relative.js';
|
|
5
|
+
import { readActorJson } from './actor-json.js';
|
|
6
6
|
import { parseConfigFile } from './parser.js';
|
|
7
7
|
import type { ValidatedActorConfig } from './structures/base.js';
|
|
8
8
|
|
|
@@ -28,18 +28,6 @@ const findOverlappingContextPaths = (contextPaths: RelativeDir[]): [RelativeDir,
|
|
|
28
28
|
return undefined;
|
|
29
29
|
};
|
|
30
30
|
|
|
31
|
-
// Downstream ActorConfig consumers still use "" for the repo root.
|
|
32
|
-
const legacyPath = (dir: RelativeDir): string => (dir.path === '.' ? '' : dir.path);
|
|
33
|
-
|
|
34
|
-
export interface LoadedActorConfig {
|
|
35
|
-
actorFullName: string;
|
|
36
|
-
folder: ExistingDir;
|
|
37
|
-
tokenEnvVar: string;
|
|
38
|
-
actorJson: ActorJsonPaths;
|
|
39
|
-
dockerContextDir: RelativeDir;
|
|
40
|
-
contextPaths: RelativeDir[];
|
|
41
|
-
}
|
|
42
|
-
|
|
43
31
|
// #endregion
|
|
44
32
|
|
|
45
33
|
// #region stages
|
|
@@ -79,7 +67,7 @@ const readConfigFileContents = async (): Promise<Record<string, unknown>> => {
|
|
|
79
67
|
* Stage 3 — resolves one normalized entry against the repo, reading the actor's `.actor/actor.json`
|
|
80
68
|
* for its `dockerContextDir` and deciding which paths count as the actor's context.
|
|
81
69
|
*/
|
|
82
|
-
export const loadActorConfig = (entry: ValidatedActorConfig):
|
|
70
|
+
export const loadActorConfig = (entry: ValidatedActorConfig): ActorConfig => {
|
|
83
71
|
const { folder } = entry;
|
|
84
72
|
const actorJson = readActorJson(entry);
|
|
85
73
|
const { dockerContextDir } = actorJson;
|
|
@@ -109,14 +97,6 @@ export const loadActorConfig = (entry: ValidatedActorConfig): LoadedActorConfig
|
|
|
109
97
|
};
|
|
110
98
|
};
|
|
111
99
|
|
|
112
|
-
const toLegacyActorConfig = (entry: LoadedActorConfig): ActorConfig => ({
|
|
113
|
-
actorFullName: entry.actorFullName,
|
|
114
|
-
folder: legacyPath(entry.folder),
|
|
115
|
-
tokenEnvVar: entry.tokenEnvVar,
|
|
116
|
-
dockerContextDir: legacyPath(entry.dockerContextDir),
|
|
117
|
-
contextPaths: entry.contextPaths.map(legacyPath),
|
|
118
|
-
});
|
|
119
|
-
|
|
120
100
|
// #endregion
|
|
121
101
|
|
|
122
102
|
export const readConfigFile = async (selection: { actors: string[]; ignore: string[] }): Promise<ActorConfig[]> => {
|
|
@@ -126,7 +106,7 @@ export const readConfigFile = async (selection: { actors: string[]; ignore: stri
|
|
|
126
106
|
const actorConfigs: ActorConfig[] = [];
|
|
127
107
|
for (const parsed of parsedConfigs) {
|
|
128
108
|
// Sequential on purpose: the first actor with a problem should be the one reported.
|
|
129
|
-
actorConfigs.push(
|
|
109
|
+
actorConfigs.push(loadActorConfig(parsed));
|
|
130
110
|
}
|
|
131
111
|
|
|
132
112
|
return selectActors(selection, actorConfigs);
|
|
@@ -55,6 +55,10 @@ export abstract class AbstractPath {
|
|
|
55
55
|
export class RelativeDir extends AbstractPath {
|
|
56
56
|
static readonly ROOT = new RelativeDir('.');
|
|
57
57
|
|
|
58
|
+
isRoot(): boolean {
|
|
59
|
+
return this.path === RelativeDir.ROOT.path;
|
|
60
|
+
}
|
|
61
|
+
|
|
58
62
|
joinDir(relativePath: string): RelativeDir {
|
|
59
63
|
return new RelativeDir(this.join(relativePath));
|
|
60
64
|
}
|
|
@@ -65,10 +69,24 @@ export class RelativeDir extends AbstractPath {
|
|
|
65
69
|
return new RelativeFile(this.join(relativePath));
|
|
66
70
|
}
|
|
67
71
|
|
|
72
|
+
relativePathTo(filePath: string): string {
|
|
73
|
+
// mainly for matching against .dockerignore since paths consumed by it are relative to the docker context dir
|
|
74
|
+
return posix.relative(this.path, filePath);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
containsPath(filePath: string): boolean {
|
|
78
|
+
try {
|
|
79
|
+
// should be OK even if not a directory (e.g. a file)
|
|
80
|
+
return this.contains(new RelativeDir(filePath));
|
|
81
|
+
} catch {
|
|
82
|
+
return false;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
68
86
|
// True when `other` is this directory itself or lies inside it.
|
|
69
87
|
contains(other: AbstractPath): boolean {
|
|
70
88
|
// the root contains every path
|
|
71
|
-
if (this.
|
|
89
|
+
if (this.isRoot()) return true;
|
|
72
90
|
return this.isEqualTo(other) || other.path.startsWith(`${this.path}/`);
|
|
73
91
|
}
|
|
74
92
|
|
|
@@ -133,14 +151,19 @@ export class RelativeFile extends AbstractPath {
|
|
|
133
151
|
return scope.contains(this);
|
|
134
152
|
}
|
|
135
153
|
|
|
154
|
+
exists(): boolean {
|
|
155
|
+
// `stat`, not `lstat`: a symlink must resolve to its target, matching what reading it does.
|
|
156
|
+
return statSync(this.path, { throwIfNoEntry: false })?.isFile() ?? false;
|
|
157
|
+
}
|
|
158
|
+
|
|
136
159
|
// Throws unless this path exists and is a file. Paths resolve against the process working
|
|
137
160
|
// directory, which is the repo root.
|
|
138
161
|
assertIsFile(): this {
|
|
139
|
-
|
|
162
|
+
if (this.exists()) return this;
|
|
163
|
+
|
|
140
164
|
const stats = statSync(this.path, { throwIfNoEntry: false });
|
|
141
165
|
if (!stats) throw new Error(`Expected file "${this}" to exist.`);
|
|
142
|
-
|
|
143
|
-
return this;
|
|
166
|
+
throw new Error(`Expected "${this}" to be a file.`);
|
|
144
167
|
}
|
|
145
168
|
}
|
|
146
169
|
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import type { ActorConfig, BuildData } from './types.js';
|
|
2
2
|
import type { SourceFile } from './utils.js';
|
|
3
|
-
|
|
4
|
-
export declare const
|
|
3
|
+
import type { RelativeDir } from './utils/path/repo-relative.js';
|
|
4
|
+
export declare const collectSourceFiles: (actorConfig: ActorConfig) => Promise<SourceFile[]>;
|
|
5
|
+
export declare const collectNonIgnoredFiles: (dockerContextDir: RelativeDir, repoRoot: string) => Promise<string[]>;
|
|
5
6
|
export declare const flattenMonorepoContext: (actorName: string, absActorDir: string, contextAbsDir: string, actorJson: Record<string, unknown>, keptContextFiles: string[]) => Promise<{
|
|
6
7
|
tempDir: string;
|
|
7
8
|
filePaths: string[];
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"build-from-local.d.ts","sourceRoot":"","sources":["../../bin/build-from-local.ts"],"names":[],"mappings":"AAYA,OAAO,KAAK,EAAE,WAAW,EAAE,SAAS,EAAE,MAAM,YAAY,CAAC;AACzD,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,YAAY,CAAC;
|
|
1
|
+
{"version":3,"file":"build-from-local.d.ts","sourceRoot":"","sources":["../../bin/build-from-local.ts"],"names":[],"mappings":"AAYA,OAAO,KAAK,EAAE,WAAW,EAAE,SAAS,EAAE,MAAM,YAAY,CAAC;AACzD,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,YAAY,CAAC;AAE7C,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,+BAA+B,CAAC;AASjE,eAAO,MAAM,kBAAkB,GAAU,aAAa,WAAW,KAAG,OAAO,CAAC,UAAU,EAAE,CA8BvF,CAAC;AAWF,eAAO,MAAM,sBAAsB,GAAU,kBAAkB,WAAW,EAAE,UAAU,MAAM,KAAG,OAAO,CAAC,MAAM,EAAE,CAc9G,CAAC;AAaF,eAAO,MAAM,sBAAsB,GAC/B,WAAW,MAAM,EACjB,aAAa,MAAM,EACnB,eAAe,MAAM,EACrB,WAAW,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAClC,kBAAkB,MAAM,EAAE,KAC3B,OAAO,CAAC;IAAE,OAAO,EAAE,MAAM,CAAC;IAAC,SAAS,EAAE,MAAM,EAAE,CAAA;CAAE,CAyClD,CAAC;AAcF,eAAO,MAAM,qBAAqB,GAC9B,aAAa,MAAM,EACnB,eAAe,MAAM,EACrB,SAAS,MAAM,EACf,WAAW,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,KACnC,OAAO,CAAC,IAAI,CAqBd,CAAC;AAEF,eAAO,MAAM,kBAAkB,GAAU,2BAGtC;IACC,YAAY,EAAE,WAAW,EAAE,CAAC;IAC5B,MAAM,EAAE,OAAO,CAAC;CACnB,KAAG,OAAO,CAAC,SAAS,EAAE,CAsBtB,CAAC"}
|