opencode-ruby-upgrader 0.1.7 → 0.1.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/PRIVACY.md +2 -0
- package/README.md +39 -8
- package/RELEASE_CHECKLIST.md +1 -1
- package/RELEASE_NOTES.md +64 -0
- package/agents/ruby-upgrade.md +4 -2
- package/bin/opencode-ruby-upgrader.mjs +18 -2
- package/docs/bundler-bridge.md +107 -0
- package/package.json +35 -7
- package/src/advisory.js +140 -0
- package/src/bundler-compat.js +68 -0
- package/src/commit-hop.js +24 -13
- package/src/controller.js +81 -7
- package/src/dashboard-page.js +1 -1
- package/src/dashboard.js +9 -1
- package/src/databases.js +35 -7
- package/src/inventory.js +5 -1
- package/src/run-state.js +91 -2
- package/src/target-runtime.js +24 -8
package/src/run-state.js
CHANGED
|
@@ -2,6 +2,7 @@ import crypto from "node:crypto";
|
|
|
2
2
|
import fs from "node:fs";
|
|
3
3
|
import path from "node:path";
|
|
4
4
|
import { execFileSync } from "node:child_process";
|
|
5
|
+
import { advisoryFindings } from "./advisory.js";
|
|
5
6
|
|
|
6
7
|
const SECRET_VALUE = /(?:gh[pousr]_[A-Za-z0-9_]{20,}|github_pat_[A-Za-z0-9_]{20,}|\bAKIA[0-9A-Z]{16}\b|-----BEGIN (?:[A-Z ]+ )?PRIVATE KEY-----|\b(?:xox[baprs]-|npm_|glpat-)[A-Za-z0-9_-]{16,})/g;
|
|
7
8
|
const SECRET_KEY = /(?:password|secret|token|api[_-]?key|credential|authorization)/i;
|
|
@@ -30,6 +31,8 @@ export function redactSourceUrl(value) {
|
|
|
30
31
|
|
|
31
32
|
function validVersion(value) { return typeof value === "string" && /^\d+\.\d+(?:\.(?:\d+|x))?$/.test(value); }
|
|
32
33
|
function validRailsVersion(value) { return typeof value === "string" && /^\d+(?:\.\d+)+$/.test(value); }
|
|
34
|
+
function validBundlerVersion(value) { return typeof value === "string" && /^\d+(?:\.\d+)*$/.test(value); }
|
|
35
|
+
const bundlerSeriesOf = (value) => String(value ?? "").split(".").slice(0, 2).join(".");
|
|
33
36
|
function validCitation(value) { return value && typeof value.title === "string" && /^https:\/\//.test(value.url ?? ""); }
|
|
34
37
|
function safeFiles(files) { return Array.isArray(files) && files.every((file) => typeof file === "string" && !path.isAbsolute(file) && !file.includes("..")); }
|
|
35
38
|
function validAppUpdateReview(review) { return review && review.command === "bin/rails app:update" && typeof review.receiptId === "string" && !Number.isNaN(Date.parse(review.executedAt ?? "")) && !Number.isNaN(Date.parse(review.reviewedAt ?? "")) && validFingerprint(review.worktree) && ["no_changes", "changes_applied", "changes_deferred"].includes(review.outcome) && safeFiles(review.files) && typeof review.summary === "string" && Boolean(review.summary); }
|
|
@@ -59,6 +62,31 @@ function receiptErrors(iteration, index, rails, required, errors) {
|
|
|
59
62
|
}
|
|
60
63
|
}
|
|
61
64
|
|
|
65
|
+
function validateBundlerBridge(run, errors) {
|
|
66
|
+
for (const key of ["runId", "title", "startedAt", "targetBundler", "targetBundlerPinnedAt", "phase", "status", "bridge"]) if (!run[key]) errors.push(`Bundler bridge report requires ${key}.`);
|
|
67
|
+
if (!validBundlerVersion(run.targetBundler)) errors.push("targetBundler must be a Bundler version.");
|
|
68
|
+
if (typeof run.runId !== "string" || !/^[a-f0-9-]{36}$/i.test(run.runId ?? "")) errors.push("runId must be a UUID.");
|
|
69
|
+
if (run.startedAt && Number.isNaN(Date.parse(run.startedAt))) errors.push("startedAt must be ISO-8601.");
|
|
70
|
+
if (!statuses.has(run.status) || !phases.has(run.phase)) errors.push("Bundler bridge has invalid status or phase.");
|
|
71
|
+
if (run.lockNonce !== undefined && (typeof run.lockNonce !== "string" || !/^[a-f0-9-]{36}$/i.test(run.lockNonce))) errors.push("lockNonce must be a UUID.");
|
|
72
|
+
if (run.targetBundlerPinnedAt && Number.isNaN(Date.parse(run.targetBundlerPinnedAt))) errors.push("targetBundlerPinnedAt must be ISO-8601.");
|
|
73
|
+
// The link back to the blocked Ruby run is what makes the bridge separately
|
|
74
|
+
// scoped rather than a second phase of the same run, so it must be complete.
|
|
75
|
+
if (!run.bridge || typeof run.bridge !== "object" || typeof run.bridge.rubyReportPath !== "string" || !/^[a-f0-9-]{36}$/i.test(run.bridge.rubyRunId ?? "") || !validVersion(run.bridge.rubyFrom) || !validVersion(run.bridge.rubyTo) || !validBundlerVersion(run.bridge.bundlerFrom) || !validBundlerVersion(run.bridge.bundlerTo) || !validBundlerVersion(run.bridge.minimumBundler)) errors.push("Bundler bridge report has invalid Ruby-run linkage.");
|
|
76
|
+
if (!Array.isArray(run.research?.ladder) || run.research.ladder.some((version) => !validBundlerVersion(version))) errors.push("Bundler bridge research.ladder must contain Bundler versions.");
|
|
77
|
+
if (!Array.isArray(run.research?.citations) || run.research.citations.some((citation) => !validCitation(citation))) errors.push("research.citations must contain HTTPS citations.");
|
|
78
|
+
if (!Array.isArray(run.iterations)) errors.push("iterations must be an array.");
|
|
79
|
+
for (const [index, iteration] of (run.iterations ?? []).entries()) {
|
|
80
|
+
if (!validBundlerVersion(iteration?.from) || !validBundlerVersion(iteration?.to) || iteration?.status !== "complete") errors.push(`Bundler iteration ${index + 1} is invalid.`);
|
|
81
|
+
if (!safeFiles(iteration?.files) || iteration?.tests?.passed !== true || !iteration.tests?.command || !iteration.tests?.smoke || !Array.isArray(iteration?.citations) || !iteration.citations.length || !Array.isArray(iteration?.fixes) || iteration.fixes.some((fix) => !safeFiles(fix.files) || !fix.explanation)) errors.push(`Bundler iteration ${index + 1} lacks required evidence.`);
|
|
82
|
+
// The pin the hop actually produced, so a later reader can see the lockfile
|
|
83
|
+
// agreed with the validated version instead of taking it on trust.
|
|
84
|
+
if (!validBundlerVersion(iteration?.lockfilePin) || bundlerSeriesOf(iteration.lockfilePin) !== bundlerSeriesOf(iteration.to)) errors.push(`Bundler iteration ${index + 1} must record the BUNDLED WITH pin it produced.`);
|
|
85
|
+
if (iteration?.checkpointSha !== undefined && !/^[a-f0-9]{40}$/i.test(iteration.checkpointSha)) errors.push(`Bundler iteration ${index + 1} checkpointSha must be a Git SHA.`);
|
|
86
|
+
receiptErrors(iteration, index, false, run.validationReceiptsRequired === true, errors);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
62
90
|
function validateRailsBridge(run, errors) {
|
|
63
91
|
for (const key of ["runId", "title", "startedAt", "targetRails", "targetRailsPinnedAt", "phase", "status", "bridge"]) if (!run[key]) errors.push(`Rails bridge report requires ${key}.`);
|
|
64
92
|
if (!validRailsVersion(run.targetRails)) errors.push("targetRails must be a Rails version.");
|
|
@@ -88,6 +116,7 @@ export function validateRun(run) {
|
|
|
88
116
|
if (run.schemaVersion !== 2) errors.push("Report schemaVersion must be 2.");
|
|
89
117
|
if (run.validationReceiptsRequired !== undefined && run.validationReceiptsRequired !== true) errors.push("validationReceiptsRequired must be true when present.");
|
|
90
118
|
if (run.reportType === "rails_bridge") { validateRailsBridge(run, errors); return { valid: errors.length === 0, errors }; }
|
|
119
|
+
if (run.reportType === "bundler_bridge") { validateBundlerBridge(run, errors); return { valid: errors.length === 0, errors }; }
|
|
91
120
|
if (run.reportType !== undefined && run.reportType !== "ruby") errors.push("Unknown reportType.");
|
|
92
121
|
for (const key of ["runId", "title", "startedAt", "targetRuby", "targetPinnedAt", "phase", "status"]) if (!run[key]) errors.push(`Report requires ${key}.`);
|
|
93
122
|
if (run.lockNonce !== undefined && (typeof run.lockNonce !== "string" || !/^[a-f0-9-]{36}$/i.test(run.lockNonce))) errors.push("lockNonce must be a UUID.");
|
|
@@ -109,6 +138,16 @@ export function validateRun(run) {
|
|
|
109
138
|
if (typeof bridge?.rationale !== "string" || !bridge.rationale) errors.push("frameworkBridge requires a rationale.");
|
|
110
139
|
if (!Array.isArray(bridge?.citations) || !bridge.citations.length || bridge.citations.some((citation) => !validCitation(citation))) errors.push("frameworkBridge requires HTTPS citations.");
|
|
111
140
|
}
|
|
141
|
+
if (run.bundlerBridge !== undefined && run.bundlerBridge !== null) {
|
|
142
|
+
const bridge = run.bundlerBridge;
|
|
143
|
+
if (!bridge || typeof bridge !== "object" || bridge.status !== "approved") errors.push("bundlerBridge must be an approved compatibility decision.");
|
|
144
|
+
if (!validVersion(bridge?.rubyFrom) || !validVersion(bridge?.rubyTo) || !validBundlerVersion(bridge?.bundlerFrom) || !validBundlerVersion(bridge?.bundlerTo) || !validBundlerVersion(bridge?.minimumBundler)) errors.push("bundlerBridge requires Ruby from/to and Bundler from/to/floor versions.");
|
|
145
|
+
// The researched floor is what justifies blocking the hop, so its provenance
|
|
146
|
+
// has to be recorded rather than asserted.
|
|
147
|
+
if (typeof bridge?.compatibilitySource !== "string" || !/^https:\/\//.test(bridge.compatibilitySource)) errors.push("bundlerBridge requires the official compatibility source.");
|
|
148
|
+
if (typeof bridge?.rationale !== "string" || !bridge.rationale) errors.push("bundlerBridge requires a rationale.");
|
|
149
|
+
if (!Array.isArray(bridge?.citations) || !bridge.citations.length || bridge.citations.some((citation) => !validCitation(citation))) errors.push("bundlerBridge requires HTTPS citations.");
|
|
150
|
+
}
|
|
112
151
|
for (const [index, iteration] of (run.iterations ?? []).entries()) {
|
|
113
152
|
if (!validVersion(iteration?.from) || !validVersion(iteration?.to)) errors.push(`Iteration ${index + 1} requires Ruby from/to versions.`);
|
|
114
153
|
if (iteration?.status !== "complete") errors.push(`Iteration ${index + 1} must be complete.`);
|
|
@@ -173,13 +212,63 @@ export function writeRun(root, relativePath, run) {
|
|
|
173
212
|
const markdown = file.replace(/\.json$/, ".md");
|
|
174
213
|
if (fs.existsSync(markdown) && (!fs.lstatSync(markdown).isFile() || fs.lstatSync(markdown).isSymbolicLink())) throw new RunStateError("Markdown evidence must be a regular file inside the worktree.", "unsafe-report-path");
|
|
175
214
|
const safe = redact(run);
|
|
176
|
-
const target = safe.reportType === "rails_bridge" ? `- **Target Rails:** ${safe.targetRails}` : `- **Target Ruby:** ${safe.targetRuby}`;
|
|
177
|
-
const
|
|
215
|
+
const target = safe.reportType === "rails_bridge" ? `- **Target Rails:** ${safe.targetRails}` : safe.reportType === "bundler_bridge" ? `- **Target Bundler:** ${safe.targetBundler}` : `- **Target Ruby:** ${safe.targetRuby}`;
|
|
216
|
+
const followUps = followUpActions(safe);
|
|
217
|
+
const advisories = collectAdvisories(root, safe);
|
|
218
|
+
const followUpBlock = followUps.length
|
|
219
|
+
? `\n## Follow-up actions\n\nThese are yours to take. Nothing below was verified by this run; the run cannot test your production topology.\n\n${followUps.map((item) => `- ${item}`).join("\n")}\n`
|
|
220
|
+
: "";
|
|
221
|
+
// Advisory only: never a gate. Rendered as its own section so it is obvious
|
|
222
|
+
// these are observations about the reader's infrastructure, not evidence the
|
|
223
|
+
// run collected about the app.
|
|
224
|
+
const advisoryBlock = advisories.length
|
|
225
|
+
? `\n## Infrastructure review (advisory)\n\nRead-only observations about files this run did not change. Nothing here blocks the upgrade, and an intentional version pin that lags the app is legitimate. Confirm each against your actual deploy platform.\n\n| Area | Finding | Evidence | Basis |\n| --- | --- | --- | --- |\n${advisories.map((item) => `| ${item.area} | ${item.title}<br>${item.detail} | \`${item.evidence.replace(/\|/g, "\\|")}\` | ${item.confidence} |`).join("\n")}\n`
|
|
226
|
+
: "";
|
|
227
|
+
const markdownBody = `# ${safe.title}\n\n- **Status:** ${safe.status}\n- **Phase:** ${safe.phase}\n${target}\n- **Started:** ${safe.startedAt}\n${followUpBlock}${advisoryBlock}\n## Durable evidence\n\n\`\`\`json\n${JSON.stringify(safe, null, 2)}\n\`\`\`\n`;
|
|
178
228
|
const markdownTemporary = path.join(path.dirname(markdown), `.${path.basename(markdown)}.${process.pid}.${crypto.randomUUID()}.tmp`);
|
|
179
229
|
fs.writeFileSync(markdownTemporary, markdownBody, { mode: 0o600, flag: "wx" });
|
|
180
230
|
fs.renameSync(markdownTemporary, markdown);
|
|
181
231
|
}
|
|
182
232
|
|
|
233
|
+
// Deployment-visible items a local test run provably cannot check. The isolated
|
|
234
|
+
// runtime proves the code runs on the target Ruby in a container; it says nothing
|
|
235
|
+
// about the host that will actually serve it. Each item is phrased as something
|
|
236
|
+
// the reader must verify, never as something this run established, because a run
|
|
237
|
+
// that "passed" here has not touched the real deploy target at all.
|
|
238
|
+
function followUpActions(safe) {
|
|
239
|
+
const actions = [];
|
|
240
|
+
const last = safe.iterations?.at(-1);
|
|
241
|
+
if (last?.checkpointSha) actions.push(`Checkpoint \`${last.checkpointSha.slice(0, 7)}\` is validated locally but not deployed: push it and run your own staging check before it reaches production.`);
|
|
242
|
+
if (safe.phase === "blocked" || safe.status === "blocked") actions.push("This run is blocked. Resolve or explicitly approve the outstanding risks before treating the upgrade as complete.");
|
|
243
|
+
if (safe.frameworkBridge?.status === "approved") actions.push(`A Rails ${safe.frameworkBridge.railsFrom} → ${safe.frameworkBridge.railsTo} bridge was approved here but not performed. It needs its own separate run.`);
|
|
244
|
+
if (safe.bundlerBridge?.status === "approved") actions.push(`A Bundler ${safe.bundlerBridge.bundlerFrom} → ${safe.bundlerBridge.bundlerTo} bridge was approved here but not performed, on a researched floor of ${safe.bundlerBridge.minimumBundler}. Run \`begin-bundler-bridge\`, then restart the Ruby upgrade.`);
|
|
245
|
+
const environment = safe.environment;
|
|
246
|
+
if (environment?.type === "docker") {
|
|
247
|
+
// The single most common surprise: the container pins Bundler 2.4.22 and the
|
|
248
|
+
// app's own CI or deploy host may resolve a different one, changing which
|
|
249
|
+
// lockfile semantics apply.
|
|
250
|
+
actions.push(`The isolated runtime used Bundler ${environment.bundlerVersion ?? "a pinned version (unrecorded)"}${environment.bundlerVersion ? ` on Ruby ${environment.ruby}` : ""}. Confirm your deploy host and CI resolve the same Bundler version, or re-run \`bundle lock\` there.`);
|
|
251
|
+
actions.push(`Native gems were compiled for this container (${environment.database ?? "database"} on Ruby ${environment.ruby}). Rebuild native extensions on your deploy platform rather than copying \`node_modules\`-style build output.`);
|
|
252
|
+
actions.push(`Your deploy host must reach the real database directly; this run used an isolated ${environment.database} reachable only over a private Docker network and deliberately published no ports.`);
|
|
253
|
+
actions.push("Prepared containers and networks persist after the run by design. Remove them when you are done: `docker rm -f` and `docker network rm` on the names in `.ruby-upgrades/runtime.json`.");
|
|
254
|
+
}
|
|
255
|
+
actions.push("Review the validation receipts in `.ruby-upgrades/runs/` for the exact commands, exit codes, and test evidence before merging.");
|
|
256
|
+
return actions;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
// Read the worktree's own infrastructure and compare it against what this run
|
|
260
|
+
// changed. Wrapped because the report must still render when a worktree cannot
|
|
261
|
+
// be inspected (an explicit `--report` from elsewhere, or a half-removed tree);
|
|
262
|
+
// a missing advisory section must not fail a run whose evidence is already
|
|
263
|
+
// recorded.
|
|
264
|
+
export function collectAdvisories(root, safe) {
|
|
265
|
+
try {
|
|
266
|
+
return advisoryFindings({ root, run: safe, inventory: safe.inventory ?? {} });
|
|
267
|
+
} catch {
|
|
268
|
+
return [];
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
|
|
183
272
|
function lockPath(root) {
|
|
184
273
|
try { return execFileSync("git", ["rev-parse", "--git-path", "opencode-ruby-upgrade.lock"], { cwd: root, encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] }).trim(); }
|
|
185
274
|
catch { return path.join(runsDirectory(root, true), ".run.lock"); }
|
package/src/target-runtime.js
CHANGED
|
@@ -46,27 +46,36 @@ function preparationResult(result) {
|
|
|
46
46
|
return { sha256: crypto.createHash("sha256").update(output).digest("hex"), bytes: Buffer.byteLength(output) };
|
|
47
47
|
}
|
|
48
48
|
function bootstrap(spawn, runtime, db, rails) {
|
|
49
|
-
const labels = ["Node.js setup", "Bundler installation", "dependency installation", ...(rails ? [`${db.label} test database initialization`] : []), "Ruby version attestation"];
|
|
49
|
+
const labels = ["Node.js setup", "Bundler installation", "dependency installation", ...(rails ? [`${db.label} test database initialization`] : []), "Ruby version attestation", "Bundler version attestation"];
|
|
50
50
|
const commands = [
|
|
51
51
|
["exec", runtime.appContainer, "sh", "-c", NODE_INSTALL],
|
|
52
|
-
["exec", runtime.appContainer, "gem", "install", "bundler", "-v", "2.4.22", "--no-document"],
|
|
53
|
-
["exec", runtime.appContainer, "bundle", "
|
|
52
|
+
["exec", runtime.appContainer, "gem", "install", "bundler", "-v", runtime.bundlerToInstall ?? "2.4.22", "--no-document"],
|
|
53
|
+
["exec", runtime.appContainer, "bundle", `_${runtime.bundlerToInstall ?? "2.4.22"}_`, "install"],
|
|
54
54
|
...(rails ? [db.createArgs({ appContainer: runtime.appContainer, databaseContainer: runtime.databaseContainer })] : []),
|
|
55
|
-
["exec", runtime.appContainer, "ruby", "--version"]
|
|
55
|
+
["exec", runtime.appContainer, "ruby", "--version"],
|
|
56
|
+
// Attest the Bundler version actually installed, so the report can warn when
|
|
57
|
+
// CI/host bundler differs. `gem install` output is advisory; `--version` is
|
|
58
|
+
// authoritative for what `bundle exec` will actually resolve.
|
|
59
|
+
["exec", runtime.appContainer, "bundle", `_${runtime.bundlerToInstall ?? "2.4.22"}_`, "--version"]
|
|
56
60
|
];
|
|
57
61
|
const results = commands.map((args, index) => {
|
|
58
62
|
const result = docker(spawn, args);
|
|
59
63
|
if (result.status !== 0) throw new Error(`Target runtime bootstrap failed during ${labels[index]}. Rerun prepare-target-runtime --ruby <x.y.z>.`);
|
|
60
64
|
return result;
|
|
61
65
|
});
|
|
62
|
-
|
|
66
|
+
// Index the attestations explicitly rather than from the end of the array:
|
|
67
|
+
// `ruby --version` is no longer the final command, and an off-by-one here would
|
|
68
|
+
// validate the Bundler banner against the Ruby regex.
|
|
69
|
+
const rubyIndex = 3 + (rails ? 1 : 0);
|
|
70
|
+
const rubyOutput = `${results[rubyIndex].stdout ?? ""}${results[rubyIndex].stderr ?? ""}`;
|
|
63
71
|
if (!new RegExp(`^ruby ${runtime.ruby.replaceAll(".", "\\.")}(?:p\\d+|\\s|$)`).test(rubyOutput.trim())) throw new Error("Target runtime did not execute the requested Ruby version.");
|
|
64
72
|
return {
|
|
65
73
|
node: preparationResult(results[0]),
|
|
66
74
|
bundler: preparationResult(results[1]),
|
|
67
75
|
bundleInstall: preparationResult(results[2]),
|
|
68
76
|
...(rails ? { databaseCreate: preparationResult(results[3]) } : {}),
|
|
69
|
-
rubyVersion: preparationResult(results
|
|
77
|
+
rubyVersion: preparationResult(results[rubyIndex]),
|
|
78
|
+
bundlerVersion: `${results.at(-1).stdout ?? ""}${results.at(-1).stderr ?? ""}`.trim()
|
|
70
79
|
};
|
|
71
80
|
}
|
|
72
81
|
function railsProject(root) {
|
|
@@ -164,7 +173,9 @@ export function prepareTargetRuntime({ root = process.cwd(), reportPath, ruby, d
|
|
|
164
173
|
const canonical = canonicalRoot(root);
|
|
165
174
|
const db = resolveDatabase(database ?? detectDatabase(canonical));
|
|
166
175
|
const selected = selectedRun(canonical, reportPath);
|
|
167
|
-
const
|
|
176
|
+
const run = readRun(canonical, selected.reportPath);
|
|
177
|
+
const bundlerToInstall = run.reportType === "bundler_bridge" ? run.targetBundler : (run.bundlerBridge ? run.bundlerBridge.bundlerTo : undefined) || "2.4.22";
|
|
178
|
+
const runtime = { version: 2, runId: selected.run.runId, reportPath: selected.reportPath, ruby, database: db.adapter, bundlerToInstall, ...names(selected.run.runId, db) };
|
|
168
179
|
const owner = identity(runtime.runId, canonical);
|
|
169
180
|
const existing = readTargetRuntime(canonical);
|
|
170
181
|
if (existing && existing.runId !== runtime.runId) {
|
|
@@ -250,5 +261,10 @@ export function validateTargetRuntime({ root = process.cwd(), runtime = readTarg
|
|
|
250
261
|
const expectedIds = [app?.Id, database?.Id].sort();
|
|
251
262
|
const valid = appMatches(app, canonical, runtime, db) && databaseMatches(database, runtime, db) && hasOwnership(network, owner) && hasOwnership(app, owner) && hasOwnership(database, owner) && connectedIds.length === 2 && connectedIds.every((id, index) => id === expectedIds[index]) && app?.Image === runtime.resolvedImageId && (!runtime.databaseImageId || database?.Image === runtime.databaseImageId) && env.RAILS_ENV === "test" && env.DATABASE_URL === db.databaseUrl(runtime.databaseContainer);
|
|
252
263
|
if (!valid) throw new Error("Target runtime no longer matches its prepared run. Rerun prepare-target-runtime --ruby <x.y.z>.");
|
|
253
|
-
|
|
264
|
+
// bundlerVersion is surfaced so the generated report can warn that the deploy
|
|
265
|
+
// host and CI may resolve a different Bundler than this container pinned. It
|
|
266
|
+
// comes from `bundle --version` in the prepared container, not from the
|
|
267
|
+
// bootstrap log, so it reflects what `bundle exec` actually resolved.
|
|
268
|
+
const bundlerVersion = /Bundler version (\d+(?:\.\d+)+)/.exec(runtime.preparation?.bundlerVersion ?? "")?.[1] ?? undefined;
|
|
269
|
+
return { runId: runtime.runId, reportPath: runtime.reportPath, name: runtime.appContainer, id: app.Id, imageId: app.Image, imageRef: app.Config.Image, ruby: runtime.ruby, ...(bundlerVersion ? { bundlerVersion } : {}), database: runtime.database, databaseContainer: runtime.databaseContainer, databaseContainerId: database.Id, databaseImageId: database.Image, databaseImageRef: database.Config.Image, network: runtime.network, networkId: network.Id };
|
|
254
270
|
}
|