@invarn/cibuild 2.8.6 → 2.8.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.
@@ -19,13 +19,19 @@
19
19
  * 2. **Say so.** When a Gradle run exits 137 or its output says the daemon
20
20
  * disappeared, print the kernel's out-of-memory lines and one sentence
21
21
  * with the machine's memory and what the build asked for.
22
+ * 3. **Workers.** A heap cap does not bound what runs beside the heaps:
23
+ * off-heap memory, and native compilers run in parallel. IacobIonut01/ReFra
24
+ * was killed again at a capped -Xmx3712m, one JVM holding ~5.1 GiB
25
+ * resident; its own CI also sets `org.gradle.workers.max=2`. A pipeline
26
+ * may ask for a worker count, passed as `--max-workers=<n>`.
22
27
  *
23
28
  * Precedence, measured with Gradle 8.14.3 and 9.5.0 (daemon and --no-daemon):
24
29
  * `-Dorg.gradle.jvmargs` on the command line beats both the Gradle user
25
30
  * home's and the project's `gradle.properties` for the daemon's heap, and
26
31
  * `-P` beats `gradle.properties` for a Gradle property, which is how the
27
- * Kotlin Gradle plugin reads `kotlin.daemon.jvmargs`. `GRADLE_OPTS` is not
28
- * used: it sizes the client JVM, not the daemon.
32
+ * Kotlin Gradle plugin reads `kotlin.daemon.jvmargs`. `--max-workers` beats
33
+ * `org.gradle.workers.max` in both files (`gradle.startParameter.maxWorkerCount`).
34
+ * `GRADLE_OPTS` is not used: it sizes the client JVM, not the daemon.
29
35
  *
30
36
  * Written as shell functions so what ships is what the tests run. Its lines
31
37
  * start with "Gradle memory:", never with a step-progress marker.
@@ -204,6 +210,23 @@ cibuild_gradle_memory_plan() {
204
210
  CIBUILD_GRADLE_MEMORY_SENTENCE="$CIBUILD_GRADLE_MEMORY_SENTENCE This build ran them capped: $now."
205
211
  }
206
212
 
213
+ # $1 the number of Gradle workers the pipeline asks for (may be empty). Adds
214
+ # --max-workers to the plan's arguments; run after cibuild_gradle_memory_plan.
215
+ # Sets CIBUILD_GRADLE_WORKERS_LINE (what to print; empty when nothing asked).
216
+ cibuild_gradle_workers_plan() {
217
+ CIBUILD_GRADLE_WORKERS_LINE=""
218
+ [ -n "$1" ] || return 0
219
+ case "$1" in
220
+ *[!0-9]*|0*)
221
+ CIBUILD_GRADLE_WORKERS_LINE="Gradle memory: max_workers=$1 is not a positive whole number, so it is not passed and Gradle picks its own worker count."
222
+ return 0
223
+ ;;
224
+ esac
225
+ CIBUILD_GRADLE_MEMORY_ARGS+=("--max-workers=$1")
226
+ CIBUILD_GRADLE_WORKERS_LINE="Gradle memory: running with --max-workers=$1, as the pipeline asks (set on the command line, over any org.gradle.workers.max)."
227
+ CIBUILD_GRADLE_MEMORY_SENTENCE="$CIBUILD_GRADLE_MEMORY_SENTENCE Gradle ran with --max-workers=$1."
228
+ }
229
+
207
230
  # The kernel's out-of-memory lines, if this user may read the kernel log.
208
231
  # Linux guests restrict dmesg to root by default; their build user has
209
232
  # non-interactive sudo, so that is the second try.
@@ -244,12 +267,15 @@ cibuild_gradle() {
244
267
  return "$rc"
245
268
  }
246
269
 
