@invarn/cibuild 2.5.5 → 2.5.8
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/cache.d.ts.map +1 -1
- package/dist/src/yaml/steps/cache.js +124 -32
- package/dist/src/yaml/steps/steps.test.js +60 -12
- 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":"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;AA6HF;;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;YAsLzF,iBAAiB;CA0JhC"}
|
|
@@ -87,28 +87,64 @@ function resolvePresetChain(technology) {
|
|
|
87
87
|
return chain;
|
|
88
88
|
}
|
|
89
89
|
/**
|
|
90
|
-
* Emits the
|
|
91
|
-
*
|
|
92
|
-
*
|
|
93
|
-
* hit to ~/cache, and streams it back — so we pipe straight to extraction
|
|
94
|
-
* without re-writing it on the guest. The optional per-build bearer token
|
|
95
|
-
* ($CIBUILD_CACHE_TOKEN) is sent only when set. Inert when the env var is
|
|
96
|
-
* empty, so the same script runs with or without a daemon.
|
|
90
|
+
* Emits the `curl` invocation the cache steps talk to the daemon with. The
|
|
91
|
+
* bearer token is per-build and only present on the Invarn runner, so it is
|
|
92
|
+
* expanded conditionally and the same script works without one.
|
|
97
93
|
*/
|
|
98
|
-
function
|
|
94
|
+
function daemonCurl(args) {
|
|
95
|
+
return `curl -fsS \${CIBUILD_CACHE_TOKEN:+-H "Authorization: Bearer $CIBUILD_CACHE_TOKEN"} ${args}`;
|
|
96
|
+
}
|
|
97
|
+
/**
|
|
98
|
+
* Restores a cache through the daemon rather than a shared directory.
|
|
99
|
+
*
|
|
100
|
+
* Two requests, mirroring the directory-based flow it replaces: the exact key
|
|
101
|
+
* first, then — on a miss — the scope's newest tarball. The daemon resolves
|
|
102
|
+
* each from its own disk or a tailnet peer, so a hit here covers what used to
|
|
103
|
+
* be separate "local" and "peer" branches; which one it was is recorded in the
|
|
104
|
+
* daemon's own host-side log, where the fleet-wide view already lives.
|
|
105
|
+
*
|
|
106
|
+
* The 30-day age cap is preserved: the daemon dates the fallback with
|
|
107
|
+
* `mtimeMs`, standing in for the `stat` the guest can no longer perform.
|
|
108
|
+
*/
|
|
109
|
+
function emitDaemonPullCommands() {
|
|
99
110
|
return [
|
|
100
|
-
|
|
101
|
-
//
|
|
102
|
-
//
|
|
103
|
-
|
|
104
|
-
// status is preserved (pipefail), so the fall-through still works.
|
|
105
|
-
'elif [ -n "$CIBUILD_CACHE_DAEMON" ] && { curl -fsS ${CIBUILD_CACHE_TOKEN:+-H "Authorization: Bearer $CIBUILD_CACHE_TOKEN"} "$CIBUILD_CACHE_DAEMON/cache/$CACHE_KEY.tar.zst" | zstd -dc | tar -xf - -C /; } 2>/dev/null; then',
|
|
111
|
+
'echo "Cache daemon: $CIBUILD_CACHE_DAEMON"',
|
|
112
|
+
// Exact key. stderr silenced — a miss is the normal case and curl/zstd
|
|
113
|
+
// both editorialise about it; pipefail still carries the exit status.
|
|
114
|
+
`if { ${daemonCurl('"$CIBUILD_CACHE_DAEMON/cache/$CACHE_KEY.tar.zst"')} | zstd -dc | tar -xf - -C /; } 2>/dev/null; then`,
|
|
106
115
|
' echo "Cache found (daemon), extracting..."',
|
|
107
|
-
// LRU bump the tarball the daemon just persisted to ~/cache (same dir as
|
|
108
|
-
// the --dir mount); tolerate its absence in case the daemon served it
|
|
109
|
-
// without a local copy.
|
|
110
|
-
' touch "$CACHE_FILE" 2>/dev/null || true',
|
|
111
116
|
' echo "CACHE_SOURCE=daemon CACHE_KEY=$CACHE_KEY"',
|
|
117
|
+
'else',
|
|
118
|
+
// Scope fallback: the newest tarball for this <keyPrefix>-<projectId>.
|
|
119
|
+
// Gradle/Xcode re-hash their inputs on top of the warm directory, hitting
|
|
120
|
+
// the unchanged work and rebuilding only what actually moved.
|
|
121
|
+
' __ci_fb_scope="${CACHE_KEY%-*}"',
|
|
122
|
+
` __ci_fb_json=$(${daemonCurl('"$CIBUILD_CACHE_DAEMON/cache/scope/$__ci_fb_scope/latest"')} 2>/dev/null || true)`,
|
|
123
|
+
// Parse without a JSON tool: the guest image is not guaranteed to ship jq.
|
|
124
|
+
` __ci_fb_key=$(printf '%s' "$__ci_fb_json" | sed -n 's/.*"key"[[:space:]]*:[[:space:]]*"\\([^"]*\\)".*/\\1/p')`,
|
|
125
|
+
` __ci_fb_mtime=$(printf '%s' "$__ci_fb_json" | sed -n 's/.*"mtimeMs"[[:space:]]*:[[:space:]]*\\([0-9]*\\).*/\\1/p')`,
|
|
126
|
+
' if [ -n "$__ci_fb_key" ] && [ -n "$__ci_fb_mtime" ]; then',
|
|
127
|
+
' __ci_fb_age_days=$(( ($(date +%s) - __ci_fb_mtime / 1000) / 86400 ))',
|
|
128
|
+
// Age cap: a fallback older than 30d likely carries obsolete toolchain
|
|
129
|
+
// installs or cache entries from a pre-wrapper-bump world. Go cold rather
|
|
130
|
+
// than seed from ancient state. Exact-key hits are unaffected.
|
|
131
|
+
' if [ "$__ci_fb_age_days" -le 30 ]; then',
|
|
132
|
+
' echo "Cache fallback (daemon): using prior tarball $__ci_fb_key (${__ci_fb_age_days}d old)"',
|
|
133
|
+
` if { ${daemonCurl('"$CIBUILD_CACHE_DAEMON/cache/$__ci_fb_key.tar.zst"')} | zstd -dc | tar -xf - -C /; } 2>/dev/null; then`,
|
|
134
|
+
' echo "CACHE_SOURCE=fallback_daemon CACHE_KEY=$CACHE_KEY FALLBACK_KEY=$__ci_fb_key"',
|
|
135
|
+
' else',
|
|
136
|
+
' echo "Cache fallback $__ci_fb_key could not be fetched — going cold"',
|
|
137
|
+
' echo "CACHE_SOURCE=cold CACHE_KEY=$CACHE_KEY"',
|
|
138
|
+
' fi',
|
|
139
|
+
' else',
|
|
140
|
+
' echo "Cache fallback candidate $__ci_fb_key is ${__ci_fb_age_days}d old, exceeds 30d cap — going cold"',
|
|
141
|
+
' echo "CACHE_SOURCE=cold CACHE_KEY=$CACHE_KEY FALLBACK_REJECTED=$__ci_fb_key"',
|
|
142
|
+
' fi',
|
|
143
|
+
' else',
|
|
144
|
+
' echo "No cache found for key: $CACHE_KEY"',
|
|
145
|
+
' echo "CACHE_SOURCE=cold CACHE_KEY=$CACHE_KEY"',
|
|
146
|
+
' fi',
|
|
147
|
+
'fi',
|
|
112
148
|
];
|
|
113
149
|
}
|
|
114
150
|
/**
|
|
@@ -154,7 +190,11 @@ export class CachePullStepExecutor extends BaseStepExecutor {
|
|
|
154
190
|
commands.push('');
|
|
155
191
|
commands.push('# Ensure cache directory exists');
|
|
156
192
|
commands.push(`CACHE_DIR="${this.escapeBash(config.paths.cacheDir)}"`);
|
|
157
|
-
|
|
193
|
+
// Only the filesystem transport needs the directory to exist. Over the
|
|
194
|
+
// daemon the cache lives on the host, and on a runner this path is no
|
|
195
|
+
// longer mounted into the VM — an unconditional mkdir would abort the
|
|
196
|
+
// whole step under errexit.
|
|
197
|
+
commands.push('[ -n "$CIBUILD_CACHE_DAEMON" ] || mkdir -p "$CACHE_DIR"');
|
|
158
198
|
// Generate cache file name based on cache key
|
|
159
199
|
commands.push('');
|
|
160
200
|
commands.push('# Generate cache file path');
|
|
@@ -174,6 +214,17 @@ export class CachePullStepExecutor extends BaseStepExecutor {
|
|
|
174
214
|
// Check if cache file exists
|
|
175
215
|
commands.push('');
|
|
176
216
|
commands.push('# Check if cache exists');
|
|
217
|
+
// Transport split — see the preset path. An explicit-key step has no scope
|
|
218
|
+
// to fall back to, so the daemon side is the exact key or nothing.
|
|
219
|
+
commands.push('if [ -n "$CIBUILD_CACHE_DAEMON" ]; then');
|
|
220
|
+
commands.push(` if { ${daemonCurl('"$CIBUILD_CACHE_DAEMON/cache/$CACHE_KEY.tar.zst"')} | zstd -dc | tar -xf - -C /; } 2>/dev/null; then`);
|
|
221
|
+
commands.push(' echo "Cache found (daemon), extracting..."');
|
|
222
|
+
commands.push(' echo "CACHE_SOURCE=daemon"');
|
|
223
|
+
commands.push(' else');
|
|
224
|
+
commands.push(` echo "No cache found for key: ${this.escapeBash(cacheKey)}"`);
|
|
225
|
+
commands.push(' echo "CACHE_SOURCE=cold"');
|
|
226
|
+
commands.push(' fi');
|
|
227
|
+
commands.push('else');
|
|
177
228
|
commands.push('if [ -f "$CACHE_FILE" ]; then');
|
|
178
229
|
commands.push(' echo "Cache found (local), extracting..."');
|
|
179
230
|
if (isDebugMode) {
|
|
@@ -181,8 +232,6 @@ export class CachePullStepExecutor extends BaseStepExecutor {
|
|
|
181
232
|
}
|
|
182
233
|
commands.push(' zstd -dc "$CACHE_FILE" | tar -xf - -C /');
|
|
183
234
|
commands.push(' echo "CACHE_SOURCE=local"');
|
|
184
|
-
// Daemon hit (ADR 0001) — see emitDaemonPullBranch.
|
|
185
|
-
commands.push(...emitDaemonPullBranch());
|
|
186
235
|
// Peer fallback
|
|
187
236
|
if (peerCacheDir) {
|
|
188
237
|
commands.push('elif [ -f "$PEER_CACHE_FILE" ] 2>/dev/null; then');
|
|
@@ -195,6 +244,8 @@ export class CachePullStepExecutor extends BaseStepExecutor {
|
|
|
195
244
|
commands.push(` echo "No cache found for key: ${this.escapeBash(cacheKey)}"`);
|
|
196
245
|
commands.push(' echo "CACHE_SOURCE=cold"');
|
|
197
246
|
commands.push('fi');
|
|
247
|
+
// Closes the daemon/filesystem transport split.
|
|
248
|
+
commands.push('fi');
|
|
198
249
|
const script = this.createBashScriptFromCommands(commands, stepName);
|
|
199
250
|
return this.createScriptStep(script, stepName);
|
|
200
251
|
}
|
|
@@ -213,7 +264,11 @@ export class CachePullStepExecutor extends BaseStepExecutor {
|
|
|
213
264
|
// Compute cache key from lockfile checksum at runtime
|
|
214
265
|
// If technology has a fallback chain, try each lockfile in order
|
|
215
266
|
commands.push(`CACHE_DIR="${this.escapeBash(config.paths.cacheDir)}"`);
|
|
216
|
-
|
|
267
|
+
// Only the filesystem transport needs the directory to exist. Over the
|
|
268
|
+
// daemon the cache lives on the host, and on a runner this path is no
|
|
269
|
+
// longer mounted into the VM — an unconditional mkdir would abort the
|
|
270
|
+
// whole step under errexit.
|
|
271
|
+
commands.push('[ -n "$CIBUILD_CACHE_DAEMON" ] || mkdir -p "$CACHE_DIR"');
|
|
217
272
|
commands.push('');
|
|
218
273
|
// Helper: resolve lockfile path — checks exact path first, then searches
|
|
219
274
|
// recursively (handles SPM Package.resolved inside .xcodeproj/.xcworkspace)
|
|
@@ -273,6 +328,12 @@ export class CachePullStepExecutor extends BaseStepExecutor {
|
|
|
273
328
|
}
|
|
274
329
|
commands.push('CACHE_FILE="$CACHE_DIR/$CACHE_KEY.tar.zst"');
|
|
275
330
|
commands.push('');
|
|
331
|
+
// Transport split. On a runner the cache lives on the host and is reached
|
|
332
|
+
// over HTTP; standalone, it is a directory on this machine. The daemon URL
|
|
333
|
+
// is the switch, so the same generated script serves both.
|
|
334
|
+
commands.push('if [ -n "$CIBUILD_CACHE_DAEMON" ]; then');
|
|
335
|
+
commands.push(...emitDaemonPullCommands());
|
|
336
|
+
commands.push('else');
|
|
276
337
|
// Peer cache directory — set from config, empty string disables peer lookup
|
|
277
338
|
const peerCacheDir = config.paths.peerCacheDir || '';
|
|
278
339
|
if (peerCacheDir) {
|
|
@@ -292,13 +353,6 @@ export class CachePullStepExecutor extends BaseStepExecutor {
|
|
|
292
353
|
// hit daily would still be reaped on its 31st day from creation.
|
|
293
354
|
commands.push(' touch "$CACHE_FILE"');
|
|
294
355
|
commands.push(' echo "CACHE_SOURCE=local CACHE_KEY=$CACHE_KEY"');
|
|
295
|
-
// 1b. Daemon hit (ADR 0001) — on a local miss, ask this runner's cache
|
|
296
|
-
// daemon for the exact key. The daemon resolves it from a tailnet peer
|
|
297
|
-
// and atomically persists it to ~/cache (the same dir as the --dir mount),
|
|
298
|
-
// so we stream the response straight to extraction with no redundant
|
|
299
|
-
// guest-side write. Gated at runtime on the env var the runner injects, so
|
|
300
|
-
// the generated script is inert when no daemon is configured.
|
|
301
|
-
commands.push(...emitDaemonPullBranch());
|
|
302
356
|
// 2. Peer hit — tee to warm local cache while extracting in one pass
|
|
303
357
|
if (peerCacheDir) {
|
|
304
358
|
commands.push('elif [ -f "$PEER_CACHE_FILE" ] 2>/dev/null; then');
|
|
@@ -362,6 +416,8 @@ export class CachePullStepExecutor extends BaseStepExecutor {
|
|
|
362
416
|
commands.push(' echo "CACHE_SOURCE=cold CACHE_KEY=$CACHE_KEY"');
|
|
363
417
|
commands.push(' fi');
|
|
364
418
|
commands.push('fi');
|
|
419
|
+
// Closes the daemon/filesystem transport split.
|
|
420
|
+
commands.push('fi');
|
|
365
421
|
if (isDebugMode) {
|
|
366
422
|
commands.push('echo "Restored paths:"');
|
|
367
423
|
const allPaths = [...new Set(chain.flatMap(p => p.paths))];
|
|
@@ -427,7 +483,11 @@ export class CachePushStepExecutor extends BaseStepExecutor {
|
|
|
427
483
|
commands.push('');
|
|
428
484
|
commands.push('# Ensure cache directory exists');
|
|
429
485
|
commands.push(`CACHE_DIR="${this.escapeBash(config.paths.cacheDir)}"`);
|
|
430
|
-
|
|
486
|
+
// Only the filesystem transport needs the directory to exist. Over the
|
|
487
|
+
// daemon the cache lives on the host, and on a runner this path is no
|
|
488
|
+
// longer mounted into the VM — an unconditional mkdir would abort the
|
|
489
|
+
// whole step under errexit.
|
|
490
|
+
commands.push('[ -n "$CIBUILD_CACHE_DAEMON" ] || mkdir -p "$CACHE_DIR"');
|
|
431
491
|
// Generate cache file name based on cache key
|
|
432
492
|
commands.push('');
|
|
433
493
|
commands.push('# Generate cache file path');
|
|
@@ -488,7 +548,10 @@ export class CachePushStepExecutor extends BaseStepExecutor {
|
|
|
488
548
|
// Create cache if we have paths
|
|
489
549
|
commands.push('');
|
|
490
550
|
commands.push('# Skip if cache already exists for this key');
|
|
491
|
-
|
|
551
|
+
// Over the daemon there is no local file to test, so the "already up to
|
|
552
|
+
// date" shortcut can't apply — the daemon's PUT is an idempotent overwrite
|
|
553
|
+
// under the same key, which gets the same outcome.
|
|
554
|
+
commands.push('if [ -z "$CIBUILD_CACHE_DAEMON" ] && [ -f "$CACHE_FILE" ]; then');
|
|
492
555
|
commands.push(' echo "Cache already up to date for key: $CACHE_KEY"');
|
|
493
556
|
commands.push('elif [ ${#PATHS_TO_CACHE[@]} -gt 0 ]; then');
|
|
494
557
|
commands.push(' echo "Caching ${#PATHS_TO_CACHE[@]} path(s)..."');
|
|
@@ -496,6 +559,14 @@ export class CachePushStepExecutor extends BaseStepExecutor {
|
|
|
496
559
|
commands.push(' echo "Paths to cache:"');
|
|
497
560
|
commands.push(' printf " %s\\n" "${PATHS_TO_CACHE[@]}"');
|
|
498
561
|
}
|
|
562
|
+
// Transport split — see the preset path.
|
|
563
|
+
commands.push(' if [ -n "$CIBUILD_CACHE_DAEMON" ]; then');
|
|
564
|
+
commands.push(` if tar -cf - "\${PATHS_TO_CACHE[@]}" 2>/dev/null | zstd -3 | ${daemonCurl('-T - "$CIBUILD_CACHE_DAEMON/cache/$CACHE_KEY.tar.zst"')} > /dev/null; then`);
|
|
565
|
+
commands.push(' echo "Cache created successfully"');
|
|
566
|
+
commands.push(' else');
|
|
567
|
+
commands.push(' echo "Warning: failed to upload cache to the daemon"');
|
|
568
|
+
commands.push(' fi');
|
|
569
|
+
commands.push(' else');
|
|
499
570
|
// Atomic write into a per-process-unique temp (CBR-12). The "runs serialized
|
|
500
571
|
// per runner" assumption behind a fixed "$CACHE_FILE.tmp" is false with two VM
|
|
501
572
|
// slots and collides with the host cache daemon mutating this VirtioFS-shared
|
|
@@ -511,6 +582,8 @@ export class CachePushStepExecutor extends BaseStepExecutor {
|
|
|
511
582
|
commands.push(' else');
|
|
512
583
|
commands.push(' echo "Warning: Failed to create cache file"');
|
|
513
584
|
commands.push(' fi');
|
|
585
|
+
// Closes the daemon/filesystem transport split.
|
|
586
|
+
commands.push(' fi');
|
|
514
587
|
commands.push('else');
|
|
515
588
|
commands.push(' echo "No paths to cache"');
|
|
516
589
|
commands.push('fi');
|
|
@@ -531,7 +604,11 @@ export class CachePushStepExecutor extends BaseStepExecutor {
|
|
|
531
604
|
commands.push('');
|
|
532
605
|
// Compute cache key from lockfile checksum at runtime
|
|
533
606
|
commands.push(`CACHE_DIR="${this.escapeBash(config.paths.cacheDir)}"`);
|
|
534
|
-
|
|
607
|
+
// Only the filesystem transport needs the directory to exist. Over the
|
|
608
|
+
// daemon the cache lives on the host, and on a runner this path is no
|
|
609
|
+
// longer mounted into the VM — an unconditional mkdir would abort the
|
|
610
|
+
// whole step under errexit.
|
|
611
|
+
commands.push('[ -n "$CIBUILD_CACHE_DAEMON" ] || mkdir -p "$CACHE_DIR"');
|
|
535
612
|
commands.push('');
|
|
536
613
|
commands.push('__ci_find_lockfile() {');
|
|
537
614
|
commands.push(' if [ -f "$1" ]; then echo "$1"; return; fi');
|
|
@@ -610,6 +687,19 @@ export class CachePushStepExecutor extends BaseStepExecutor {
|
|
|
610
687
|
// build. Atomic .tmp + mv overwrites any prior tarball under this key.
|
|
611
688
|
commands.push('if [ ${#PATHS_TO_CACHE[@]} -gt 0 ]; then');
|
|
612
689
|
commands.push(' echo "Caching ${#PATHS_TO_CACHE[@]} path(s)..."');
|
|
690
|
+
// Transport split, mirroring cache-pull. Over the daemon the tarball is
|
|
691
|
+
// streamed straight out as the PUT body — nothing is staged on the guest,
|
|
692
|
+
// so there is no temp file to collide or orphan, and the daemon does the
|
|
693
|
+
// atomic temp+rename on the host side where the cache actually lives.
|
|
694
|
+
commands.push(' if [ -n "$CIBUILD_CACHE_DAEMON" ]; then');
|
|
695
|
+
commands.push(` if tar -cf - "\${PATHS_TO_CACHE[@]}" 2>/dev/null | zstd -3 | ${daemonCurl('-T - "$CIBUILD_CACHE_DAEMON/cache/$CACHE_KEY.tar.zst"')} > /dev/null; then`);
|
|
696
|
+
commands.push(' echo "Cache created successfully"');
|
|
697
|
+
commands.push(' else');
|
|
698
|
+
// Never fatal: a build that produced good output should not be failed by a
|
|
699
|
+
// cache upload, and the next build simply misses.
|
|
700
|
+
commands.push(' echo "Warning: failed to upload cache to the daemon"');
|
|
701
|
+
commands.push(' fi');
|
|
702
|
+
commands.push(' else');
|
|
613
703
|
// Per-process-unique temp (CBR-12). The old fixed "$CACHE_FILE.tmp" assumed
|
|
614
704
|
// a single serialized writer — false with two VM slots, and it collides with
|
|
615
705
|
// the host cache daemon mutating this VirtioFS-shared dir, which (a) made the
|
|
@@ -632,6 +722,8 @@ export class CachePushStepExecutor extends BaseStepExecutor {
|
|
|
632
722
|
commands.push(' else');
|
|
633
723
|
commands.push(' echo "Warning: Failed to create cache file"');
|
|
634
724
|
commands.push(' fi');
|
|
725
|
+
// Closes the daemon/filesystem transport split.
|
|
726
|
+
commands.push(' fi');
|
|
635
727
|
commands.push('else');
|
|
636
728
|
commands.push(' echo "No paths to cache"');
|
|
637
729
|
commands.push('fi');
|