@invarn/cibuild 2.5.6 → 2.5.9
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/dist/cli.cjs +10 -10
- package/dist/src/commands/build.d.ts +13 -2
- package/dist/src/commands/build.d.ts.map +1 -1
- package/dist/src/commands/build.js +32 -6
- package/dist/src/commands/build.test.js +62 -0
- package/dist/src/shared/git.d.ts +25 -0
- package/dist/src/shared/git.d.ts.map +1 -0
- package/dist/src/shared/git.js +53 -0
- package/dist/src/shared/git.test.d.ts +9 -0
- package/dist/src/shared/git.test.d.ts.map +1 -0
- package/dist/src/shared/git.test.js +66 -0
- package/dist/src/yaml/steps/base.d.ts.map +1 -1
- package/dist/src/yaml/steps/base.js +11 -4
- package/dist/src/yaml/steps/cache-push-daemon.test.d.ts +27 -0
- package/dist/src/yaml/steps/cache-push-daemon.test.d.ts.map +1 -0
- package/dist/src/yaml/steps/cache-push-daemon.test.js +132 -0
- package/dist/src/yaml/steps/cache.d.ts.map +1 -1
- package/dist/src/yaml/steps/cache.js +54 -13
- package/dist/src/yaml/steps/envstore-loader.test.d.ts +15 -0
- package/dist/src/yaml/steps/envstore-loader.test.d.ts.map +1 -0
- package/dist/src/yaml/steps/envstore-loader.test.js +143 -0
- package/dist/src/yaml/types.d.ts +6 -0
- package/dist/src/yaml/types.d.ts.map +1 -1
- package/package.json +1 -1
|
@@ -38,8 +38,19 @@ export interface IosSetupOptions {
|
|
|
38
38
|
appStoreDeploy: boolean;
|
|
39
39
|
bundleId: string;
|
|
40
40
|
}
|
|
41
|
-
|
|
42
|
-
|
|
41
|
+
/**
|
|
42
|
+
* The `trigger_map` block appended to every generated pipeline.
|
|
43
|
+
*
|
|
44
|
+
* Without it the pipeline is inert on the Invarn side: `dispatchRepoEvent`
|
|
45
|
+
* skips any pipeline whose YAML declares no trigger_map, so a scaffolded repo
|
|
46
|
+
* receives pushes and builds nothing, with no error to explain it.
|
|
47
|
+
*
|
|
48
|
+
* `release` is deliberately absent — it deploys to Google Play / App Store,
|
|
49
|
+
* and a webhook must never reach it.
|
|
50
|
+
*/
|
|
51
|
+
export declare function triggerMapBlock(defaultBranch?: string): string;
|
|
52
|
+
export declare function generateAndroidPipeline(javaVersion?: number, setup?: SetupOptions, variants?: WorkflowVariants, cacheTechnology?: "gradle" | "kmm", metaNamespace?: 'invarn' | 'cibuild.io', defaultBranch?: string): string;
|
|
53
|
+
export declare function generateIosPipeline(projectPath: string, setup: IosSetupOptions, variants: IosWorkflowVariants, metaNamespace?: 'invarn' | 'cibuild.io', defaultBranch?: string): string;
|
|
43
54
|
export declare function handleBuildCommand(detectMobileProjectRoot: (dir: string) => "android" | "ios" | "kmm" | null, options?: {
|
|
44
55
|
createPipelinesDir?: boolean;
|
|
45
56
|
nonInteractive?: boolean;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"build.d.ts","sourceRoot":"","sources":["../../../src/commands/build.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"build.d.ts","sourceRoot":"","sources":["../../../src/commands/build.ts"],"names":[],"mappings":"AAYA,MAAM,WAAW,YAAY;IAC3B,QAAQ,EAAE,OAAO,CAAC;IAClB,kBAAkB,EAAE,OAAO,CAAC;IAC5B,cAAc,EAAE,OAAO,CAAC;IACxB,gBAAgB,EAAE,OAAO,CAAC;IAC1B,qBAAqB,EAAE,MAAM,CAAC;IAC9B,oFAAoF;IACpF,YAAY,EAAE,KAAK,GAAG,KAAK,CAAC;IAC5B,8FAA8F;IAC9F,aAAa,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;CACvC;AAED,MAAM,WAAW,qBAAqB;IACpC,8EAA8E;IAC9E,OAAO,EAAE,MAAM,CAAC;IAChB,+FAA+F;IAC/F,SAAS,EAAE,MAAM,CAAC;IAClB,6FAA6F;IAC7F,UAAU,EAAE,MAAM,CAAC;CACpB;AAED,MAAM,WAAW,gBAAgB;IAC/B,OAAO,EAAE,qBAAqB,CAAC;IAC/B,WAAW,EAAE,qBAAqB,CAAC;IACnC,OAAO,EAAE,qBAAqB,CAAC;CAChC;AAiBD,MAAM,WAAW,uBAAuB;IACtC,MAAM,EAAE,MAAM,CAAC;IACf,aAAa,EAAE,MAAM,CAAC;IACtB,kBAAkB,EAAE,MAAM,CAAC;CAC5B;AAED,MAAM,WAAW,mBAAmB;IAClC,OAAO,EAAE,uBAAuB,CAAC;IACjC,WAAW,EAAE,uBAAuB,CAAC;IACrC,OAAO,EAAE,uBAAuB,CAAC;CAClC;AAED,MAAM,WAAW,eAAe;IAC9B,SAAS,EAAE,OAAO,CAAC;IACnB,WAAW,EAAE,OAAO,CAAC;IACrB,cAAc,EAAE,OAAO,CAAC;IACxB,QAAQ,EAAE,MAAM,CAAC;CAClB;AAoJD;;;;;;;;;GASG;AACH,wBAAgB,eAAe,CAAC,aAAa,GAAE,MAAgC,GAAG,MAAM,CAcvF;AAED,wBAAgB,uBAAuB,CACrC,WAAW,GAAE,MAAW,EACxB,KAAK,GAAE,YAAgL,EACvL,QAAQ,GAAE,gBAAmC,EAC7C,eAAe,GAAE,QAAQ,GAAG,KAAgB,EAC5C,aAAa,GAAE,QAAQ,GAAG,YAA2B,EACrD,aAAa,GAAE,MAAgC,GAC9C,MAAM,CA2NR;AAED,wBAAgB,mBAAmB,CACjC,WAAW,EAAE,MAAM,EACnB,KAAK,EAAE,eAAe,EACtB,QAAQ,EAAE,mBAAmB,EAC7B,aAAa,GAAE,QAAQ,GAAG,YAA2B,EACrD,aAAa,GAAE,MAAgC,GAC9C,MAAM,CA4LR;AAiVD,wBAAsB,kBAAkB,CACtC,uBAAuB,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,SAAS,GAAG,KAAK,GAAG,KAAK,GAAG,IAAI,EAC1E,OAAO,GAAE;IACP,kBAAkB,CAAC,EAAE,OAAO,CAAC;IAC7B,cAAc,CAAC,EAAE,OAAO,CAAC;IACzB,aAAa,CAAC,EAAE,QAAQ,GAAG,YAAY,CAAC;CACpC,GACL,OAAO,CAAC,IAAI,CAAC,CA4Bf"}
|
|
@@ -6,6 +6,7 @@ import { scanIosProject, formatIosScanResult } from "./ios-scanner.js";
|
|
|
6
6
|
import { SecretsManager } from "../yaml/secrets-manager.js";
|
|
7
7
|
import { collectFileSecret } from "./file-secret-collector.js";
|
|
8
8
|
import { generateGitHubActionsWorkflow } from "./github-workflow.js";
|
|
9
|
+
import { detectDefaultBranch, FALLBACK_DEFAULT_BRANCH } from "../shared/git.js";
|
|
9
10
|
function variantFromName(name) {
|
|
10
11
|
const buildType = name.charAt(0).toUpperCase() + name.slice(1);
|
|
11
12
|
return { variant: name, buildType, gradleTask: `assemble${buildType}` };
|
|
@@ -122,7 +123,32 @@ function detectSetupNeeds(warnings) {
|
|
|
122
123
|
artifactType: 'apk',
|
|
123
124
|
};
|
|
124
125
|
}
|
|
125
|
-
|
|
126
|
+
/**
|
|
127
|
+
* The `trigger_map` block appended to every generated pipeline.
|
|
128
|
+
*
|
|
129
|
+
* Without it the pipeline is inert on the Invarn side: `dispatchRepoEvent`
|
|
130
|
+
* skips any pipeline whose YAML declares no trigger_map, so a scaffolded repo
|
|
131
|
+
* receives pushes and builds nothing, with no error to explain it.
|
|
132
|
+
*
|
|
133
|
+
* `release` is deliberately absent — it deploys to Google Play / App Store,
|
|
134
|
+
* and a webhook must never reach it.
|
|
135
|
+
*/
|
|
136
|
+
export function triggerMapBlock(defaultBranch = FALLBACK_DEFAULT_BRANCH) {
|
|
137
|
+
const branch = defaultBranch.trim() || FALLBACK_DEFAULT_BRANCH;
|
|
138
|
+
// JSON.stringify gives a valid double-quoted YAML scalar for any branch
|
|
139
|
+
// name git will accept. Quoting is not cosmetic: a branch called `no` or
|
|
140
|
+
// `y` parses as a boolean unquoted, and one called `1.0` as a number.
|
|
141
|
+
return `
|
|
142
|
+
# Which GitHub events build which workflow. Without this block the pipeline
|
|
143
|
+
# is never triggered by a push or a pull request.
|
|
144
|
+
trigger_map:
|
|
145
|
+
- push_branch: ${JSON.stringify(branch)}
|
|
146
|
+
workflow: primary
|
|
147
|
+
- pull_request_target_branch: "*"
|
|
148
|
+
workflow: pull-request
|
|
149
|
+
`;
|
|
150
|
+
}
|
|
151
|
+
export function generateAndroidPipeline(javaVersion = 17, setup = { keystore: false, keystoreProperties: false, googleServices: false, googlePlayDeploy: false, googlePlayPackageName: '', artifactType: 'apk', keystorePaths: {} }, variants = DEFAULT_VARIANTS, cacheTechnology = "gradle", metaNamespace = 'cibuild.io', defaultBranch = FALLBACK_DEFAULT_BRANCH) {
|
|
126
152
|
const keystoreStepFor = (wf) => {
|
|
127
153
|
if (!setup.keystore)
|
|
128
154
|
return "";
|
|
@@ -331,9 +357,9 @@ ${googlePlayStep}
|
|
|
331
357
|
echo "${releaseArtifactLabel}: \$artifact"
|
|
332
358
|
done
|
|
333
359
|
fi
|
|
334
|
-
`;
|
|
360
|
+
${triggerMapBlock(defaultBranch)}`;
|
|
335
361
|
}
|
|
336
|
-
export function generateIosPipeline(projectPath, setup, variants, metaNamespace = 'cibuild.io') {
|
|
362
|
+
export function generateIosPipeline(projectPath, setup, variants, metaNamespace = 'cibuild.io', defaultBranch = FALLBACK_DEFAULT_BRANCH) {
|
|
337
363
|
const { primary: pv, pullRequest: prv, release: rv } = variants;
|
|
338
364
|
// Global scheme is the primary scheme (used as default across workflows)
|
|
339
365
|
const globalScheme = pv.scheme;
|
|
@@ -508,7 +534,7 @@ ${appStoreDeployStep}
|
|
|
508
534
|
if [ -n "\$CIBUILD_XCARCHIVE_PATH" ]; then
|
|
509
535
|
echo "Archive: \$CIBUILD_XCARCHIVE_PATH"
|
|
510
536
|
fi
|
|
511
|
-
`;
|
|
537
|
+
${triggerMapBlock(defaultBranch)}`;
|
|
512
538
|
}
|
|
513
539
|
/**
|
|
514
540
|
* Prints a hint listing env vars / Gradle properties detected during scanning
|
|
@@ -771,7 +797,7 @@ async function handleIosBuildCommand(cwd, options = {}) {
|
|
|
771
797
|
}
|
|
772
798
|
}
|
|
773
799
|
// 7. Generate and write the pipeline
|
|
774
|
-
const yaml = generateIosPipeline(iosScanResult.projectPath, iosSetupOptions, iosVariants, options.metaNamespace);
|
|
800
|
+
const yaml = generateIosPipeline(iosScanResult.projectPath, iosSetupOptions, iosVariants, options.metaNamespace, detectDefaultBranch());
|
|
775
801
|
writeFileSync(outputPath, yaml, "utf-8");
|
|
776
802
|
console.log("\n✅ Generated .ci/pipelines/cibuild.yml");
|
|
777
803
|
console.log(" Platform: iOS");
|
|
@@ -1042,7 +1068,7 @@ async function handleAndroidBuildCommand(cwd, options, cacheTechnology) {
|
|
|
1042
1068
|
// that exact JDK — a newer JDK can cross-compile. Enforce a minimum of 17
|
|
1043
1069
|
// since ubuntu-latest runners ship with JDK 17+ and JDK <17 is unavailable.
|
|
1044
1070
|
const javaVersion = Math.max(scanResult.detectedJavaVersion ?? 17, 17);
|
|
1045
|
-
const yaml = generateAndroidPipeline(javaVersion, setupOptions, variants, cacheTechnology, options.metaNamespace);
|
|
1071
|
+
const yaml = generateAndroidPipeline(javaVersion, setupOptions, variants, cacheTechnology, options.metaNamespace, detectDefaultBranch());
|
|
1046
1072
|
writeFileSync(outputPath, yaml, "utf-8");
|
|
1047
1073
|
console.log("\n✅ Generated .ci/pipelines/cibuild.yml");
|
|
1048
1074
|
console.log(` Platform: ${platformLabel}`);
|
|
@@ -10,7 +10,9 @@
|
|
|
10
10
|
* `metaNamespace: 'invarn'` and get `meta.invarn:`.
|
|
11
11
|
*/
|
|
12
12
|
import { describe, test, expect } from '@jest/globals';
|
|
13
|
+
import { load } from 'js-yaml';
|
|
13
14
|
import { generateIosPipeline, generateAndroidPipeline, } from './build.js';
|
|
15
|
+
const parse = (yaml) => load(yaml);
|
|
14
16
|
const iosSetup = {
|
|
15
17
|
cocoaPods: false,
|
|
16
18
|
codeSigning: false,
|
|
@@ -56,4 +58,64 @@ describe('generateAndroidPipeline — meta namespace', () => {
|
|
|
56
58
|
expect(yaml).not.toContain('meta:\n cibuild.io:\n');
|
|
57
59
|
});
|
|
58
60
|
});
|
|
61
|
+
/**
|
|
62
|
+
* A generated pipeline with no `trigger_map` is inert on the Invarn side:
|
|
63
|
+
* the webhook handler skips any pipeline whose YAML has none, so a repo
|
|
64
|
+
* scaffolded by `ci build` receives pushes and builds nothing, with no
|
|
65
|
+
* error anywhere to explain it.
|
|
66
|
+
*
|
|
67
|
+
* Both generators emit `primary`, `pull-request` and `release`. Pushes go to
|
|
68
|
+
* `primary`, pull requests to `pull-request`, and `release` stays manual —
|
|
69
|
+
* it deploys to Google Play / App Store and must never fire off a push.
|
|
70
|
+
*/
|
|
71
|
+
describe.each([
|
|
72
|
+
['generateIosPipeline', (branch) => generateIosPipeline('./MyApp', iosSetup, iosVariants, 'cibuild.io', branch)],
|
|
73
|
+
['generateAndroidPipeline', (branch) => generateAndroidPipeline(17, undefined, undefined, 'gradle', 'cibuild.io', branch)],
|
|
74
|
+
])('%s — trigger_map', (_name, generate) => {
|
|
75
|
+
test('emits a trigger_map', () => {
|
|
76
|
+
const doc = parse(generate());
|
|
77
|
+
expect(Array.isArray(doc.trigger_map)).toBe(true);
|
|
78
|
+
expect(doc.trigger_map.length).toBeGreaterThan(0);
|
|
79
|
+
});
|
|
80
|
+
test('every rule names a workflow the pipeline actually declares', () => {
|
|
81
|
+
const doc = parse(generate());
|
|
82
|
+
const declared = Object.keys(doc.workflows ?? {});
|
|
83
|
+
for (const rule of doc.trigger_map) {
|
|
84
|
+
expect(declared).toContain(rule.workflow);
|
|
85
|
+
}
|
|
86
|
+
});
|
|
87
|
+
test('routes a push to primary and a pull request to pull-request', () => {
|
|
88
|
+
const doc = parse(generate());
|
|
89
|
+
const push = doc.trigger_map.find((r) => r.push_branch !== undefined);
|
|
90
|
+
const pr = doc.trigger_map.find((r) => r.pull_request_target_branch !== undefined);
|
|
91
|
+
expect(push?.workflow).toBe('primary');
|
|
92
|
+
expect(pr?.workflow).toBe('pull-request');
|
|
93
|
+
});
|
|
94
|
+
test('never fires release off a webhook', () => {
|
|
95
|
+
const doc = parse(generate());
|
|
96
|
+
expect(doc.trigger_map.map((r) => r.workflow)).not.toContain('release');
|
|
97
|
+
});
|
|
98
|
+
test('defaults the push branch to main', () => {
|
|
99
|
+
const doc = parse(generate());
|
|
100
|
+
const push = doc.trigger_map.find((r) => r.push_branch !== undefined);
|
|
101
|
+
expect(push?.push_branch).toBe('main');
|
|
102
|
+
});
|
|
103
|
+
test('keys the push branch to the branch it is given', () => {
|
|
104
|
+
const doc = parse(generate('develop'));
|
|
105
|
+
const push = doc.trigger_map.find((r) => r.push_branch !== undefined);
|
|
106
|
+
expect(push?.push_branch).toBe('develop');
|
|
107
|
+
});
|
|
108
|
+
test('survives a branch name containing a slash', () => {
|
|
109
|
+
const doc = parse(generate('release/1.0'));
|
|
110
|
+
const push = doc.trigger_map.find((r) => r.push_branch !== undefined);
|
|
111
|
+
expect(push?.push_branch).toBe('release/1.0');
|
|
112
|
+
});
|
|
113
|
+
test('still parses when the branch name would otherwise read as a YAML bool', () => {
|
|
114
|
+
// `push_branch: no` parses as `false` unquoted. Git allows a branch
|
|
115
|
+
// called `no`, so the value has to be quoted on the way out.
|
|
116
|
+
const doc = parse(generate('no'));
|
|
117
|
+
const push = doc.trigger_map.find((r) => r.push_branch !== undefined);
|
|
118
|
+
expect(push?.push_branch).toBe('no');
|
|
119
|
+
});
|
|
120
|
+
});
|
|
59
121
|
//# sourceMappingURL=build.test.js.map
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Git probes used while scaffolding a pipeline.
|
|
3
|
+
*
|
|
4
|
+
* Scaffolding runs inside the user's checkout, so the repository itself is
|
|
5
|
+
* the best available source of truth for values the generated YAML has to
|
|
6
|
+
* hardcode. Everything here is best-effort: a scaffold must still succeed in
|
|
7
|
+
* a directory that is not a git repository at all.
|
|
8
|
+
*/
|
|
9
|
+
/** What we key a `trigger_map` to when git can't tell us anything better. */
|
|
10
|
+
export declare const FALLBACK_DEFAULT_BRANCH = "main";
|
|
11
|
+
/**
|
|
12
|
+
* The branch a generated `trigger_map` should build on push.
|
|
13
|
+
*
|
|
14
|
+
* Order matters. The remote's HEAD is what GitHub calls the repository's
|
|
15
|
+
* default branch, and it is what the webhook will report on a merge — so it
|
|
16
|
+
* wins over whatever happens to be checked out. Only when there is no remote
|
|
17
|
+
* (a repo that has not been pushed anywhere yet) do we fall back to the
|
|
18
|
+
* current branch, and only when that is unavailable too — no repo, or a
|
|
19
|
+
* detached HEAD — do we guess.
|
|
20
|
+
*
|
|
21
|
+
* Guessing wrong is not silent: the pipeline is written to disk where the
|
|
22
|
+
* user can read and correct the branch before committing it.
|
|
23
|
+
*/
|
|
24
|
+
export declare function detectDefaultBranch(cwd?: string): string;
|
|
25
|
+
//# sourceMappingURL=git.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"git.d.ts","sourceRoot":"","sources":["../../../src/shared/git.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAIH,6EAA6E;AAC7E,eAAO,MAAM,uBAAuB,SAAS,CAAC;AAc9C;;;;;;;;;;;;GAYG;AACH,wBAAgB,mBAAmB,CAAC,GAAG,GAAE,MAAsB,GAAG,MAAM,CAevE"}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Git probes used while scaffolding a pipeline.
|
|
3
|
+
*
|
|
4
|
+
* Scaffolding runs inside the user's checkout, so the repository itself is
|
|
5
|
+
* the best available source of truth for values the generated YAML has to
|
|
6
|
+
* hardcode. Everything here is best-effort: a scaffold must still succeed in
|
|
7
|
+
* a directory that is not a git repository at all.
|
|
8
|
+
*/
|
|
9
|
+
import { execFileSync } from 'node:child_process';
|
|
10
|
+
/** What we key a `trigger_map` to when git can't tell us anything better. */
|
|
11
|
+
export const FALLBACK_DEFAULT_BRANCH = 'main';
|
|
12
|
+
function git(cwd, args) {
|
|
13
|
+
try {
|
|
14
|
+
return execFileSync('git', args, {
|
|
15
|
+
cwd,
|
|
16
|
+
encoding: 'utf8',
|
|
17
|
+
stdio: ['ignore', 'pipe', 'ignore'],
|
|
18
|
+
}).trim();
|
|
19
|
+
}
|
|
20
|
+
catch {
|
|
21
|
+
return null;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* The branch a generated `trigger_map` should build on push.
|
|
26
|
+
*
|
|
27
|
+
* Order matters. The remote's HEAD is what GitHub calls the repository's
|
|
28
|
+
* default branch, and it is what the webhook will report on a merge — so it
|
|
29
|
+
* wins over whatever happens to be checked out. Only when there is no remote
|
|
30
|
+
* (a repo that has not been pushed anywhere yet) do we fall back to the
|
|
31
|
+
* current branch, and only when that is unavailable too — no repo, or a
|
|
32
|
+
* detached HEAD — do we guess.
|
|
33
|
+
*
|
|
34
|
+
* Guessing wrong is not silent: the pipeline is written to disk where the
|
|
35
|
+
* user can read and correct the branch before committing it.
|
|
36
|
+
*/
|
|
37
|
+
export function detectDefaultBranch(cwd = process.cwd()) {
|
|
38
|
+
// `origin/main` → `main`. Absent unless the remote HEAD ref is set locally,
|
|
39
|
+
// which `git clone` does and `git init` does not.
|
|
40
|
+
const remoteHead = git(cwd, ['symbolic-ref', '--short', 'refs/remotes/origin/HEAD']);
|
|
41
|
+
if (remoteHead) {
|
|
42
|
+
const branch = remoteHead.replace(/^origin\//, '').trim();
|
|
43
|
+
if (branch)
|
|
44
|
+
return branch;
|
|
45
|
+
}
|
|
46
|
+
// `--abbrev-ref HEAD` answers the literal string "HEAD" on a detached
|
|
47
|
+
// checkout, which is not a branch name and must not become one.
|
|
48
|
+
const current = git(cwd, ['rev-parse', '--abbrev-ref', 'HEAD']);
|
|
49
|
+
if (current && current !== 'HEAD')
|
|
50
|
+
return current;
|
|
51
|
+
return FALLBACK_DEFAULT_BRANCH;
|
|
52
|
+
}
|
|
53
|
+
//# sourceMappingURL=git.js.map
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Unit tests for the default-branch probe.
|
|
3
|
+
*
|
|
4
|
+
* The probe shells out to git, so these drive it against real throwaway
|
|
5
|
+
* repositories in a temp dir rather than mocking child_process — the whole
|
|
6
|
+
* point of the helper is that it agrees with what git actually reports.
|
|
7
|
+
*/
|
|
8
|
+
export {};
|
|
9
|
+
//# sourceMappingURL=git.test.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"git.test.d.ts","sourceRoot":"","sources":["../../../src/shared/git.test.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG"}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Unit tests for the default-branch probe.
|
|
3
|
+
*
|
|
4
|
+
* The probe shells out to git, so these drive it against real throwaway
|
|
5
|
+
* repositories in a temp dir rather than mocking child_process — the whole
|
|
6
|
+
* point of the helper is that it agrees with what git actually reports.
|
|
7
|
+
*/
|
|
8
|
+
import { describe, test, expect, beforeAll, afterAll } from '@jest/globals';
|
|
9
|
+
import { execFileSync } from 'node:child_process';
|
|
10
|
+
import { mkdtempSync, rmSync, writeFileSync } from 'node:fs';
|
|
11
|
+
import { tmpdir } from 'node:os';
|
|
12
|
+
import { join } from 'node:path';
|
|
13
|
+
import { detectDefaultBranch } from './git.js';
|
|
14
|
+
function git(cwd, ...args) {
|
|
15
|
+
execFileSync('git', args, { cwd, stdio: 'ignore' });
|
|
16
|
+
}
|
|
17
|
+
/** A repo with one commit on `branch`, and no remote. */
|
|
18
|
+
function makeRepo(root, name, branch) {
|
|
19
|
+
const dir = join(root, name);
|
|
20
|
+
execFileSync('mkdir', ['-p', dir]);
|
|
21
|
+
git(dir, 'init', '--quiet', `--initial-branch=${branch}`);
|
|
22
|
+
git(dir, 'config', 'user.email', 'test@example.com');
|
|
23
|
+
git(dir, 'config', 'user.name', 'Test');
|
|
24
|
+
writeFileSync(join(dir, 'README.md'), '# test\n');
|
|
25
|
+
git(dir, 'add', 'README.md');
|
|
26
|
+
git(dir, 'commit', '--quiet', '-m', 'init');
|
|
27
|
+
return dir;
|
|
28
|
+
}
|
|
29
|
+
describe('detectDefaultBranch', () => {
|
|
30
|
+
let root;
|
|
31
|
+
beforeAll(() => {
|
|
32
|
+
root = mkdtempSync(join(tmpdir(), 'cibuild-git-'));
|
|
33
|
+
});
|
|
34
|
+
afterAll(() => {
|
|
35
|
+
rmSync(root, { recursive: true, force: true });
|
|
36
|
+
});
|
|
37
|
+
test('reads the current branch when there is no remote', () => {
|
|
38
|
+
expect(detectDefaultBranch(makeRepo(root, 'plain', 'main'))).toBe('main');
|
|
39
|
+
});
|
|
40
|
+
test('reads a non-main current branch', () => {
|
|
41
|
+
expect(detectDefaultBranch(makeRepo(root, 'legacy', 'master'))).toBe('master');
|
|
42
|
+
});
|
|
43
|
+
test('prefers the remote HEAD over the checked-out branch', () => {
|
|
44
|
+
// A clone sitting on a topic branch must still key its trigger to the
|
|
45
|
+
// branch the remote considers default — otherwise the generated pipeline
|
|
46
|
+
// would only ever build whatever branch happened to be checked out at
|
|
47
|
+
// scaffold time.
|
|
48
|
+
const origin = makeRepo(root, 'origin-repo', 'develop');
|
|
49
|
+
const clone = join(root, 'cloned');
|
|
50
|
+
execFileSync('git', ['clone', '--quiet', origin, clone], { stdio: 'ignore' });
|
|
51
|
+
git(clone, 'checkout', '--quiet', '-b', 'feature/whatever');
|
|
52
|
+
expect(detectDefaultBranch(clone)).toBe('develop');
|
|
53
|
+
});
|
|
54
|
+
test('falls back to main outside a git repository', () => {
|
|
55
|
+
const notARepo = join(root, 'bare-dir');
|
|
56
|
+
execFileSync('mkdir', ['-p', notARepo]);
|
|
57
|
+
expect(detectDefaultBranch(notARepo)).toBe('main');
|
|
58
|
+
});
|
|
59
|
+
test('falls back to main on a detached HEAD with no remote', () => {
|
|
60
|
+
const dir = makeRepo(root, 'detached', 'main');
|
|
61
|
+
const sha = execFileSync('git', ['rev-parse', 'HEAD'], { cwd: dir, encoding: 'utf8' }).trim();
|
|
62
|
+
git(dir, 'checkout', '--quiet', sha);
|
|
63
|
+
expect(detectDefaultBranch(dir)).toBe('main');
|
|
64
|
+
});
|
|
65
|
+
});
|
|
66
|
+
//# sourceMappingURL=git.test.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"base.d.ts","sourceRoot":"","sources":["../../../../src/yaml/steps/base.ts"],"names":[],"mappings":"AAAA;;GAEG;AAIH,OAAO,KAAK,EAAE,OAAO,EAAE,QAAQ,EAAE,MAAM,gBAAgB,CAAC;AACxD,OAAO,KAAK,EACV,qBAAqB,EACrB,qBAAqB,EACrB,kBAAkB,EAClB,UAAU,EACX,MAAM,wBAAwB,CAAC;AAEhC;;GAEG;AACH,MAAM,WAAW,oBAAoB;IACnC,UAAU,EAAE,MAAM,CAAC;IACnB,GAAG,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAC5B,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAED;;;;GAIG;AACH,MAAM,WAAW,YAAY;IAC3B;;;;;;OAMG;IACH,OAAO,CAAC,MAAM,EAAE,GAAG,EAAE,GAAG,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,EAAE,MAAM,EAAE,QAAQ,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC;IAEtF;;;;;;;OAOG;IACH,yBAAyB,CAAC,CACxB,MAAM,EAAE,GAAG,EACX,GAAG,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,EAC3B,MAAM,EAAE,QAAQ,GACf,qBAAqB,EAAE,CAAC;IAE3B;;;;OAIG;IACH,UAAU,CAAC,IAAI,UAAU,EAAE,CAAC;IAE5B;;;OAGG;IACH,oBAAoB,CAAC,IAAI,OAAO,CAAC;CAClC;AAED;;GAEG;AACH,8BAAsB,gBAAiB,YAAW,YAAY;IAC5D;;OAEG;IACH,QAAQ,CAAC,OAAO,CAAC,MAAM,EAAE,GAAG,EAAE,GAAG,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,EAAE,MAAM,EAAE,QAAQ,GAAG,OAAO,CAAC,OAAO,CAAC;IAE9F;;;;;OAKG;IACH,SAAS,CAAC,gBAAgB,CAAC,MAAM,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,OAAO;IAUjE;;;;;OAKG;IACH,SAAS,CAAC,gBAAgB,CAAC,MAAM,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,OAAO;IASjE;;;;;OAKG;IACH,SAAS,CAAC,cAAc,CAAC,MAAM,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,OAAO;IAS/D;;;;;OAKG;IACH,SAAS,CAAC,cAAc,CAAC,MAAM,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,OAAO;IAS/D;;;;;;OAMG;IACH,SAAS,CAAC,QAAQ,CAAC,CAAC,EAAE,MAAM,EAAE,GAAG,EAAE,GAAG,EAAE,MAAM,EAAE,YAAY,EAAE,CAAC,GAAG,CAAC;IAOnE;;;;;;;OAOG;IACH,SAAS,CAAC,gBAAgB,CAAC,MAAM,EAAE,GAAG,EAAE,GAAG,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,GAAG;IAS3E;;;;OAIG;IACH,SAAS,CAAC,UAAU,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM;IAKzC;;;;;;OAMG;IACH,SAAS,CAAC,iBAAiB,CAAC,GAAG,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,GAAG,MAAM;IAgB7E;;;;;;OAMG;IACH,SAAS,CAAC,gBAAgB,CAAC,aAAa,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,MAAM;
|
|
1
|
+
{"version":3,"file":"base.d.ts","sourceRoot":"","sources":["../../../../src/yaml/steps/base.ts"],"names":[],"mappings":"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"}
|
|
@@ -133,12 +133,19 @@ set -o pipefail # Catch errors in pipes
|
|
|
133
133
|
|
|
134
134
|
# Step: ${stepName}
|
|
135
135
|
|
|
136
|
-
# Load environment variables from envman store (set by previous steps)
|
|
136
|
+
# Load environment variables from envman store (set by previous steps).
|
|
137
|
+
# Records are terminated by NUL, not by newline. An environment variable may
|
|
138
|
+
# hold a newline -- a commit message body, a PEM key, a JSON blob -- but never
|
|
139
|
+
# a NUL, so no value can be read back as a further record. The key is then
|
|
140
|
+
# checked against the shell's identifier grammar as a second, independent
|
|
141
|
+
# defence: even a malformed entry can neither be exported nor fail the step.
|
|
137
142
|
ENVSTORE_FILE="\${ENVMAN_ENVSTORE_PATH:-.ci/.envstore.json}"
|
|
138
143
|
if [ -f "$ENVSTORE_FILE" ]; then
|
|
139
|
-
while IFS='=' read -r key value; do
|
|
140
|
-
[
|
|
141
|
-
|
|
144
|
+
while IFS='=' read -r -d '' key value; do
|
|
145
|
+
if [[ "$key" =~ ^[A-Za-z_][A-Za-z0-9_]*$ ]]; then
|
|
146
|
+
export "$key=$value"
|
|
147
|
+
fi
|
|
148
|
+
done < <(node -e "const fs=require('fs');const NUL=String.fromCharCode(0);try{const s=JSON.parse(fs.readFileSync('$ENVSTORE_FILE','utf-8'));(s.envs||[]).forEach(e=>process.stdout.write(e.key+'='+e.value+NUL))}catch(err){}" 2>/dev/null || true)
|
|
142
149
|
fi
|
|
143
150
|
|
|
144
151
|
${scriptContent}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `cache-push` with an explicit `cache_key`, over the daemon.
|
|
3
|
+
*
|
|
4
|
+
* Every curated Invarn template uses the explicit-key form — `cache_key:
|
|
5
|
+
* pods-{{checksum "Podfile.lock"}}-$CIBUILD_GIT_BRANCH` and its Gradle/KMM
|
|
6
|
+
* siblings — for both halves of the pair. The pull side assigns the shell
|
|
7
|
+
* variable the daemon URL interpolates; the push side did not, and there is
|
|
8
|
+
* no `set -u`, so `$CACHE_KEY` expanded to nothing and the upload went to
|
|
9
|
+
* `<daemon>/cache/.tar.zst`. The daemon requires at least one character
|
|
10
|
+
* before `.tar.zst` and answers 400, which is exactly what the first real
|
|
11
|
+
* `invarn setup` proof logged on 2026-08-30:
|
|
12
|
+
*
|
|
13
|
+
* curl: (22) The requested URL returned error: 400
|
|
14
|
+
* Warning: failed to upload cache to the daemon
|
|
15
|
+
* [step_6] ✓ Success (109ms)
|
|
16
|
+
*
|
|
17
|
+
* Nothing was ever stored, so the pull could never hit, and every iOS build
|
|
18
|
+
* paid a cold `pod install`.
|
|
19
|
+
*
|
|
20
|
+
* These tests RUN the generated script. A containment check on the script
|
|
21
|
+
* text cannot see this defect: the text contains the literal `$CACHE_KEY`
|
|
22
|
+
* either way, which is why the existing assertion passed throughout. `curl`
|
|
23
|
+
* and `zstd` are stubbed onto PATH so the pipeline completes on any host and
|
|
24
|
+
* the URL actually requested can be read back.
|
|
25
|
+
*/
|
|
26
|
+
export {};
|
|
27
|
+
//# sourceMappingURL=cache-push-daemon.test.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cache-push-daemon.test.d.ts","sourceRoot":"","sources":["../../../../src/yaml/steps/cache-push-daemon.test.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;GAwBG"}
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `cache-push` with an explicit `cache_key`, over the daemon.
|
|
3
|
+
*
|
|
4
|
+
* Every curated Invarn template uses the explicit-key form — `cache_key:
|
|
5
|
+
* pods-{{checksum "Podfile.lock"}}-$CIBUILD_GIT_BRANCH` and its Gradle/KMM
|
|
6
|
+
* siblings — for both halves of the pair. The pull side assigns the shell
|
|
7
|
+
* variable the daemon URL interpolates; the push side did not, and there is
|
|
8
|
+
* no `set -u`, so `$CACHE_KEY` expanded to nothing and the upload went to
|
|
9
|
+
* `<daemon>/cache/.tar.zst`. The daemon requires at least one character
|
|
10
|
+
* before `.tar.zst` and answers 400, which is exactly what the first real
|
|
11
|
+
* `invarn setup` proof logged on 2026-08-30:
|
|
12
|
+
*
|
|
13
|
+
* curl: (22) The requested URL returned error: 400
|
|
14
|
+
* Warning: failed to upload cache to the daemon
|
|
15
|
+
* [step_6] ✓ Success (109ms)
|
|
16
|
+
*
|
|
17
|
+
* Nothing was ever stored, so the pull could never hit, and every iOS build
|
|
18
|
+
* paid a cold `pod install`.
|
|
19
|
+
*
|
|
20
|
+
* These tests RUN the generated script. A containment check on the script
|
|
21
|
+
* text cannot see this defect: the text contains the literal `$CACHE_KEY`
|
|
22
|
+
* either way, which is why the existing assertion passed throughout. `curl`
|
|
23
|
+
* and `zstd` are stubbed onto PATH so the pipeline completes on any host and
|
|
24
|
+
* the URL actually requested can be read back.
|
|
25
|
+
*/
|
|
26
|
+
import { describe, test, expect } from '@jest/globals';
|
|
27
|
+
import { execFileSync } from 'child_process';
|
|
28
|
+
import { mkdtempSync, mkdirSync, writeFileSync, readFileSync, rmSync, existsSync, chmodSync } from 'fs';
|
|
29
|
+
import { join } from 'path';
|
|
30
|
+
import { tmpdir } from 'os';
|
|
31
|
+
import { CachePushStepExecutor } from './cache.js';
|
|
32
|
+
import { testConfigNoPeer } from './test-config.js';
|
|
33
|
+
const CACHE_KEY = 'pods-abc123def456-main';
|
|
34
|
+
/**
|
|
35
|
+
* Generates the explicit-key push step, runs it, and reports what `curl` was
|
|
36
|
+
* actually asked to do.
|
|
37
|
+
*
|
|
38
|
+
* @param curlExit exit status the stub returns, and `curlBody` is what it
|
|
39
|
+
* prints — so a rejection can be exercised without a daemon.
|
|
40
|
+
*/
|
|
41
|
+
async function runPush({ curlExit = 0, curlBody = '' } = {}) {
|
|
42
|
+
const dir = mkdtempSync(join(tmpdir(), 'cibuild-push-'));
|
|
43
|
+
try {
|
|
44
|
+
// Something real to cache, so the step does not take its empty branch.
|
|
45
|
+
const payload = join(dir, 'Pods');
|
|
46
|
+
mkdirSync(payload, { recursive: true });
|
|
47
|
+
writeFileSync(join(payload, 'thing.txt'), 'contents\n', 'utf-8');
|
|
48
|
+
const bin = join(dir, 'bin');
|
|
49
|
+
mkdirSync(bin, { recursive: true });
|
|
50
|
+
const urlLog = join(dir, 'curl-urls.txt');
|
|
51
|
+
// Records the URL, drains stdin so the upstream pipe does not break.
|
|
52
|
+
writeFileSync(join(bin, 'curl'), [
|
|
53
|
+
'#!/bin/bash',
|
|
54
|
+
'for arg in "$@"; do',
|
|
55
|
+
' case "$arg" in',
|
|
56
|
+
` http*) printf '%s\\n' "$arg" >> "${urlLog}" ;;`,
|
|
57
|
+
' esac',
|
|
58
|
+
'done',
|
|
59
|
+
'cat > /dev/null',
|
|
60
|
+
curlBody ? `printf '%s' ${JSON.stringify(curlBody)}` : ':',
|
|
61
|
+
`exit ${curlExit}`,
|
|
62
|
+
].join('\n'), 'utf-8');
|
|
63
|
+
chmodSync(join(bin, 'curl'), 0o755);
|
|
64
|
+
// zstd is not on every host, and without it the pipeline fails before
|
|
65
|
+
// curl is ever reached — which would make these tests pass vacuously.
|
|
66
|
+
writeFileSync(join(bin, 'zstd'), '#!/bin/bash\ncat\n', 'utf-8');
|
|
67
|
+
chmodSync(join(bin, 'zstd'), 0o755);
|
|
68
|
+
const { script } = await new CachePushStepExecutor().execute({ cache_key: CACHE_KEY, cache_paths: ['Pods'] }, {}, testConfigNoPeer);
|
|
69
|
+
const scriptPath = join(dir, '__cache_push.sh');
|
|
70
|
+
writeFileSync(scriptPath, script, 'utf-8');
|
|
71
|
+
let stdout = '';
|
|
72
|
+
try {
|
|
73
|
+
stdout = execFileSync('bash', [scriptPath], {
|
|
74
|
+
cwd: dir,
|
|
75
|
+
encoding: 'utf-8',
|
|
76
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
77
|
+
env: {
|
|
78
|
+
...process.env,
|
|
79
|
+
PATH: `${bin}:${process.env.PATH}`,
|
|
80
|
+
CIBUILD_CACHE_DAEMON: 'http://daemon.invalid',
|
|
81
|
+
CIBUILD_CACHE_TOKEN: 'tok',
|
|
82
|
+
},
|
|
83
|
+
timeout: 20000,
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
catch (e) {
|
|
87
|
+
const err = e;
|
|
88
|
+
stdout = String(err.stdout ?? '') + String(err.stderr ?? '');
|
|
89
|
+
}
|
|
90
|
+
const urls = existsSync(urlLog)
|
|
91
|
+
? readFileSync(urlLog, 'utf-8').split('\n').filter(Boolean)
|
|
92
|
+
: [];
|
|
93
|
+
return { stdout, urls };
|
|
94
|
+
}
|
|
95
|
+
finally {
|
|
96
|
+
rmSync(dir, { recursive: true, force: true });
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
describe('cache-push with an explicit key, over the daemon', () => {
|
|
100
|
+
test('uploads under the key it was given', async () => {
|
|
101
|
+
const { urls } = await runPush();
|
|
102
|
+
expect(urls).toHaveLength(1);
|
|
103
|
+
expect(urls[0]).toBe(`http://daemon.invalid/cache/${CACHE_KEY}.tar.zst`);
|
|
104
|
+
});
|
|
105
|
+
test('never uploads to an empty key', async () => {
|
|
106
|
+
// The defect, named. `<daemon>/cache/.tar.zst` is what an unassigned
|
|
107
|
+
// `$CACHE_KEY` produces, and what the daemon answers 400 to.
|
|
108
|
+
const { urls } = await runPush();
|
|
109
|
+
for (const url of urls) {
|
|
110
|
+
expect(url).not.toMatch(/\/cache\/\.tar\.zst$/);
|
|
111
|
+
}
|
|
112
|
+
});
|
|
113
|
+
test('the step survives a rejection rather than failing the build', async () => {
|
|
114
|
+
// Deliberate: a build that produced good output should not be failed by a
|
|
115
|
+
// cache upload. Whether the STEP should still report success having
|
|
116
|
+
// stored nothing is a separate, open question.
|
|
117
|
+
const { stdout } = await runPush({ curlExit: 22 });
|
|
118
|
+
expect(stdout).toContain('Warning: failed to upload cache to the daemon');
|
|
119
|
+
});
|
|
120
|
+
test('a rejection says why, not just that it happened', async () => {
|
|
121
|
+
// `curl -f` throws the response body away, so the daemon's own reason —
|
|
122
|
+
// `invalid_cache_key`, `cache_busy`, an auth failure — never reached the
|
|
123
|
+
// log. All the reader got was an exit code, which is how a one-line bug
|
|
124
|
+
// stayed open behind a warning that named nothing.
|
|
125
|
+
const { stdout } = await runPush({
|
|
126
|
+
curlExit: 22,
|
|
127
|
+
curlBody: '{"error":"invalid_cache_key"}',
|
|
128
|
+
});
|
|
129
|
+
expect(stdout).toContain('invalid_cache_key');
|
|
130
|
+
});
|
|
131
|
+
});
|
|
132
|
+
//# sourceMappingURL=cache-push-daemon.test.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"cache.d.ts","sourceRoot":"","sources":["../../../../src/yaml/steps/cache.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,OAAO,EAAE,gBAAgB,EAAE,MAAM,WAAW,CAAC;AAC7C,OAAO,KAAK,EAAE,OAAO,EAAE,QAAQ,EAAE,MAAM,gBAAgB,CAAC;AAkBxD;;;;;;GAMG;AACH,MAAM,WAAW,WAAW;IAC1B,4HAA4H;IAC5H,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,sHAAsH;IACtH,WAAW,CAAC,EAAE,MAAM,EAAE,CAAC;IACvB,SAAS,EAAE,MAAM,CAAC;IAClB,KAAK,EAAE,MAAM,EAAE,CAAC;IAChB,yGAAyG;IACzG,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB;AAED,eAAO,MAAM,aAAa,EAAE,MAAM,CAAC,MAAM,EAAE,WAAW,CAUrD,CAAC;
|
|
1
|
+
{"version":3,"file":"cache.d.ts","sourceRoot":"","sources":["../../../../src/yaml/steps/cache.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,OAAO,EAAE,gBAAgB,EAAE,MAAM,WAAW,CAAC;AAC7C,OAAO,KAAK,EAAE,OAAO,EAAE,QAAQ,EAAE,MAAM,gBAAgB,CAAC;AAkBxD;;;;;;GAMG;AACH,MAAM,WAAW,WAAW;IAC1B,4HAA4H;IAC5H,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,sHAAsH;IACtH,WAAW,CAAC,EAAE,MAAM,EAAE,CAAC;IACvB,SAAS,EAAE,MAAM,CAAC;IAClB,KAAK,EAAE,MAAM,EAAE,CAAC;IAChB,yGAAyG;IACzG,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB;AAED,eAAO,MAAM,aAAa,EAAE,MAAM,CAAC,MAAM,EAAE,WAAW,CAUrD,CAAC;AAgLF;;GAEG;AACH,MAAM,WAAW,eAAe;IAC9B,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,WAAW,CAAC,EAAE,MAAM,EAAE,CAAC;IACvB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,aAAa,CAAC,EAAE,OAAO,CAAC;CACzB;AAED;;GAEG;AACH,MAAM,WAAW,eAAe;IAC9B,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,WAAW,CAAC,EAAE,MAAM,EAAE,CAAC;IACvB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,aAAa,CAAC,EAAE,OAAO,CAAC;IACxB,qBAAqB,CAAC,EAAE,OAAO,CAAC;CACjC;AAED;;;GAGG;AACH,qBAAa,qBAAsB,SAAQ,gBAAgB;IACnD,OAAO,CAAC,MAAM,EAAE,eAAe,EAAE,GAAG,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,EAAE,MAAM,EAAE,QAAQ,GAAG,OAAO,CAAC,OAAO,CAAC;YA0HzF,iBAAiB;CA0MhC;AAED;;;GAGG;AACH,qBAAa,qBAAsB,SAAQ,gBAAgB;IACnD,OAAO,CAAC,MAAM,EAAE,eAAe,EAAE,GAAG,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,EAAE,MAAM,EAAE,QAAQ,GAAG,OAAO,CAAC,OAAO,CAAC;YAkLzF,iBAAiB;CAgJhC"}
|