@akanjs/devkit 3.1.0-rc.1 → 3.1.0-rc.11
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/abstractDoc.ts +1 -1
- package/agentsIndex.test.ts +4 -1
- package/akanConfig/akanConfig.test.ts +51 -5
- package/akanConfig/akanConfig.ts +33 -14
- package/akanConfig/akanWorkspaceConfig.test.ts +6 -6
- package/akanConfig/akanWorkspaceConfig.ts +1 -1
- package/akanContext.ts +4 -4
- package/akanMcpContract.ts +1 -1
- package/applicationBuildRunner.test.ts +57 -2
- package/applicationBuildRunner.ts +15 -6
- package/artifact/implicitRootLayout.test.ts +2 -0
- package/artifact/implicitRootLayout.ts +1 -0
- package/biome.base.json +1 -11
- package/biomeBase.ts +1 -1
- package/cloud/cloudApi.ts +22 -15
- package/cloud/cloudHttpError.test.ts +32 -0
- package/cloud/cloudHttpError.ts +54 -0
- package/cloud/cloudUpdates.ts +138 -0
- package/cloud/configLock.ts +2 -1
- package/cloud/globalConfig.test.ts +17 -0
- package/cloud/globalConfig.ts +9 -2
- package/cloud/index.ts +3 -0
- package/cloud/pipelineSecrets.ts +114 -0
- package/cloud/updatePackUpload.test.ts +100 -45
- package/cloud/updatePackUpload.ts +38 -18
- package/codeAgent/agent/CodeAgentProxy.ts +2 -2
- package/codeAgent/agent/akanCodeModel.ts +1 -1
- package/codeAgent/agent/akanContextFiles.test.ts +3 -1
- package/codeAgent/tools/WebToolPack.ts +4 -4
- package/codeAgent/tools/akanCodePlugins.test.ts +20 -16
- package/commandDecorators/argMeta.ts +11 -2
- package/commandDecorators/command.ts +45 -10
- package/commandDecorators/commandBuilder.ts +6 -1
- package/commandDecorators/commandDecorators.test.ts +205 -4
- package/commandDecorators/exitCodeError.ts +15 -0
- package/commandDecorators/index.ts +1 -0
- package/createTunnel.test.ts +14 -0
- package/createTunnel.ts +3 -1
- package/dependencyScanner.ts +1 -1
- package/devkitUtils.test.ts +1 -1
- package/executors.test.ts +131 -3
- package/executors.ts +56 -36
- package/frontendBuild/autoImportSync.test.ts +3 -1
- package/frontendBuild/clientEntryDiscovery.ts +1 -1
- package/frontendBuild/csrArtifactBuilder.test.ts +30 -0
- package/frontendBuild/csrArtifactBuilder.ts +23 -4
- package/frontendBuild/csrDevPatcher.test.ts +4 -1
- package/frontendBuild/cssBlockParser.test.ts +123 -0
- package/frontendBuild/cssBlockParser.ts +132 -0
- package/frontendBuild/cssCompiler.test.ts +53 -0
- package/frontendBuild/cssCompiler.ts +8 -4
- package/frontendBuild/fontOptimizer.ts +2 -1
- package/frontendBuild/frontendBuild.test.ts +14 -0
- package/frontendBuild/hmrWatcher.ts +1 -1
- package/frontendBuild/pagesBundleBuilder.test.ts +98 -0
- package/frontendBuild/pagesBundleBuilder.ts +64 -9
- package/frontendBuild/routeClientBuilder.ts +1 -1
- package/frontendBuild/sourceMtimeIndex.test.ts +1 -1
- package/frontendBuild/themeValidator.test.ts +83 -2
- package/frontendBuild/themeValidator.ts +37 -15
- package/getRelatedCnsts.ts +2 -1
- package/incrementalBuilder/buildBatch.proc.ts +12 -7
- package/incrementalBuilder/incrementalBuilder.proc.test.ts +63 -0
- package/incrementalBuilder/incrementalBuilder.proc.ts +7 -5
- package/integration/devStabilityHarness.ts +5 -3
- package/lint/__fixtures__/no-deep-internal-import/bad.tsx +1 -0
- package/lint/__fixtures__/no-deep-internal-import/good.tsx +1 -0
- package/lint/__fixtures__/no-import-client-functions/bad.tsx +1 -0
- package/lint/__fixtures__/no-import-client-functions/good.tsx +1 -0
- package/lint/__fixtures__/no-import-client-in-server/bad.tsx +1 -0
- package/lint/__fixtures__/no-import-client-in-server/good.tsx +1 -0
- package/lint/__fixtures__/no-import-external-library/bad.tsx +1 -0
- package/lint/__fixtures__/no-import-external-library/good.tsx +1 -0
- package/lint/__fixtures__/no-import-external-library-in-test/bad.tsx +1 -0
- package/lint/__fixtures__/no-import-external-library-in-test/good.tsx +1 -0
- package/lint/__fixtures__/no-import-server-in-client/bad.tsx +1 -0
- package/lint/__fixtures__/no-import-server-in-client/good.tsx +1 -0
- package/lint/__fixtures__/no-init-fetch-in-client/store-file/bad.ts +1 -1
- package/lint/__fixtures__/no-static-in-object-light-model/bad.ts +2 -2
- package/mobile/desktopServerStage.test.ts +13 -0
- package/mobile/desktopServerStage.ts +5 -7
- package/mobile/index.ts +2 -0
- package/mobile/mobileTarget.ts +1 -2
- package/mobile/nativeApi.test.ts +26 -0
- package/mobile/nativeApi.ts +5 -2
- package/mobile/nativeApp.test.ts +135 -33
- package/mobile/nativeApp.ts +109 -73
- package/mobile/nativeConfig.test.ts +3 -0
- package/mobile/nativeConfig.ts +5 -3
- package/mobile/nativePublicEnv.test.ts +131 -0
- package/mobile/nativePublicEnv.ts +116 -0
- package/mobile/nativeWebDir.ts +3 -2
- package/mobile/serverBundleSecrets.test.ts +79 -0
- package/mobile/serverBundleSecrets.ts +60 -16
- package/mobile/updatePackFolder.test.ts +98 -0
- package/mobile/updatePackFolder.ts +131 -0
- package/mobile/updateUploader.test.ts +115 -22
- package/mobile/updateUploader.ts +77 -10
- package/package.json +20 -20
- package/piDependencyPins.test.ts +18 -19
- package/pipeline/PipelineCompiler.test.ts +157 -1
- package/pipeline/PipelineCompiler.ts +80 -46
- package/pipeline/PipelineDefinition.test.ts +9 -0
- package/pipeline/PipelineDefinition.ts +4 -3
- package/pipeline/PipelineDryRun.test.ts +27 -0
- package/pipeline/PipelineDryRun.ts +2 -2
- package/pipeline/PipelineInputReader.ts +157 -253
- package/pipeline/PipelineRules.ts +92 -0
- package/pipeline/index.ts +1 -0
- package/platformTest/LinuxDockerTarget.ts +2 -1
- package/platformTest/PlatformTestTarget.ts +7 -5
- package/platformTest/WindowsSshTarget.ts +2 -1
- package/qualityScanner.test.ts +5 -3
- package/qualityScanner.ts +1 -1
- package/recipeScanner.test.ts +9 -3
- package/scanInfo.ts +1 -1
- package/slicePlanner.ts +5 -3
- package/subspace.test.ts +90 -19
- package/subspace.ts +52 -500
- package/subspaceClone.ts +362 -0
- package/subspaceConfig.ts +1 -2
- package/subspaceReport.ts +162 -0
- package/testHelpers.ts +2 -0
- package/transforms/barrelImportsPlugin.ts +1 -1
- package/transforms/browserDirnamePlugin.test.ts +88 -0
- package/transforms/browserDirnamePlugin.ts +25 -0
- package/transforms/rscUseClientTransform.ts +2 -2
- package/transforms/serverEnvPlugin.test.ts +76 -0
- package/transforms/serverEnvPlugin.ts +9 -25
- package/transforms/serverEnvProfile.test.ts +189 -0
- package/transforms/serverEnvProfile.ts +324 -60
- package/transforms/transforms.test.ts +18 -2
- package/transforms/tsconfigPackageResolver.test.ts +9 -0
- package/typecheck/projectTypecheck.ts +6 -5
- package/types.ts +1 -0
- package/workflow/artifacts.ts +1 -1
- package/workflow/rolloutGate.test.ts +5 -1
- package/workflow/source.ts +2 -2
package/mobile/updateUploader.ts
CHANGED
|
@@ -1,4 +1,8 @@
|
|
|
1
|
+
import { readdir } from "node:fs/promises";
|
|
1
2
|
import path from "node:path";
|
|
3
|
+
import type { UpdateEnvelope, UpdateManifest } from "@akanjs/native/api";
|
|
4
|
+
import { isRecord, trimTrailingSlashes } from "akanjs/common";
|
|
5
|
+
import { UpdatePackFolder } from "./updatePackFolder";
|
|
2
6
|
|
|
3
7
|
/** What the uploader needs of an S3 client; Bun.S3Client is one. */
|
|
4
8
|
export interface UpdateBucket {
|
|
@@ -11,6 +15,15 @@ export interface UpdateUploadResult {
|
|
|
11
15
|
skipped: number;
|
|
12
16
|
}
|
|
13
17
|
|
|
18
|
+
/** The release one `publish-update` wrote, as it reports it. */
|
|
19
|
+
export interface PublishedUpdate {
|
|
20
|
+
/** `<updates>/<os>-<arch>` for a desktop app, `<updates>/<platform>` for a phone's web bundle. */
|
|
21
|
+
dir: string;
|
|
22
|
+
channel: string;
|
|
23
|
+
/** Its manifest's `bundle`, which tells it from the other manifests of the channel there. */
|
|
24
|
+
bundle: string;
|
|
25
|
+
}
|
|
26
|
+
|
|
14
27
|
//* A manifest names files that must already be there, so they go first and the manifests last, each signature before
|
|
15
28
|
//* its manifest as `publish-update` writes them. A release file never changes once published (its name carries its
|
|
16
29
|
//* hash), so one the bucket holds is skipped; a manifest is always written.
|
|
@@ -19,9 +32,9 @@ export class UpdateUploader {
|
|
|
19
32
|
|
|
20
33
|
/** `s3://bucket/prefix`: the credentials and endpoint come from S3_* (or AWS_*) in the environment. */
|
|
21
34
|
static parseTarget(target: string): { bucket: string; prefix: string } {
|
|
22
|
-
const match = /^s3:\/\/([^/]+)
|
|
35
|
+
const match = /^s3:\/\/([^/]+)(?:\/(.*))?$/.exec(target.trim());
|
|
23
36
|
if (!match?.[1]) throw new Error(`--upload takes s3://<bucket>/<prefix>, not ${JSON.stringify(target)}.`);
|
|
24
|
-
return { bucket: match[1], prefix: (match[2] ?? "")
|
|
37
|
+
return { bucket: match[1], prefix: trimTrailingSlashes(match[2] ?? "") };
|
|
25
38
|
}
|
|
26
39
|
|
|
27
40
|
static order(files: string[]): { releases: string[]; manifests: string[] } {
|
|
@@ -34,6 +47,64 @@ export class UpdateUploader {
|
|
|
34
47
|
};
|
|
35
48
|
}
|
|
36
49
|
|
|
50
|
+
//* Never the whole folder: pack-update's records sit there too, and so do manifests of other channels and native
|
|
51
|
+
//* APIs, which a later publish from another machine may have replaced.
|
|
52
|
+
static async releaseFiles(updatesDir: string, { dir, channel, bundle }: PublishedUpdate): Promise<string[]> {
|
|
53
|
+
const keyOf = (file: unknown) => UpdateUploader.#keyOf(updatesDir, dir, file);
|
|
54
|
+
const desktop = await UpdateUploader.#read<UpdateManifest>(path.join(dir, `${channel}.json`));
|
|
55
|
+
if (desktop?.bundle === bundle) {
|
|
56
|
+
const archive = desktop.archive?.url;
|
|
57
|
+
const patches = UpdateUploader.#field(desktop.patches, "url");
|
|
58
|
+
return [archive, ...patches, `${channel}.json.sig`, `${channel}.json`].map(keyOf);
|
|
59
|
+
}
|
|
60
|
+
const names = await readdir(path.join(dir, channel)).catch(() => [] as string[]);
|
|
61
|
+
for (const name of names.filter((name) => name.endsWith(".json"))) {
|
|
62
|
+
const envelope = await UpdateUploader.#read<UpdateEnvelope>(path.join(dir, channel, name));
|
|
63
|
+
const manifest =
|
|
64
|
+
typeof envelope?.manifest === "string"
|
|
65
|
+
? UpdateUploader.#parse<UpdateManifest>(Buffer.from(envelope.manifest, "base64").toString("utf8"))
|
|
66
|
+
: null;
|
|
67
|
+
if (manifest?.bundle !== bundle) continue;
|
|
68
|
+
const hashes = [...new Set(UpdateUploader.#field(manifest.files, "sha256"))];
|
|
69
|
+
if (!hashes.every((hash): hash is string => typeof hash === "string" && /^[0-9a-f]{64}$/.test(hash)))
|
|
70
|
+
throw new Error(
|
|
71
|
+
`${path.join(dir, channel, name)} lists a file whose sha256 is not 64 hex digits; nothing was uploaded.`,
|
|
72
|
+
);
|
|
73
|
+
return [...hashes.map((hash) => `${UpdatePackFolder.filesDir}/${hash}`), `${channel}/${name}`].map(keyOf);
|
|
74
|
+
}
|
|
75
|
+
throw new Error(`${dir} holds no ${channel} manifest of release ${bundle}; nothing was uploaded.`);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
static #keyOf(updatesDir: string, dir: string, file: unknown) {
|
|
79
|
+
const resolved = typeof file === "string" ? path.resolve(dir, file) : "";
|
|
80
|
+
const key = path.relative(updatesDir, resolved);
|
|
81
|
+
if (!resolved.startsWith(`${path.resolve(dir)}${path.sep}`) || key.startsWith("..") || path.isAbsolute(key))
|
|
82
|
+
throw new Error(
|
|
83
|
+
`The release in ${dir} names ${typeof file === "string" ? JSON.stringify(file) : "a file with no path"}, which is not inside that folder; nothing was uploaded.`,
|
|
84
|
+
);
|
|
85
|
+
return key.split(path.sep).join("/");
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
static #field(list: unknown, key: string): unknown[] {
|
|
89
|
+
return Array.isArray(list) ? list.map((item) => (isRecord(item) ? item[key] : undefined)) : [];
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
static async #read<T>(file: string) {
|
|
93
|
+
const text = await Bun.file(file)
|
|
94
|
+
.text()
|
|
95
|
+
.catch(() => "");
|
|
96
|
+
return UpdateUploader.#parse<T>(text);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
static #parse<T>(text: string): Partial<T> | null {
|
|
100
|
+
try {
|
|
101
|
+
const value: unknown = JSON.parse(text);
|
|
102
|
+
return isRecord(value) ? (value as Partial<T>) : null;
|
|
103
|
+
} catch {
|
|
104
|
+
return null;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
37
108
|
constructor(
|
|
38
109
|
readonly bucket: UpdateBucket,
|
|
39
110
|
readonly prefix: string,
|
|
@@ -44,12 +115,8 @@ export class UpdateUploader {
|
|
|
44
115
|
return new UpdateUploader(new Bun.S3Client({ bucket }), prefix);
|
|
45
116
|
}
|
|
46
117
|
|
|
47
|
-
async upload(
|
|
48
|
-
const
|
|
49
|
-
for await (const file of new Bun.Glob("**/*").scan({ cwd: dir, onlyFiles: true })) {
|
|
50
|
-
if (!file.endsWith(".tmp")) files.push(file.split(path.sep).join("/"));
|
|
51
|
-
}
|
|
52
|
-
const { releases, manifests } = UpdateUploader.order(files);
|
|
118
|
+
async upload(updatesDir: string, release: PublishedUpdate): Promise<UpdateUploadResult> {
|
|
119
|
+
const { releases, manifests } = UpdateUploader.order(await UpdateUploader.releaseFiles(updatesDir, release));
|
|
53
120
|
const keyOf = (file: string) => (this.prefix ? `${this.prefix}/${file}` : file);
|
|
54
121
|
let uploaded = 0;
|
|
55
122
|
let skipped = 0;
|
|
@@ -58,12 +125,12 @@ export class UpdateUploader {
|
|
|
58
125
|
skipped++;
|
|
59
126
|
continue;
|
|
60
127
|
}
|
|
61
|
-
await this.bucket.write(keyOf(file), Bun.file(path.join(
|
|
128
|
+
await this.bucket.write(keyOf(file), Bun.file(path.join(updatesDir, file)));
|
|
62
129
|
uploaded++;
|
|
63
130
|
}
|
|
64
131
|
for (const file of manifests) {
|
|
65
132
|
const type = file.endsWith(".json") ? "application/json" : "text/plain";
|
|
66
|
-
await this.bucket.write(keyOf(file), Bun.file(path.join(
|
|
133
|
+
await this.bucket.write(keyOf(file), Bun.file(path.join(updatesDir, file)), { type });
|
|
67
134
|
uploaded++;
|
|
68
135
|
}
|
|
69
136
|
return { uploaded, skipped };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@akanjs/devkit",
|
|
3
|
-
"version": "3.1.0-rc.
|
|
3
|
+
"version": "3.1.0-rc.11",
|
|
4
4
|
"sourceType": "module",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"publishConfig": {
|
|
@@ -40,31 +40,31 @@
|
|
|
40
40
|
"./*": "./*.ts"
|
|
41
41
|
},
|
|
42
42
|
"dependencies": {
|
|
43
|
-
"@earendil-works/
|
|
44
|
-
"@earendil-works/pi-
|
|
45
|
-
"@earendil-works/pi-
|
|
46
|
-
"@earendil-works/pi-coding-agent": "
|
|
47
|
-
"@earendil-works/pi-
|
|
48
|
-
"@earendil-works/pi-tui": "
|
|
49
|
-
"@inquirer/prompts": "^8.
|
|
50
|
-
"@tailwindcss/node": "^4.3.
|
|
51
|
-
"akanjs": "3.1.0-rc.
|
|
52
|
-
"chalk": "^
|
|
53
|
-
"commander": "^
|
|
54
|
-
"dayjs": "^1.11.
|
|
55
|
-
"fontaine": "^0.
|
|
43
|
+
"@earendil-works/pi-agent-core": "1.3.0",
|
|
44
|
+
"@earendil-works/pi-ai": "1.3.0",
|
|
45
|
+
"@earendil-works/pi-codemode": "1.3.0",
|
|
46
|
+
"@earendil-works/pi-coding-agent": "1.3.0",
|
|
47
|
+
"@earendil-works/pi-mcp": "1.3.0",
|
|
48
|
+
"@earendil-works/pi-tui": "1.3.0",
|
|
49
|
+
"@inquirer/prompts": "^8.7.3",
|
|
50
|
+
"@tailwindcss/node": "^4.3.3",
|
|
51
|
+
"akanjs": "3.1.0-rc.11",
|
|
52
|
+
"chalk": "^6.0.1",
|
|
53
|
+
"commander": "^15.0.0",
|
|
54
|
+
"dayjs": "^1.11.23",
|
|
55
|
+
"fontaine": "^1.0.1",
|
|
56
56
|
"fonteditor-core": "^2.6.3",
|
|
57
|
-
"ignore": "^7.0.
|
|
58
|
-
"ora": "^9.4.
|
|
57
|
+
"ignore": "^7.0.12",
|
|
58
|
+
"ora": "^9.4.1",
|
|
59
59
|
"ssh2": "^1.17.0",
|
|
60
|
-
"subset-font": "^2.
|
|
60
|
+
"subset-font": "^2.9.0",
|
|
61
61
|
"tailwind-scrollbar": "^4.0.2",
|
|
62
|
-
"tailwindcss": "^4.3.
|
|
63
|
-
"typebox": "1.
|
|
62
|
+
"tailwindcss": "^4.3.3",
|
|
63
|
+
"typebox": "1.3.37",
|
|
64
64
|
"typescript": "^6.0.3"
|
|
65
65
|
},
|
|
66
66
|
"peerDependencies": {
|
|
67
|
-
"react": "19.3.
|
|
67
|
+
"react": "19.3.1"
|
|
68
68
|
},
|
|
69
69
|
"peerDependenciesMeta": {
|
|
70
70
|
"react": {
|
package/piDependencyPins.test.ts
CHANGED
|
@@ -2,31 +2,24 @@ import { describe, expect, test } from "bun:test";
|
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
import type { PackageJson } from "@akanjs/devkit/types";
|
|
4
4
|
|
|
5
|
-
//?
|
|
6
|
-
//?
|
|
5
|
+
//? PackageRunner re-declares pi's sibling pins because neither our `overrides` nor pi's own ranges reach a
|
|
6
|
+
//? consumer's install. The source of truth is pi's `dependencies`: since 1.3.0 it pins every sibling exactly, where
|
|
7
|
+
//? 0.86.1 floated `^0.86.1` and only the (now removed) `npm-shrinkwrap.json` held the resolved version.
|
|
7
8
|
|
|
8
9
|
const repoRoot = path.resolve(import.meta.dir, "../../..");
|
|
9
10
|
const piName = "@earendil-works/pi-coding-agent";
|
|
10
11
|
const piDir = `${repoRoot}/node_modules/${piName}`;
|
|
11
12
|
const publishedPackages = ["pkgs/@akanjs/devkit/package.json", "pkgs/@akanjs/cli/package.json"];
|
|
12
13
|
|
|
13
|
-
interface Shrinkwrap {
|
|
14
|
-
packages: Record<string, { version?: string }>;
|
|
15
|
-
}
|
|
16
|
-
|
|
17
14
|
const readJson = async <T>(file: string) => (await Bun.file(file).json()) as T;
|
|
18
15
|
|
|
19
|
-
const
|
|
20
|
-
const {
|
|
21
|
-
|
|
22
|
-
const entries = Object.entries(packages)
|
|
23
|
-
.filter(([key]) => /^node_modules\/@earendil-works\/[^/]+$/.test(key))
|
|
24
|
-
.map(([key, entry]) => [key.replace(/^node_modules\//, ""), entry.version ?? ""] as const);
|
|
25
|
-
return Object.fromEntries(entries);
|
|
16
|
+
const siblingEntries = async () => {
|
|
17
|
+
const { dependencies = {} } = await readJson<PackageJson>(`${piDir}/package.json`);
|
|
18
|
+
return Object.entries(dependencies).filter(([name]) => /^@earendil-works\/[^/]+$/.test(name));
|
|
26
19
|
};
|
|
27
20
|
|
|
28
21
|
describe("pi dependency pins", () => {
|
|
29
|
-
test("the
|
|
22
|
+
test("the package read here belongs to the coding agent we publish", async () => {
|
|
30
23
|
const installed = await readJson<PackageJson>(`${piDir}/package.json`);
|
|
31
24
|
for (const file of publishedPackages) {
|
|
32
25
|
const { dependencies = {} } = await readJson<PackageJson>(`${repoRoot}/${file}`);
|
|
@@ -34,13 +27,19 @@ describe("pi dependency pins", () => {
|
|
|
34
27
|
}
|
|
35
28
|
});
|
|
36
29
|
|
|
37
|
-
test("
|
|
38
|
-
const
|
|
39
|
-
expect(
|
|
30
|
+
test("the coding agent pins each sibling exactly", async () => {
|
|
31
|
+
const siblings = await siblingEntries();
|
|
32
|
+
expect(siblings.length).toBeGreaterThan(0);
|
|
33
|
+
const floating = siblings.filter(([, version]) => !/^\d+\.\d+\.\d+/.test(version));
|
|
34
|
+
expect(floating).toEqual([]);
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
test("every sibling pi package is published at the version the coding agent pins", async () => {
|
|
38
|
+
const siblings = await siblingEntries();
|
|
40
39
|
for (const file of publishedPackages) {
|
|
41
40
|
const { dependencies = {} } = await readJson<PackageJson>(`${repoRoot}/${file}`);
|
|
42
|
-
const declared = Object.fromEntries(
|
|
43
|
-
expect({ file, ...declared }).toEqual({ file, ...
|
|
41
|
+
const declared = Object.fromEntries(siblings.map(([name]) => [name, dependencies[name]]));
|
|
42
|
+
expect({ file, ...declared }).toEqual({ file, ...Object.fromEntries(siblings) });
|
|
44
43
|
}
|
|
45
44
|
});
|
|
46
45
|
});
|
|
@@ -337,7 +337,28 @@ describe("PipelineCompiler", () => {
|
|
|
337
337
|
]);
|
|
338
338
|
const e2e = plan.phases.at(-1);
|
|
339
339
|
expect(e2e).toMatchObject({ id: "e2e", timeoutSeconds: 3600, milestone: "finish" });
|
|
340
|
-
expect(e2e?.stages.map((stage) => [stage.id, stage.name, stage.app])).toEqual([["e2e
|
|
340
|
+
expect(e2e?.stages.map((stage) => [stage.id, stage.name, stage.app])).toEqual([["e2e.apps.api", "api", "api"]]);
|
|
341
|
+
});
|
|
342
|
+
|
|
343
|
+
test("a matrix copy's id never collides with an app's own stage or a post step", () => {
|
|
344
|
+
const stageIds = (plan: PipelinePlan) => plan.phases.flatMap((phase) => phase.stages.map((stage) => stage.id));
|
|
345
|
+
const plan = planOf(
|
|
346
|
+
compile({
|
|
347
|
+
workspace: { after: [{ name: "Web", matrix: "apps", sh: "bun run e2e $AKAN_CI_APP" }] },
|
|
348
|
+
apps: { web: { stages: [{ use: "build" }, { name: "API", sh: "bun run test:api" }] } },
|
|
349
|
+
}),
|
|
350
|
+
);
|
|
351
|
+
expect(stageIds(plan)).toEqual(expect.arrayContaining(["web:api", "web.apps.api", "web.apps.web"]));
|
|
352
|
+
expect(new Set(stageIds(plan)).size).toBe(stageIds(plan).length);
|
|
353
|
+
const post = planOf(
|
|
354
|
+
compile({
|
|
355
|
+
ctx: context({ apps: ["success"] }),
|
|
356
|
+
workspaceApps: ["success"],
|
|
357
|
+
workspace: { after: [{ name: "Post", matrix: "apps", sh: "x" }], post: { success: ["y"] } },
|
|
358
|
+
}),
|
|
359
|
+
);
|
|
360
|
+
expect(stageIds(post)).toContain("post.apps.success");
|
|
361
|
+
expect(post.post.success.map((step) => step.id)).toEqual(["post.success/1"]);
|
|
341
362
|
});
|
|
342
363
|
|
|
343
364
|
test("env is workspace, then app, then stage, then step; secrets are each step's union", () => {
|
|
@@ -571,6 +592,10 @@ describe("PipelineCompiler", () => {
|
|
|
571
592
|
expect(codes(appResult({ stages: [{ name: "x", sh: "x", secrets: ["lower"] }] }))).toContain(
|
|
572
593
|
"pipeline.secret.name",
|
|
573
594
|
);
|
|
595
|
+
const pasted = appResult({ stages: [{ name: "x", sh: "x", secrets: ["NPM_TOKEN", "npm_live_value"] }] });
|
|
596
|
+
const secretName = pasted.diagnostics.find((diagnostic) => diagnostic.code === "pipeline.secret.name");
|
|
597
|
+
expect(secretName?.path).toEndWith(".secrets[1]");
|
|
598
|
+
expect(secretName?.message).not.toContain("npm_live_value");
|
|
574
599
|
expect(codes(appResult({ stages: [{ name: "x", steps: [{ sh: "x", cwd: "../up" }] }] }))).toContain(
|
|
575
600
|
"pipeline.cwd.outside",
|
|
576
601
|
);
|
|
@@ -604,6 +629,30 @@ describe("PipelineCompiler", () => {
|
|
|
604
629
|
);
|
|
605
630
|
});
|
|
606
631
|
|
|
632
|
+
test("a parallel branch may not take the id of a post list's steps", () => {
|
|
633
|
+
const refused = compile({
|
|
634
|
+
workspace: { before: [{ name: "Post", parallel: [{ name: "Success", sh: "x" }] }], post: { success: ["y"] } },
|
|
635
|
+
});
|
|
636
|
+
expect(refused.ok).toBe(false);
|
|
637
|
+
expect(refused.diagnostics).toEqual([
|
|
638
|
+
expect.objectContaining({
|
|
639
|
+
code: "pipeline.id.reserved",
|
|
640
|
+
file: "akan.config.ts",
|
|
641
|
+
path: "pipeline.before[0].parallel[0].name",
|
|
642
|
+
}),
|
|
643
|
+
]);
|
|
644
|
+
expect(
|
|
645
|
+
compile({
|
|
646
|
+
workspace: { after: [{ id: "post", name: "Checks", parallel: [{ id: "always", name: "A", sh: "x" }] }] },
|
|
647
|
+
}).diagnostics,
|
|
648
|
+
).toEqual([expect.objectContaining({ code: "pipeline.id.reserved", path: "pipeline.after[0].parallel[0].id" })]);
|
|
649
|
+
const otherBranch = compile({
|
|
650
|
+
workspace: { before: [{ name: "Post", parallel: [{ name: "Lint", sh: "x" }] }], post: { success: ["y"] } },
|
|
651
|
+
});
|
|
652
|
+
expect(otherBranch.ok).toBe(true);
|
|
653
|
+
expect(compile({ workspace: { after: [{ name: "Post", sh: "x" }], post: { success: ["y"] } } }).ok).toBe(true);
|
|
654
|
+
});
|
|
655
|
+
|
|
607
656
|
test("loading and sampling", () => {
|
|
608
657
|
const thrown = appResult(() => {
|
|
609
658
|
throw new Error("boom");
|
|
@@ -620,6 +669,113 @@ describe("PipelineCompiler", () => {
|
|
|
620
669
|
expect(codes(sampled)).toContain("pipeline.function.sampled");
|
|
621
670
|
});
|
|
622
671
|
|
|
672
|
+
test("a function returning nothing, an async one, and a falsy pipeline are errors, never a default", async () => {
|
|
673
|
+
const refused = (result: PipelinePlanResult, file: string) => {
|
|
674
|
+
expect(result.ok).toBe(false);
|
|
675
|
+
expect(result.diagnostics).toContainEqual(
|
|
676
|
+
expect.objectContaining({ severity: "error", code: "pipeline.schema.invalid", file, path: "pipeline" }),
|
|
677
|
+
);
|
|
678
|
+
return result.diagnostics.map((diagnostic) => diagnostic.message).join("\n");
|
|
679
|
+
};
|
|
680
|
+
const onMainOnly = ((ctx: { branch: string }) =>
|
|
681
|
+
ctx.branch === "main" ? { stages: [{ use: "build" }] } : undefined) as unknown as AppPipelineConfig;
|
|
682
|
+
expect(appResult(onMainOnly).ok).toBe(true);
|
|
683
|
+
const develop = compile({ ctx: context({ branch: "develop", apps: ["web"] }), apps: { web: onMainOnly } });
|
|
684
|
+
expect(refused(develop, "apps/web/akan.config.ts")).toContain("returned nothing");
|
|
685
|
+
expect(refused(compile({ workspace: (() => undefined) as never }), "akan.config.ts")).toContain(
|
|
686
|
+
"returned nothing",
|
|
687
|
+
);
|
|
688
|
+
expect(refused(compile({ workspace: (async () => ({ before: [] })) as never }), "akan.config.ts")).toContain(
|
|
689
|
+
"Promise",
|
|
690
|
+
);
|
|
691
|
+
const rejecting = (async () => {
|
|
692
|
+
throw new Error("boom");
|
|
693
|
+
}) as unknown as AppPipelineConfig;
|
|
694
|
+
expect(refused(appResult(rejecting), "apps/web/akan.config.ts")).toContain("Promise");
|
|
695
|
+
await Bun.sleep(1);
|
|
696
|
+
expect(refused(appResult(false as never), "apps/web/akan.config.ts")).toContain("must be an object");
|
|
697
|
+
expect(refused(compile({ workspace: 0 as never }), "akan.config.ts")).toContain("must be an object");
|
|
698
|
+
});
|
|
699
|
+
|
|
700
|
+
test("a plan larger than an executor reads, as written, is refused", () => {
|
|
701
|
+
const steps = Array.from({ length: 20 }, () => "x".repeat(60_000));
|
|
702
|
+
const result = appResult({ stages: [{ use: "build" }, { name: "Big", steps }] });
|
|
703
|
+
expect(result.ok).toBe(false);
|
|
704
|
+
expect(result.diagnostics).toContainEqual(
|
|
705
|
+
expect.objectContaining({ code: "pipeline.limit.exceeded", file: null, path: null }),
|
|
706
|
+
);
|
|
707
|
+
});
|
|
708
|
+
|
|
709
|
+
test("conditions", () => {
|
|
710
|
+
expect(appResult({ stages: [{ use: "build", when: { trigger: [] } }] }).diagnostics).toContainEqual(
|
|
711
|
+
expect.objectContaining({ code: "pipeline.schema.invalid", path: "pipeline.stages[0].when.trigger" }),
|
|
712
|
+
);
|
|
713
|
+
const branch = Array.from({ length: 6 }, (_, idx) => `release/${String(idx).repeat(240)}`);
|
|
714
|
+
const plan = planOf(
|
|
715
|
+
compile({
|
|
716
|
+
ctx: context({ branch: "develop", env: "develop", apps: ["web"] }),
|
|
717
|
+
apps: { web: { stages: [{ use: "build", when: { branch } }] } },
|
|
718
|
+
}),
|
|
719
|
+
);
|
|
720
|
+
const reason = plan.phases[0]?.stages[0]?.skip?.reason ?? "";
|
|
721
|
+
expect(reason.startsWith('branch "develop" does not match "release/')).toBe(true);
|
|
722
|
+
expect(reason.endsWith("…")).toBe(true);
|
|
723
|
+
expect(reason.length).toBeLessThanOrEqual(1024);
|
|
724
|
+
});
|
|
725
|
+
|
|
726
|
+
test("a value the plan would refuse is reported where it was written", () => {
|
|
727
|
+
expect(
|
|
728
|
+
appResult({ stages: [{ use: "build" }, { name: "x", sh: "x", env: { BIG: "x".repeat(64 * 1024 + 1) } }] })
|
|
729
|
+
.diagnostics,
|
|
730
|
+
).toContainEqual(
|
|
731
|
+
expect.objectContaining({ code: "pipeline.limit.exceeded", path: "pipeline.stages[1].env.BIG" }),
|
|
732
|
+
);
|
|
733
|
+
const unset = compile({ workspace: { before: [{ name: "Checks", parallel: undefined as never }] } });
|
|
734
|
+
expect(unset.ok).toBe(false);
|
|
735
|
+
expect(unset.diagnostics).toEqual([
|
|
736
|
+
expect.objectContaining({
|
|
737
|
+
code: "pipeline.schema.invalid",
|
|
738
|
+
file: "akan.config.ts",
|
|
739
|
+
path: "pipeline.before[0].parallel",
|
|
740
|
+
}),
|
|
741
|
+
]);
|
|
742
|
+
});
|
|
743
|
+
|
|
744
|
+
test("a key set to undefined is the key left out", () => {
|
|
745
|
+
const stage = { name: "Test", sh: "bun test", use: undefined, parallel: undefined, matrix: undefined };
|
|
746
|
+
const plan = planOf(
|
|
747
|
+
compile({
|
|
748
|
+
workspace: {
|
|
749
|
+
before: [stage as never, { name: "Checks", parallel: [{ name: "Lint", sh: "x", use: undefined }] }],
|
|
750
|
+
},
|
|
751
|
+
apps: { web: { stages: [{ use: "build", targets: undefined } as never, stage as never] } },
|
|
752
|
+
}),
|
|
753
|
+
);
|
|
754
|
+
expect(phaseIds(plan)).toEqual(["test", "checks", "build", "after-build", "docker", "deploy"]);
|
|
755
|
+
expect(appResult({ stages: [{ use: undefined } as never] }).diagnostics).toContainEqual(
|
|
756
|
+
expect.objectContaining({ code: "pipeline.schema.invalid", path: "pipeline.stages[0].use" }),
|
|
757
|
+
);
|
|
758
|
+
});
|
|
759
|
+
|
|
760
|
+
test("a value JSON cannot hold is reported, never thrown", () => {
|
|
761
|
+
const cyclic: Record<string, unknown> = {};
|
|
762
|
+
cyclic.self = cyclic;
|
|
763
|
+
const result = appResult({ stages: [{ use: "build", timeout: 10n as never }, { use: cyclic as never }] });
|
|
764
|
+
expect(result.ok).toBe(false);
|
|
765
|
+
expect(result.diagnostics.map((diagnostic) => [diagnostic.code, diagnostic.message.split(" ")[0]])).toEqual([
|
|
766
|
+
["pipeline.duration.invalid", "10n"],
|
|
767
|
+
["pipeline.schema.invalid", "[object"],
|
|
768
|
+
]);
|
|
769
|
+
});
|
|
770
|
+
|
|
771
|
+
test("a long first line names its step without splitting a surrogate pair", () => {
|
|
772
|
+
const plan = planOf(
|
|
773
|
+
appResult({ stages: [{ use: "build" }, { name: "Emoji", sh: `${"a".repeat(78)}${"😀".repeat(5)}` }] }),
|
|
774
|
+
);
|
|
775
|
+
const name = plan.phases.find((phase) => phase.id === "after-build")?.stages[0]?.steps[0]?.name;
|
|
776
|
+
expect(name).toBe(`${"a".repeat(78)}…`);
|
|
777
|
+
});
|
|
778
|
+
|
|
623
779
|
test("milestones that move backwards are a warning", () => {
|
|
624
780
|
const result = appResult({
|
|
625
781
|
stages: [{ use: "build" }, { use: "docker" }, { name: "Late", sh: "x", milestone: "verify" }],
|
|
@@ -25,19 +25,21 @@ import {
|
|
|
25
25
|
pipelineActions,
|
|
26
26
|
pipelineMilestones,
|
|
27
27
|
pipelineOtaPlatforms,
|
|
28
|
+
pipelinePlanLimits,
|
|
28
29
|
pipelinePlanVersion,
|
|
30
|
+
toError,
|
|
29
31
|
} from "akanjs/common";
|
|
30
|
-
import {
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
} from "./
|
|
32
|
+
import { PipelineInputReader } from "./PipelineInputReader";
|
|
33
|
+
import type {
|
|
34
|
+
PipelineActionStageRule,
|
|
35
|
+
PipelineAppRule,
|
|
36
|
+
PipelinePostRule,
|
|
37
|
+
PipelineShellStageRule,
|
|
38
|
+
PipelineStepRule,
|
|
39
|
+
PipelineWhenRule,
|
|
40
|
+
PipelineWorkspaceRule,
|
|
41
|
+
PipelineWorkspaceStageRule,
|
|
42
|
+
} from "./PipelineRules";
|
|
41
43
|
|
|
42
44
|
export interface PipelineSourceFile<T> {
|
|
43
45
|
/** Relative to the workspace root, with `/`. */
|
|
@@ -59,7 +61,6 @@ export interface PipelineCompileInput {
|
|
|
59
61
|
akanVersion: string;
|
|
60
62
|
/** Function forms run against a sample context (`validate`), which is said once per file. */
|
|
61
63
|
sampled?: boolean;
|
|
62
|
-
/** What loading the files already found. */
|
|
63
64
|
diagnostics?: readonly PipelineDiagnostic[];
|
|
64
65
|
}
|
|
65
66
|
|
|
@@ -87,7 +88,6 @@ interface StageOptions {
|
|
|
87
88
|
timeoutSeconds?: number | null;
|
|
88
89
|
}
|
|
89
90
|
|
|
90
|
-
/** Composes the workspace's and each app's rules into one plan for one run. */
|
|
91
91
|
export class PipelineCompiler {
|
|
92
92
|
static readonly #slotAfter = {
|
|
93
93
|
build: "after-build",
|
|
@@ -112,6 +112,8 @@ export class PipelineCompiler {
|
|
|
112
112
|
static readonly #reservedPhaseId =
|
|
113
113
|
/^(?:build|docker|deploy|ota|(?:before-build|after-build|after-docker|after-deploy|after-ota)(?:-\d+)?)$/;
|
|
114
114
|
static readonly #actionStepNames = { build: "akan build", docker: "image", deploy: "deploy", ota: "ota" } as const;
|
|
115
|
+
//? PipelinePlanSchema reads at most 1024 characters of a skip reason.
|
|
116
|
+
static readonly #reasonChars = 1024;
|
|
115
117
|
|
|
116
118
|
static compile(input: PipelineCompileInput): PipelinePlanResult {
|
|
117
119
|
return new PipelineCompiler(input).#compile();
|
|
@@ -130,13 +132,9 @@ export class PipelineCompiler {
|
|
|
130
132
|
const { context, workspaceApps } = this.#input;
|
|
131
133
|
this.#checkContext();
|
|
132
134
|
const pipelineContext: PipelineContext = {
|
|
133
|
-
|
|
134
|
-
env: context.env,
|
|
135
|
+
...context,
|
|
135
136
|
apps: [...context.apps],
|
|
136
137
|
workspaceApps: [...workspaceApps],
|
|
137
|
-
trigger: context.trigger,
|
|
138
|
-
buildNum: context.buildNum,
|
|
139
|
-
commit: context.commit,
|
|
140
138
|
};
|
|
141
139
|
const workspace = this.#readWorkspace(pipelineContext);
|
|
142
140
|
const lanes = new Map<string, AppLane>();
|
|
@@ -164,11 +162,23 @@ export class PipelineCompiler {
|
|
|
164
162
|
try {
|
|
165
163
|
PipelinePlanSchema.parse(plan);
|
|
166
164
|
} catch (error) {
|
|
167
|
-
const message = error
|
|
165
|
+
const message = toError(error).message;
|
|
168
166
|
this.#error(message.includes("at most") ? "pipeline.limit.exceeded" : "pipeline.internal", message, null, null);
|
|
169
167
|
return this.#failed();
|
|
170
168
|
}
|
|
171
|
-
|
|
169
|
+
const result: PipelinePlanResult = { ok: true, plan, diagnostics: this.#diagnostics };
|
|
170
|
+
//? Sized as `akan pipeline plan --out` writes it, since that text is what an executor's parseText bounds.
|
|
171
|
+
const bytes = new TextEncoder().encode(`${JSON.stringify(result, null, 2)}\n`).length;
|
|
172
|
+
if (bytes > pipelinePlanLimits.planBytes) {
|
|
173
|
+
this.#error(
|
|
174
|
+
"pipeline.limit.exceeded",
|
|
175
|
+
`the plan is ${bytes} bytes as written, and an executor reads at most ${pipelinePlanLimits.planBytes}`,
|
|
176
|
+
null,
|
|
177
|
+
null,
|
|
178
|
+
);
|
|
179
|
+
return this.#failed();
|
|
180
|
+
}
|
|
181
|
+
return result;
|
|
172
182
|
}
|
|
173
183
|
|
|
174
184
|
#checkContext() {
|
|
@@ -184,11 +194,11 @@ export class PipelineCompiler {
|
|
|
184
194
|
|
|
185
195
|
#readWorkspace(context: PipelineContext): PipelineWorkspaceRule | null {
|
|
186
196
|
const source = this.#input.workspace;
|
|
187
|
-
if (
|
|
188
|
-
const
|
|
189
|
-
if (
|
|
197
|
+
if (source?.pipeline == null) return null;
|
|
198
|
+
const evaluated = this.#evaluate(source.pipeline, context, source.file);
|
|
199
|
+
if (!evaluated) return null;
|
|
190
200
|
const reader = new PipelineInputReader(source.file);
|
|
191
|
-
const rule = reader.workspace(value);
|
|
201
|
+
const rule = reader.workspace(evaluated.value);
|
|
192
202
|
this.#diagnostics.push(...reader.diagnostics);
|
|
193
203
|
return rule;
|
|
194
204
|
}
|
|
@@ -197,34 +207,55 @@ export class PipelineCompiler {
|
|
|
197
207
|
const source = this.#input.apps.get(app);
|
|
198
208
|
if (!source) return null;
|
|
199
209
|
const appContext: AppPipelineContext = { ...context, app };
|
|
200
|
-
const
|
|
201
|
-
|
|
210
|
+
const evaluated =
|
|
211
|
+
source.pipeline == null
|
|
212
|
+
? { value: defaultAppPipeline }
|
|
213
|
+
: this.#evaluate(source.pipeline, appContext, source.file);
|
|
214
|
+
if (!evaluated) return null;
|
|
202
215
|
const reader = new PipelineInputReader(source.file);
|
|
203
|
-
const rule = reader.app(value);
|
|
216
|
+
const rule = reader.app(evaluated.value);
|
|
204
217
|
this.#diagnostics.push(...reader.diagnostics);
|
|
205
218
|
return rule ? this.#lane(app, rule, source.file) : null;
|
|
206
219
|
}
|
|
207
220
|
|
|
208
|
-
#evaluate<C>(config: unknown, context: C, file: string): unknown {
|
|
209
|
-
|
|
210
|
-
if (
|
|
211
|
-
this.#
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
221
|
+
#evaluate<C>(config: unknown, context: C, file: string): { value: unknown } | null {
|
|
222
|
+
let value = config;
|
|
223
|
+
if (typeof config === "function") {
|
|
224
|
+
if (this.#input.sampled)
|
|
225
|
+
this.#warning(
|
|
226
|
+
"pipeline.function.sampled",
|
|
227
|
+
"a function form was checked against one sample context; other branches, triggers or apps may differ",
|
|
228
|
+
file,
|
|
229
|
+
"pipeline",
|
|
230
|
+
);
|
|
231
|
+
try {
|
|
232
|
+
value = (config as (context: C) => unknown)(context);
|
|
233
|
+
} catch (error) {
|
|
234
|
+
this.#error("pipeline.load.failed", `the pipeline function threw: ${toError(error).message}`, file, "pipeline");
|
|
235
|
+
return null;
|
|
236
|
+
}
|
|
237
|
+
if (value === undefined) {
|
|
238
|
+
this.#error(
|
|
239
|
+
"pipeline.schema.invalid",
|
|
240
|
+
"the pipeline function returned nothing for this run: return a pipeline on every path",
|
|
241
|
+
file,
|
|
242
|
+
"pipeline",
|
|
243
|
+
);
|
|
244
|
+
return null;
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
if (typeof (value as PromiseLike<unknown> | null)?.then === "function") {
|
|
248
|
+
//? Handled so a rejection cannot end the CLI before it writes the envelope.
|
|
249
|
+
void Promise.resolve(value).catch(() => undefined);
|
|
220
250
|
this.#error(
|
|
221
|
-
"pipeline.
|
|
222
|
-
|
|
251
|
+
"pipeline.schema.invalid",
|
|
252
|
+
"is a Promise, which the planner cannot wait for: return the pipeline itself, not from an async function",
|
|
223
253
|
file,
|
|
224
254
|
"pipeline",
|
|
225
255
|
);
|
|
226
|
-
return
|
|
256
|
+
return null;
|
|
227
257
|
}
|
|
258
|
+
return { value };
|
|
228
259
|
}
|
|
229
260
|
|
|
230
261
|
#lane(app: string, rule: PipelineAppRule, file: string): AppLane {
|
|
@@ -552,7 +583,9 @@ export class PipelineCompiler {
|
|
|
552
583
|
maxParallel: stage.maxParallel,
|
|
553
584
|
failFast: false,
|
|
554
585
|
timeoutSeconds: stage.timeoutSeconds,
|
|
555
|
-
stages: apps.map((app) =>
|
|
586
|
+
stages: apps.map((app) =>
|
|
587
|
+
this.#shellStage(copy, { id: `${stage.id}.apps.${app}`, name: app, app, milestone, env }),
|
|
588
|
+
),
|
|
556
589
|
},
|
|
557
590
|
];
|
|
558
591
|
}
|
|
@@ -603,10 +636,11 @@ export class PipelineCompiler {
|
|
|
603
636
|
|
|
604
637
|
#skipOf(when: PipelineWhenRule): PipelineSkip | null {
|
|
605
638
|
const { branch, trigger } = this.#input.context;
|
|
639
|
+
const skip = (reason: string) => ({ reason: PipelineInputReader.clip(reason, PipelineCompiler.#reasonChars) });
|
|
606
640
|
if (when.branch && !PipelinePattern.matches(when.branch, branch))
|
|
607
|
-
return
|
|
641
|
+
return skip(`branch "${branch}" does not match ${when.branch.map((glob) => `"${glob}"`).join(", ")}`);
|
|
608
642
|
if (when.trigger && !when.trigger.includes(trigger))
|
|
609
|
-
return
|
|
643
|
+
return skip(`trigger "${trigger}" is not ${when.trigger.map((name) => `"${name}"`).join(" or ")}`);
|
|
610
644
|
return null;
|
|
611
645
|
}
|
|
612
646
|
|
|
@@ -615,7 +649,7 @@ export class PipelineCompiler {
|
|
|
615
649
|
const sourceOf = (source: PipelineSourceFile<unknown>): PipelinePlanSource => ({
|
|
616
650
|
file: source.file,
|
|
617
651
|
sha256: source.sha256,
|
|
618
|
-
declared: source.pipeline
|
|
652
|
+
declared: source.pipeline != null,
|
|
619
653
|
});
|
|
620
654
|
return {
|
|
621
655
|
workspace: workspace ? sourceOf(workspace) : null,
|