247
- # $1 JVM arguments the pipeline asks for (may be empty). Run in the Gradle
248
- # project directory, after GRADLE_CMD is chosen.
270
+ # $1 JVM arguments the pipeline asks for, $2 the Gradle workers it asks for
271
+ # (either may be empty). Run in the Gradle project directory, after GRADLE_CMD
272
+ # is chosen.
249
273
  cibuild_gradle_memory_cap() {
250
274
  cibuild_gradle_memory_plan "$(cibuild_mem_mib || true)" "gradle.properties" \\
251
275
  "\${GRADLE_USER_HOME:-$HOME/.gradle}/gradle.properties" "$1"
276
+ cibuild_gradle_workers_plan "$2"
252
277
  if [ -n "$CIBUILD_GRADLE_MEMORY_LINE" ]; then echo "$CIBUILD_GRADLE_MEMORY_LINE"; fi
278
+ if [ -n "$CIBUILD_GRADLE_WORKERS_LINE" ]; then echo "$CIBUILD_GRADLE_WORKERS_LINE"; fi
253
279
  CIBUILD_GRADLE_BIN="$GRADLE_CMD"
254
280
  GRADLE_CMD=cibuild_gradle
255
281
  }`;
@@ -260,9 +286,15 @@ cibuild_gradle_memory_cap() {
260
286
  *
261
287
  * `jvmargs` is what the pipeline asks for instead of the project's
262
288
  * `org.gradle.jvmargs` (a value the repository's own CI sets, say); the cap
263
- * still applies to it.
289
+ * still applies to it. `maxWorkers` is the worker count it asks for (the
290
+ * repository CI's `org.gradle.workers.max`), passed as `--max-workers`.
264
291
  */
265
- export function gradleMemoryCommands(jvmargs, escape) {
266
- return ['', GRADLE_MEMORY_FUNCTIONS, '', `cibuild_gradle_memory_cap '${escape(jvmargs)}'`];
292
+ export function gradleMemoryCommands(jvmargs, escape, maxWorkers = '') {
293
+ return [
294
+ '',
295
+ GRADLE_MEMORY_FUNCTIONS,
296
+ '',
297
+ `cibuild_gradle_memory_cap '${escape(jvmargs)}' '${escape(maxWorkers)}'`,
298
+ ];
267
299
  }
268
300
  //# sourceMappingURL=gradle-memory.js.map
@@ -124,6 +124,52 @@ describe("cibuild_gradle_memory_plan", () => {
124
124
  ]);
125
125
  });
126
126
  });
127
+ /** The plan, then the workers plan, as the step runs them. */
128
+ function workersPlan(memMib, maxWorkers) {
129
+ const script = `${GRADLE_MEMORY_FUNCTIONS}
130
+ cibuild_gradle_memory_plan "$1" gradle.properties "$2/gradle.properties" ""
131
+ cibuild_gradle_workers_plan "$3"
132
+ for a in "\${CIBUILD_GRADLE_MEMORY_ARGS[@]}"; do printf 'ARG %s\\n' "$a"; done
133
+ printf 'LINE %s\\n' "$CIBUILD_GRADLE_WORKERS_LINE"
134
+ printf 'SENTENCE %s\\n' "$CIBUILD_GRADLE_MEMORY_SENTENCE"`;
135
+ const out = execFileSync("bash", ["-c", script, "plan", memMib, join(dir, "no-user-home"), maxWorkers], {
136
+ cwd: dir,
137
+ encoding: "utf-8",
138
+ });
139
+ const lines = out.split("\n");
140
+ return {
141
+ args: lines.filter((l) => l.startsWith("ARG ")).map((l) => l.slice(4)),
142
+ line: (lines.find((l) => l.startsWith("LINE ")) ?? "").slice(5),
143
+ sentence: (lines.find((l) => l.startsWith("SENTENCE ")) ?? "").slice(9),
144
+ };
145
+ }
146
+ // ReFra was killed again with its heap capped: off-heap memory and parallel
147
+ // native compilers are past what a heap cap bounds. Its CI also sets
148
+ // org.gradle.workers.max=2, which the pipeline carries as max_workers.
149
+ describe("cibuild_gradle_workers_plan", () => {
150
+ test("ReFra: the worker count goes after the heap cap, and the sentence names it", () => {
151
+ copyFileSync(join(FIXTURES, "refra.gradle.properties"), join(dir, "gradle.properties"));
152
+ const p = workersPlan(TEN_GIB, "2");
153
+ expect(p.args).toEqual(["-Dorg.gradle.jvmargs=-Xmx3840m -Dfile.encoding=UTF-8", "--max-workers=2"]);
154
+ expect(p.line).toBe("Gradle memory: running with --max-workers=2, as the pipeline asks (set on the command line, over any org.gradle.workers.max).");
155
+ expect(p.sentence).toMatch(/This build ran them capped: Gradle -Xmx3840m, .* Gradle ran with --max-workers=2\.$/);
156
+ });
157
+ test("heaps that fit still get the worker count", () => {
158
+ const p = workersPlan(TEN_GIB, "4");
159
+ expect(p.args).toEqual(["--max-workers=4"]);
160
+ });
161
+ test("nothing asked, nothing passed or printed", () => {
162
+ const p = workersPlan(TEN_GIB, "");
163
+ expect(p.args).toEqual([]);
164
+ expect(p.line).toBe("");
165
+ expect(p.sentence).not.toContain("max-workers");
166
+ });
167
+ test.each(["0", "02", "-1", "two", "2 --offline"])("%s is not a worker count, and says so", (value) => {
168
+ const p = workersPlan(TEN_GIB, value);
169
+ expect(p.args).toEqual([]);
170
+ expect(p.line).toBe(`Gradle memory: max_workers=${value} is not a positive whole number, so it is not passed and Gradle picks its own worker count.`);
171
+ });
172
+ });
127
173
  /** A directory standing in for PATH, with a fake `dmesg` that prints `oom`. */
128
174
  function fakeBin(oom) {
129
175
  const bin = join(dir, "bin");
@@ -173,6 +219,38 @@ describe("the Gradle steps", () => {
173
219
  const args = execFileSync("cat", [join(dir, "gradlew-args")], { encoding: "utf-8" }).trim().split("\n");
174
220
  expect(args).toEqual(["-Dorg.gradle.jvmargs=-Xmx256m -Dfile.encoding=UTF-8", "assembleDebug", "--stacktrace"]);
175
221
  });
222
+ test("the pipeline's max_workers reach Gradle after the heap arguments, before the task", async () => {
223
+ fakeGradlew("exit 0");
224
+ const step = await new GradleBuildStepExecutor().execute(
225
+ // YAML reads `max_workers: 2` as a number.
226
+ { gradle_task: "assembleDebug", gradle_options: "--stacktrace", jvmargs: "-Xmx256m", max_workers: 2 }, {}, testConfig);
227
+ const run = runStep(step.script, fakeBin(""));
228
+ expect(run.status).toBe(0);
229
+ expect(run.stdout).toContain("Gradle memory: running with --max-workers=2, as the pipeline asks");
230
+ const args = execFileSync("cat", [join(dir, "gradlew-args")], { encoding: "utf-8" }).trim().split("\n");
231
+ expect(args).toEqual(["-Dorg.gradle.jvmargs=-Xmx256m", "--max-workers=2", "assembleDebug", "--stacktrace"]);
232
+ });
233
+ test("a killed daemon's sentence names the worker count it ran with", async () => {
234
+ copyFileSync(join(FIXTURES, "refra.gradle.properties"), join(dir, "gradle.properties"));
235
+ fakeGradlew('echo "Gradle build daemon disappeared unexpectedly"\nexit 1');
236
+ const step = await new GradleBuildStepExecutor().execute({ gradle_task: "assembleDebug", max_workers: "2" }, {}, testConfig);
237
+ const run = runStep(step.script, fakeBin(OOM_LINE));
238
+ expect(run.status).toBe(1);
239
+ // The test machine's own memory decides whether a cap applied, so only the
240
+ // sentence's end is asserted.
241
+ expect(run.stdout).toMatch(/^The build machine has .*asked for 16\.0 GiB .* Gradle ran with --max-workers=2\.$/m);
242
+ });
243
+ test("every Gradle step passes max_workers to the plan", async () => {
244
+ const steps = [
245
+ await new GradleBuildStepExecutor().execute({ max_workers: 3 }, {}, testConfig),
246
+ await new AndroidLintStepExecutor().execute({ max_workers: 3 }, {}, testConfig),
247
+ await new AndroidUnitTestStepExecutor().execute({ max_workers: 3 }, {}, testConfig),
248
+ await new AndroidBuildForUITestingStepExecutor().execute({ max_workers: 3 }, {}, testConfig),
249
+ ];
250
+ for (const step of steps) {
251
+ expect(step.script).toContain("cibuild_gradle_memory_cap '' '3'");
252
+ }
253
+ });
176
254
  test("every Gradle step runs through the plan, and every one is valid bash", async () => {
177
255
  const steps = [
178
256
  await new GradleBuildStepExecutor().execute({}, {}, testConfig),
@@ -182,7 +260,7 @@ describe("the Gradle steps", () => {
182
260
  ];
183
261
  for (const step of steps) {
184
262
  const script = step.script;
185
- expect(script).toContain("cibuild_gradle_memory_cap ''");
263
+ expect(script).toContain("cibuild_gradle_memory_cap '' ''");
186
264
  execFileSync("bash", ["-n"], { input: script });
187
265
  }
188
266
  });
@@ -1 +1 @@
1
- {"version":3,"file":"ios-deps.d.ts","sourceRoot":"","sources":["../../../../src/yaml/steps/ios-deps.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,OAAO,EAAE,gBAAgB,EAAE,MAAM,WAAW,CAAC;AAE7C,OAAO,KAAK,EAAE,OAAO,EAAE,QAAQ,EAAE,MAAM,gBAAgB,CAAC;AACxD,OAAO,KAAK,EAAE,qBAAqB,EAAc,MAAM,wBAAwB,CAAC;AAMhF,MAAM,WAAW,sBAAsB;IACrC,0CAA0C;IAC1C,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,4DAA4D;IAC5D,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,4DAA4D;IAC5D,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB;;;;;;;;;;;;;;;;;OAiBG;IACH,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,uCAAuC;IACvC,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB;;;;;;;;;;;;;;OAcG;IACH,cAAc,CAAC,EAAE,OAAO,CAAC;CAC1B;AAED;;;;GAIG;AACH,qBAAa,4BAA6B,SAAQ,gBAAgB;IAChE,yBAAyB,CACvB,OAAO,EAAE,sBAAsB,EAC/B,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,EAC5B,OAAO,EAAE,QAAQ,GAChB,qBAAqB,EAAE;IAUpB,OAAO,CACX,MAAM,EAAE,sBAAsB,EAC9B,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,EAC5B,OAAO,EAAE,QAAQ,GAChB,OAAO,CAAC,OAAO,CAAC;CAoJpB;AAMD,MAAM,WAAW,cAAc;IAC7B,mDAAmD;IACnD,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,kDAAkD;IAClD,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,wDAAwD;IACxD,mBAAmB,CAAC,EAAE,MAAM,CAAC;IAC7B,6BAA6B;IAC7B,OAAO,CAAC,EAAE,OAAO,CAAC;CACnB;AAED;;GAEG;AACH,qBAAa,oBAAqB,SAAQ,gBAAgB;IACxD,yBAAyB,CACvB,OAAO,EAAE,cAAc,EACvB,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,EAC5B,OAAO,EAAE,QAAQ,GAChB,qBAAqB,EAAE;IAUpB,OAAO,CACX,MAAM,EAAE,cAAc,EACtB,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,EAC5B,OAAO,EAAE,QAAQ,GAChB,OAAO,CAAC,OAAO,CAAC;CAyDpB"}
1
+ {"version":3,"file":"ios-deps.d.ts","sourceRoot":"","sources":["../../../../src/yaml/steps/ios-deps.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,OAAO,EAAE,gBAAgB,EAAE,MAAM,WAAW,CAAC;AAE7C,OAAO,KAAK,EAAE,OAAO,EAAE,QAAQ,EAAE,MAAM,gBAAgB,CAAC;AACxD,OAAO,KAAK,EAAE,qBAAqB,EAAc,MAAM,wBAAwB,CAAC;AAMhF,MAAM,WAAW,sBAAsB;IACrC,0CAA0C;IAC1C,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,4DAA4D;IAC5D,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,4DAA4D;IAC5D,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB;;;;;;;;;;;;;;;;;OAiBG;IACH,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,uCAAuC;IACvC,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB;;;;;;;;;;;;;;OAcG;IACH,cAAc,CAAC,EAAE,OAAO,CAAC;CAC1B;AAED;;;;GAIG;AACH,qBAAa,4BAA6B,SAAQ,gBAAgB;IAChE,yBAAyB,CACvB,OAAO,EAAE,sBAAsB,EAC/B,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,EAC5B,OAAO,EAAE,QAAQ,GAChB,qBAAqB,EAAE;IAUpB,OAAO,CACX,MAAM,EAAE,sBAAsB,EAC9B,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,EAC5B,OAAO,EAAE,QAAQ,GAChB,OAAO,CAAC,OAAO,CAAC;CAiLpB;AAMD,MAAM,WAAW,cAAc;IAC7B,mDAAmD;IACnD,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,kDAAkD;IAClD,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,wDAAwD;IACxD,mBAAmB,CAAC,EAAE,MAAM,CAAC;IAC7B,6BAA6B;IAC7B,OAAO,CAAC,EAAE,OAAO,CAAC;CACnB;AAED;;GAEG;AACH,qBAAa,oBAAqB,SAAQ,gBAAgB;IACxD,yBAAyB,CACvB,OAAO,EAAE,cAAc,EACvB,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,EAC5B,OAAO,EAAE,QAAQ,GAChB,qBAAqB,EAAE;IAUpB,OAAO,CACX,MAAM,EAAE,cAAc,EACtB,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,EAC5B,OAAO,EAAE,QAAQ,GAChB,OAAO,CAAC,OAAO,CAAC;CAyDpB"}
@@ -83,9 +83,14 @@ export class CocoapodsInstallStepExecutor extends BaseStepExecutor {
83
83
  //
84
84
  // Three moves, each conditional, so a machine missing a piece keeps the
85
85
  // old behaviour rather than failing on it:
86
- // - prefer an installed Homebrew Ruby. It is keg-only, so it is not on
87
- // PATH even where it is installed (Homebrew's cocoapods and fastlane
88
- // formulae depend on it), and the system 2.6 wins by default;
86
+ // - run the Ruby the project names: Gemfile.lock's `RUBY VERSION`, then
87
+ // `.ruby-version`. Only a named Ruby 3 or newer moves to Homebrew's
88
+ // Ruby, which is keg-only, so not on PATH even where it is installed
89
+ // (Homebrew's cocoapods and fastlane formulae depend on it). Anything
90
+ // else, including a project that names none, stays on system Ruby 2.6.
91
+ // Homebrew's is Ruby 4 on the mac image, and an old lockfile's gems do
92
+ // not load there — rake 13.0.6 requires `ostruct`, no longer a default
93
+ // gem — so "newest installed" is the wrong default;
89
94
  // - point GEM_HOME at a writable directory outside the checkout, so both
90
95
  // `gem install bundler` and `bundle install` can write without sudo,
91
96
  // and put its bin directory first;
@@ -93,10 +98,34 @@ export class CocoapodsInstallStepExecutor extends BaseStepExecutor {
93
98
  // `bundle _<version>_`, which is the form RubyGems accepts.
94
99
  commands.push('BUNDLE_CMD="bundle"');
95
100
  commands.push('if [ "$USE_BUNDLE_EXEC" = "true" ]; then');
96
- commands.push(' if command -v brew &>/dev/null; then');
97
- commands.push(' BREW_RUBY="$(brew --prefix --installed ruby 2>/dev/null || true)"');
98
- commands.push(' if [ -n "$BREW_RUBY" ] && [ -x "$BREW_RUBY/bin/ruby" ]; then');
99
- commands.push(' export PATH="$BREW_RUBY/bin:$PATH"');
101
+ commands.push(' WANTED_RUBY=""');
102
+ commands.push(' WANTED_FROM=""');
103
+ commands.push(' if [ -f "Gemfile.lock" ]; then');
104
+ commands.push(' WANTED_RUBY="$(awk \'/^RUBY VERSION/ { getline; sub(/^[[:space:]]*ruby[[:space:]]+/, ""); sub(/p[0-9]+.*$/, ""); gsub(/[[:space:]]/, ""); print; exit }\' Gemfile.lock)"');
105
+ commands.push(' [ -n "$WANTED_RUBY" ] && WANTED_FROM="Gemfile.lock RUBY VERSION"');
106
+ commands.push(' fi');
107
+ commands.push(' if [ -z "$WANTED_RUBY" ] && [ -f ".ruby-version" ]; then');
108
+ commands.push(' WANTED_RUBY="$(head -n 1 .ruby-version | tr -d \'[:space:]\')"');
109
+ commands.push(' WANTED_RUBY="${WANTED_RUBY#ruby-}"');
110
+ commands.push(' WANTED_FROM=".ruby-version"');
111
+ commands.push(' fi');
112
+ commands.push(' case "${WANTED_RUBY%%.*}" in');
113
+ commands.push(' \'\'|*[!0-9]*) WANTED_RUBY="" ;;');
114
+ commands.push(' esac');
115
+ commands.push(' if [ -z "$WANTED_RUBY" ]; then');
116
+ commands.push(' RUBY_REASON="system: lockfile names no Ruby"');
117
+ commands.push(' else');
118
+ commands.push(' RUBY_REASON="from $WANTED_FROM $WANTED_RUBY"');
119
+ commands.push(' if [ "${WANTED_RUBY%%.*}" -ge 3 ]; then');
120
+ commands.push(' BREW_RUBY=""');
121
+ commands.push(' if command -v brew &>/dev/null; then');
122
+ commands.push(' BREW_RUBY="$(brew --prefix --installed ruby 2>/dev/null || true)"');
123
+ commands.push(' fi');
124
+ commands.push(' if [ -n "$BREW_RUBY" ] && [ -x "$BREW_RUBY/bin/ruby" ]; then');
125
+ commands.push(' export PATH="$BREW_RUBY/bin:$PATH"');
126
+ commands.push(' else');
127
+ commands.push(' RUBY_REASON="$RUBY_REASON, but no Homebrew Ruby is installed"');
128
+ commands.push(' fi');
100
129
  commands.push(' fi');
101
130
  commands.push(' fi');
102
131
  commands.push(' if ! command -v bundle &>/dev/null && ! command -v gem &>/dev/null; then');
@@ -109,7 +138,7 @@ export class CocoapodsInstallStepExecutor extends BaseStepExecutor {
109
138
  commands.push(' mkdir -p "$GEM_HOME"');
110
139
  commands.push(' export PATH="$GEM_HOME/bin:$PATH"');
111
140
  commands.push(' RUBY_VERSION_USED="$(ruby -e \'print RUBY_VERSION\' 2>/dev/null || echo unknown)"');
112
- commands.push(' echo "Ruby: $(command -v ruby || echo none) ($RUBY_VERSION_USED), GEM_HOME: $GEM_HOME"');
141
+ commands.push(' echo "Ruby: $(command -v ruby || echo none) ($RUBY_VERSION_USED), GEM_HOME: $GEM_HOME — $RUBY_REASON"');
113
142
  commands.push(' LOCKED_BUNDLER=""');
114
143
  commands.push(' if [ -f "Gemfile.lock" ]; then');
115
144
  commands.push(' LOCKED_BUNDLER="$(awk \'/^BUNDLED WITH/ { getline; gsub(/[[:space:]]/, ""); print; exit }\' Gemfile.lock)"');
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@invarn/cibuild",
3
- "version": "2.8.6",
3
+ "version": "2.8.8",
4
4
  "description": "CI Build CLI — local pipeline orchestration and validation",
5
5
  "type": "module",
6
6
  "main": "dist/cli.cjs",