openship 0.6.6 → 0.6.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/bare-FA66N7GC.js +29 -0
- package/dist/{chunk-77QDF6UD.js → chunk-3MP6ACKM.js} +587 -156
- package/dist/{chunk-YIMOCZBZ.js → chunk-5DJOB7IX.js} +3 -3
- package/dist/{chunk-WVAML4H6.js → chunk-5I3COAFH.js} +274 -65
- package/dist/{chunk-5DMWSTOM.js → chunk-5UXHT6ED.js} +940 -224
- package/dist/{chunk-LUKYPVJ4.js → chunk-6GU6JSK6.js} +329 -466
- package/dist/{chunk-RSR7PFIQ.js → chunk-7PEXM2UI.js} +213 -45
- package/dist/{chunk-VTGPEC6B.js → chunk-7YBWXEM6.js} +6 -4
- package/dist/chunk-AHJYP7DG.js +938 -0
- package/dist/chunk-BO6CY4V2.js +265 -0
- package/dist/{chunk-FE7ZZQSE.js → chunk-D3OEMXYP.js} +3 -3
- package/dist/chunk-D6HU4VUZ.js +249 -0
- package/dist/{chunk-LFE5KQ7Y.js → chunk-ENKJ7XIF.js} +152 -57
- package/dist/{chunk-YMMQRE5J.js → chunk-GNOMEMNI.js} +1 -1
- package/dist/{chunk-QVW76WDL.js → chunk-IAMOUDHY.js} +190 -64
- package/dist/{chunk-OBPKYXQL.js → chunk-MUWVC5R4.js} +219 -58
- package/dist/chunk-N5YZMO33.js +111 -0
- package/dist/{chunk-A4GXYE4L.js → chunk-NIYD7MD2.js} +206 -52
- package/dist/{chunk-UVNMQM7W.js → chunk-NPSRGIHV.js} +8 -6
- package/dist/{chunk-2GWYYHOR.js → chunk-NRU2EBYZ.js} +2 -2
- package/dist/{chunk-CBLD2HS5.js → chunk-NUT6HKK6.js} +8 -5
- package/dist/{chunk-ZFJXPCQP.js → chunk-P7RCYELG.js} +4 -3
- package/dist/chunk-R4VDPFLS.js +106 -0
- package/dist/{chunk-HKRQXPTI.js → chunk-R5DC5PEF.js} +1 -1
- package/dist/{chunk-NK5N5LXN.js → chunk-RCMERVBL.js} +24 -9
- package/dist/{chunk-RXQBD4ZY.js → chunk-RHLNVZMS.js} +2 -2
- package/dist/chunk-SK4FFCFR.js +70 -0
- package/dist/{chunk-4I2BM2DT.js → chunk-T46BQOLY.js} +3 -3
- package/dist/{chunk-6FH4AXVY.js → chunk-T6DVUX4F.js} +1 -1
- package/dist/{chunk-PCPVTH6M.js → chunk-TAZZZKYO.js} +6 -4
- package/dist/{chunk-MLYSEZQB.js → chunk-UAWY6X7V.js} +43 -72
- package/dist/{chunk-Q7WGTQQ5.js → chunk-VAI5VOOI.js} +1 -1
- package/dist/{chunk-UJ6JZM65.js → chunk-WEZFTL4V.js} +1325 -513
- package/dist/{chunk-2YM3NI2E.js → chunk-X53JXRS4.js} +38 -44
- package/dist/{cloud-KRTZ2IL5.js → cloud-2CRDKOWQ.js} +1 -1
- package/dist/cloud-REV3DPJH.js +42 -0
- package/dist/{detect-OIE5V3VZ.js → detect-BBFT5GEU.js} +9 -6
- package/dist/{docker-VTR2GK5Y.js → docker-EG3QSE6I.js} +11 -8
- package/dist/{docker-edge-executor-22IBXNYJ.js → docker-edge-executor-E5TOTLMR.js} +8 -6
- package/dist/edge-import-YJVJUGYC.js +56 -0
- package/dist/{edge-preflight-GBNCPNVU.js → edge-preflight-QUW4BM7N.js} +15 -12
- package/dist/{ensure-container-edge-KPI2CVXJ.js → ensure-container-edge-FOEFQ3PI.js} +15 -10
- package/dist/{executor-7LXXZPBR.js → executor-NTWDJ3DB.js} +8 -6
- package/dist/host-channel-preflight-OMCZMMKR.js +54 -0
- package/dist/index.js +198 -76
- package/dist/{local-executor-WOFZD72X.js → local-executor-OQ2GRN7K.js} +3 -2
- package/dist/{nginx-KUFMLMAC.js → nginx-4U4C2XUS.js} +11 -8
- package/dist/{noop-NJ72F46T.js → noop-CLLJHCPL.js} +1 -1
- package/dist/{openresty-lua-ATTH35BE.js → openresty-lua-LLWT5XQW.js} +8 -5
- package/dist/{privilege-I4W3ZNZK.js → privilege-MNDVMDST.js} +6 -5
- package/dist/server/index.js +40049 -30378
- package/dist/server/migrations/0109_service_build_args.sql +9 -0
- package/dist/server/migrations/0110_service_deployment_host_ports.sql +18 -0
- package/dist/server/migrations/0111_host_port_claim.sql +381 -0
- package/dist/server/migrations/0112_backup_run_execution_lease.sql +12 -0
- package/dist/server/migrations/0113_docker_migration_execution_lease.sql +11 -0
- package/dist/server/migrations/0114_build_session_execution_lease_index.sql +6 -0
- package/dist/server/migrations/0115_orphan_target_key.sql +3 -0
- package/dist/server/migrations/0116_domain_ssl_challenge.sql +2 -0
- package/dist/server/migrations/meta/_journal.json +56 -0
- package/dist/setup-IFIMPQZJ.js +25 -0
- package/dist/{ssh-client-JBMA7SN4.js → ssh-client-2XWZCMGI.js} +3 -2
- package/dist/up-HQS7IR7H.js +61 -0
- package/package.json +1 -1
- package/dist/bare-OXJVW3AK.js +0 -25
- package/dist/chunk-LH2PLRQO.js +0 -434
- package/dist/chunk-SM26HDS2.js +0 -185
- package/dist/cloud-75VIT4KV.js +0 -37
- package/dist/edge-import-WJUB4M7F.js +0 -52
- package/dist/host-channel-preflight-KE6OONYK.js +0 -50
- package/dist/setup-XGLGUUFW.js +0 -22
- package/dist/up-K4BV7466.js +0 -57
|
@@ -0,0 +1,938 @@
|
|
|
1
|
+
#!/usr/bin/env sh
|
|
2
|
+
":" //# ; exec "$(command -v node || command -v bun)" "$0" "$@"
|
|
3
|
+
import { createRequire as __ospCreateRequire } from "node:module";
|
|
4
|
+
const require = __ospCreateRequire(import.meta.url);
|
|
5
|
+
import {
|
|
6
|
+
getTarCreateEnv,
|
|
7
|
+
prepareSourceTarArgs
|
|
8
|
+
} from "./chunk-VAI5VOOI.js";
|
|
9
|
+
import {
|
|
10
|
+
TRANSFER_EXCLUDES,
|
|
11
|
+
nodeBinDirs,
|
|
12
|
+
nodeBinPathExport,
|
|
13
|
+
packageManagerEnsureCommand,
|
|
14
|
+
safeErrorMessage,
|
|
15
|
+
shellQuote
|
|
16
|
+
} from "./chunk-WEZFTL4V.js";
|
|
17
|
+
|
|
18
|
+
// ../../packages/adapters/src/runtime/git-clone.ts
|
|
19
|
+
var sq = shellQuote;
|
|
20
|
+
function gitCredentialPair(hostname, token) {
|
|
21
|
+
if (hostname === "github.com" && !token.startsWith("ghs_")) {
|
|
22
|
+
return { username: token, password: "x-oauth-basic" };
|
|
23
|
+
}
|
|
24
|
+
return { username: "x-access-token", password: token };
|
|
25
|
+
}
|
|
26
|
+
function injectGitToken(repoUrl, token) {
|
|
27
|
+
const secret = token?.trim();
|
|
28
|
+
if (!secret) return repoUrl;
|
|
29
|
+
try {
|
|
30
|
+
const url = new URL(repoUrl);
|
|
31
|
+
if (url.protocol !== "https:") return repoUrl;
|
|
32
|
+
const { username, password } = gitCredentialPair(url.hostname, secret);
|
|
33
|
+
url.username = username;
|
|
34
|
+
url.password = password;
|
|
35
|
+
return url.toString();
|
|
36
|
+
} catch {
|
|
37
|
+
return repoUrl;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
function toGitHubSshUrl(repoUrl) {
|
|
41
|
+
try {
|
|
42
|
+
const url = new URL(repoUrl);
|
|
43
|
+
const host = url.hostname;
|
|
44
|
+
const repoPath = url.pathname.replace(/^\/+/, "").replace(/\.git$/, "");
|
|
45
|
+
return `git@${host}:${repoPath}.git`;
|
|
46
|
+
} catch {
|
|
47
|
+
return repoUrl;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
var TERMINAL_PROMPT = ["GIT_TERMINAL_PROMPT", "0", false];
|
|
51
|
+
function invocation(cloneUrl, entries, disableHelper) {
|
|
52
|
+
return {
|
|
53
|
+
cloneUrl,
|
|
54
|
+
gitEnv: entries.map(([k, v, q]) => `${k}=${q ? sq(v) : v}`).join(" "),
|
|
55
|
+
credFlag: disableHelper ? "-c credential.helper=" : "",
|
|
56
|
+
env: Object.fromEntries(entries.map(([k, v]) => [k, v])),
|
|
57
|
+
credArgs: disableHelper ? ["-c", "credential.helper="] : []
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
function assertNotOptionLike(repoUrl) {
|
|
61
|
+
if (repoUrl.trimStart().startsWith("-")) {
|
|
62
|
+
throw new Error(
|
|
63
|
+
`Refusing to clone from ${JSON.stringify(repoUrl)}: a repository URL must not start with "-" (git would read it as an option).`
|
|
64
|
+
);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
function assembleGitClone(auth) {
|
|
68
|
+
assertNotOptionLike(auth.repoUrl);
|
|
69
|
+
if (auth.ssh) {
|
|
70
|
+
const sshCmd = `ssh -i ${sq(auth.ssh.keyFile)} -o IdentitiesOnly=yes -o StrictHostKeyChecking=yes -o UserKnownHostsFile=${sq(auth.ssh.knownHostsFile)}`;
|
|
71
|
+
return invocation(
|
|
72
|
+
toGitHubSshUrl(auth.repoUrl),
|
|
73
|
+
[TERMINAL_PROMPT, ["GIT_SSH_COMMAND", sshCmd, true]],
|
|
74
|
+
false
|
|
75
|
+
);
|
|
76
|
+
}
|
|
77
|
+
if (auth.gitCredentialHelperPath) {
|
|
78
|
+
return invocation(
|
|
79
|
+
auth.repoUrl,
|
|
80
|
+
[
|
|
81
|
+
TERMINAL_PROMPT,
|
|
82
|
+
["GIT_CONFIG_COUNT", "2", false],
|
|
83
|
+
["GIT_CONFIG_KEY_0", "credential.helper", false],
|
|
84
|
+
["GIT_CONFIG_VALUE_0", auth.gitCredentialHelperPath, true],
|
|
85
|
+
["GIT_CONFIG_KEY_1", "credential.useHttpPath", false],
|
|
86
|
+
["GIT_CONFIG_VALUE_1", "true", false]
|
|
87
|
+
],
|
|
88
|
+
false
|
|
89
|
+
);
|
|
90
|
+
}
|
|
91
|
+
if (auth.ambient) {
|
|
92
|
+
if (auth.ambient.via === "ssh") {
|
|
93
|
+
return invocation(toGitHubSshUrl(auth.repoUrl), [TERMINAL_PROMPT], false);
|
|
94
|
+
}
|
|
95
|
+
if (auth.ambient.via === "gh") {
|
|
96
|
+
return invocation(
|
|
97
|
+
auth.repoUrl,
|
|
98
|
+
[
|
|
99
|
+
TERMINAL_PROMPT,
|
|
100
|
+
["GIT_CONFIG_COUNT", "1", false],
|
|
101
|
+
["GIT_CONFIG_KEY_0", "credential.helper", false],
|
|
102
|
+
["GIT_CONFIG_VALUE_0", "!gh auth git-credential", true]
|
|
103
|
+
],
|
|
104
|
+
false
|
|
105
|
+
);
|
|
106
|
+
}
|
|
107
|
+
return invocation(auth.repoUrl, [TERMINAL_PROMPT], false);
|
|
108
|
+
}
|
|
109
|
+
return invocation(
|
|
110
|
+
injectGitToken(auth.repoUrl, auth.gitToken),
|
|
111
|
+
[TERMINAL_PROMPT, ["GIT_ASKPASS", "/bin/echo", false]],
|
|
112
|
+
true
|
|
113
|
+
);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// ../../packages/adapters/src/runtime/git-ssh-material.ts
|
|
117
|
+
import { mkdir, rm, writeFile } from "fs/promises";
|
|
118
|
+
async function materializeGitSsh(writer, dir, gitSsh) {
|
|
119
|
+
const keyFile = `${dir}/id`;
|
|
120
|
+
const knownHostsFile = `${dir}/known_hosts`;
|
|
121
|
+
await writer.ensureDir(dir);
|
|
122
|
+
try {
|
|
123
|
+
await writer.writeSecret(keyFile, gitSsh.privateKey);
|
|
124
|
+
await writer.writeSecret(knownHostsFile, gitSsh.knownHosts);
|
|
125
|
+
} catch (err) {
|
|
126
|
+
await writer.remove(dir);
|
|
127
|
+
throw err;
|
|
128
|
+
}
|
|
129
|
+
return {
|
|
130
|
+
keyFile,
|
|
131
|
+
knownHostsFile,
|
|
132
|
+
cleanup: () => writer.remove(dir)
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
function shellGitSshWriter(io) {
|
|
136
|
+
return {
|
|
137
|
+
ensureDir: async (dir) => {
|
|
138
|
+
await io.exec(`mkdir -p ${sq(dir)} && chmod 700 ${sq(dir)}`);
|
|
139
|
+
},
|
|
140
|
+
writeSecret: async (path, content) => {
|
|
141
|
+
await io.writeSecret(path, content);
|
|
142
|
+
await io.exec(`chmod 600 ${sq(path)}`);
|
|
143
|
+
},
|
|
144
|
+
remove: async (dir) => {
|
|
145
|
+
await io.exec(`rm -rf ${sq(dir)}`).catch(() => {
|
|
146
|
+
});
|
|
147
|
+
}
|
|
148
|
+
};
|
|
149
|
+
}
|
|
150
|
+
function localGitSshWriter() {
|
|
151
|
+
return {
|
|
152
|
+
ensureDir: (dir) => mkdir(dir, { recursive: true, mode: 448 }).then(() => {
|
|
153
|
+
}),
|
|
154
|
+
writeSecret: (path, content) => writeFile(path, content, { mode: 384 }),
|
|
155
|
+
remove: (dir) => rm(dir, { recursive: true, force: true }).catch(() => {
|
|
156
|
+
})
|
|
157
|
+
};
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// ../../packages/adapters/src/runtime/build-pipeline.ts
|
|
161
|
+
var BuildLogger = class {
|
|
162
|
+
constructor(onLog) {
|
|
163
|
+
this.onLog = onLog;
|
|
164
|
+
}
|
|
165
|
+
/** Emit a plain log line. */
|
|
166
|
+
log(message, level = "info", meta) {
|
|
167
|
+
this.onLog?.({ timestamp: (/* @__PURE__ */ new Date()).toISOString(), message, level, ...meta });
|
|
168
|
+
}
|
|
169
|
+
/** Emit a step lifecycle event (running / completed / failed / skipped). */
|
|
170
|
+
step(step, status, message) {
|
|
171
|
+
this.onLog?.({
|
|
172
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
173
|
+
message,
|
|
174
|
+
level: status === "failed" ? "error" : "info",
|
|
175
|
+
step,
|
|
176
|
+
stepStatus: status
|
|
177
|
+
});
|
|
178
|
+
}
|
|
179
|
+
/**
|
|
180
|
+
* Run a step: emit running → execute → emit completed/failed.
|
|
181
|
+
* Throws on failure so the caller can handle it.
|
|
182
|
+
*/
|
|
183
|
+
async runStep(step, label, fn) {
|
|
184
|
+
this.step(step, "running", label);
|
|
185
|
+
try {
|
|
186
|
+
await fn();
|
|
187
|
+
this.step(step, "completed", `${label} - done`);
|
|
188
|
+
} catch (err) {
|
|
189
|
+
const msg = safeErrorMessage(err);
|
|
190
|
+
this.step(step, "failed", `${label} - ${msg}`);
|
|
191
|
+
throw err;
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
/** Get the underlying callback for passing to exec / stream functions. */
|
|
195
|
+
get callback() {
|
|
196
|
+
return (entry) => this.onLog?.(entry);
|
|
197
|
+
}
|
|
198
|
+
};
|
|
199
|
+
var BuildCancelledError = class extends Error {
|
|
200
|
+
constructor(message = "Build cancelled") {
|
|
201
|
+
super(message);
|
|
202
|
+
this.name = "BuildCancelledError";
|
|
203
|
+
}
|
|
204
|
+
};
|
|
205
|
+
async function killProcessesUnderDir(executor, dir, opts) {
|
|
206
|
+
const quoted = sq(dir);
|
|
207
|
+
const roots = opts?.includeSuffixed ? [quoted, `${quoted}-*`] : [quoted];
|
|
208
|
+
const patterns = roots.flatMap((root) => [
|
|
209
|
+
root,
|
|
210
|
+
`${root}/*`,
|
|
211
|
+
`${root}" (deleted)"`,
|
|
212
|
+
`${root}/*" (deleted)"`
|
|
213
|
+
]);
|
|
214
|
+
const scan = (sig) => `for p in /proc/[0-9]*; do c=$(readlink "$p/cwd" 2>/dev/null); case "$c" in ${patterns.join("|")}) kill -${sig} "\${p##*/}" 2>/dev/null || true;; esac; done`;
|
|
215
|
+
await executor.exec(`${scan("TERM")}; sleep 2; ${scan("KILL")}`).catch(() => {
|
|
216
|
+
});
|
|
217
|
+
}
|
|
218
|
+
async function runBuildPipeline(env, config, logger) {
|
|
219
|
+
let startTime = Date.now();
|
|
220
|
+
let currentStep = "prepare";
|
|
221
|
+
const exec = (command) => env.exec(command, logger.callback);
|
|
222
|
+
const buildDir = resolveBuildDirectory(env.projectDir, config.rootDirectory);
|
|
223
|
+
if (env.hasNativeEnv) {
|
|
224
|
+
const { cpuCores, memoryMb, diskMb } = config.resources;
|
|
225
|
+
logger.log(`Machine: ${cpuCores} CPU \xB7 ${memoryMb} MB RAM \xB7 ${diskMb} MB Disk`);
|
|
226
|
+
}
|
|
227
|
+
try {
|
|
228
|
+
if (env.preflight) {
|
|
229
|
+
await logger.runStep("prepare", "Preparing server", async () => {
|
|
230
|
+
await env.preflight(config, logger);
|
|
231
|
+
});
|
|
232
|
+
}
|
|
233
|
+
startTime = Date.now();
|
|
234
|
+
currentStep = "clone";
|
|
235
|
+
if (config.localPath || config.sourceStaged) {
|
|
236
|
+
logger.step("clone", "completed", "Source ready");
|
|
237
|
+
} else {
|
|
238
|
+
await logger.runStep(
|
|
239
|
+
"clone",
|
|
240
|
+
`Cloning ${config.repoUrl} (branch: ${config.branch})`,
|
|
241
|
+
async () => {
|
|
242
|
+
let sshMaterial;
|
|
243
|
+
if (config.gitSsh) {
|
|
244
|
+
const writeSecretFile = env.writeSecretFile;
|
|
245
|
+
if (!writeSecretFile) {
|
|
246
|
+
throw new Error(
|
|
247
|
+
"SSH-based GitHub auth isn't supported on this build runtime \u2014 use a token or the tunnel relay."
|
|
248
|
+
);
|
|
249
|
+
}
|
|
250
|
+
sshMaterial = await materializeGitSsh(
|
|
251
|
+
shellGitSshWriter({ exec, writeSecret: writeSecretFile }),
|
|
252
|
+
`${env.projectDir}.gitssh`,
|
|
253
|
+
config.gitSsh
|
|
254
|
+
);
|
|
255
|
+
}
|
|
256
|
+
const { cloneUrl, gitEnv: GIT_ENV, credFlag: CRED } = assembleGitClone({
|
|
257
|
+
repoUrl: config.repoUrl,
|
|
258
|
+
gitToken: config.gitToken,
|
|
259
|
+
gitCredentialHelperPath: config.gitCredentialHelperPath,
|
|
260
|
+
ssh: sshMaterial,
|
|
261
|
+
ambient: config.gitAmbient
|
|
262
|
+
});
|
|
263
|
+
try {
|
|
264
|
+
if (config.commitSha) {
|
|
265
|
+
try {
|
|
266
|
+
await exec(
|
|
267
|
+
`${GIT_ENV} git ${CRED} clone --progress --depth 50 --branch ${sq(config.branch)} ${sq(cloneUrl)} ${sq(env.projectDir)} && cd ${sq(env.projectDir)} && git ${CRED} -c advice.detachedHead=false checkout ${sq(config.commitSha)}`
|
|
268
|
+
);
|
|
269
|
+
} catch {
|
|
270
|
+
logger.log(
|
|
271
|
+
`Checkout of ${config.commitSha} failed inside the depth-50 clone; running git fetch --unshallow and retrying.`,
|
|
272
|
+
"warn"
|
|
273
|
+
);
|
|
274
|
+
await exec(
|
|
275
|
+
`cd ${sq(env.projectDir)} && ${GIT_ENV} git ${CRED} fetch --progress --unshallow && git ${CRED} -c advice.detachedHead=false checkout ${sq(config.commitSha)}`
|
|
276
|
+
);
|
|
277
|
+
}
|
|
278
|
+
} else {
|
|
279
|
+
await exec(
|
|
280
|
+
`${GIT_ENV} git ${CRED} clone --progress --depth 1 --branch ${sq(config.branch)} ${sq(cloneUrl)} ${sq(env.projectDir)}`
|
|
281
|
+
);
|
|
282
|
+
}
|
|
283
|
+
} finally {
|
|
284
|
+
await sshMaterial?.cleanup();
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
);
|
|
288
|
+
}
|
|
289
|
+
const envPrefix = env.hasNativeEnv ? "" : Object.entries(config.envVars).filter(([k]) => /^[A-Za-z_][A-Za-z0-9_]*$/.test(k)).map(([k, v]) => `export ${k}=${sq(v)}`).join(" && ");
|
|
290
|
+
const binPathExport = nodeBinPathExport(config.packageManager, [buildDir, env.projectDir]);
|
|
291
|
+
const inDir = (cmd) => {
|
|
292
|
+
const full = `cd ${sq(buildDir)} && ${cmd}`;
|
|
293
|
+
const prefix = [envPrefix, binPathExport].filter(Boolean).join(" && ");
|
|
294
|
+
return prefix ? `${prefix} && ${full}` : full;
|
|
295
|
+
};
|
|
296
|
+
const pmEnsure = packageManagerEnsureCommand(config.packageManager);
|
|
297
|
+
currentStep = "install";
|
|
298
|
+
if (config.installCommand) {
|
|
299
|
+
const installCmd = pmEnsure ? `${pmEnsure} && ${config.installCommand}` : config.installCommand;
|
|
300
|
+
await logger.runStep(
|
|
301
|
+
"install",
|
|
302
|
+
`Installing dependencies (${config.packageManager})`,
|
|
303
|
+
async () => {
|
|
304
|
+
await exec(inDir(installCmd));
|
|
305
|
+
}
|
|
306
|
+
);
|
|
307
|
+
} else {
|
|
308
|
+
logger.step("install", "skipped", "No install command configured");
|
|
309
|
+
}
|
|
310
|
+
if (config.buildCommand) {
|
|
311
|
+
currentStep = "build";
|
|
312
|
+
const buildCmd = pmEnsure && !config.installCommand ? `${pmEnsure} && ${config.buildCommand}` : config.buildCommand;
|
|
313
|
+
await logger.runStep("build", `Building (${config.buildCommand})`, async () => {
|
|
314
|
+
await exec(inDir(buildCmd));
|
|
315
|
+
});
|
|
316
|
+
} else {
|
|
317
|
+
logger.step("build", "skipped", "No build command configured");
|
|
318
|
+
}
|
|
319
|
+
const durationMs = Date.now() - startTime;
|
|
320
|
+
return { status: "deploying", durationMs };
|
|
321
|
+
} catch (err) {
|
|
322
|
+
const durationMs = Date.now() - startTime;
|
|
323
|
+
if (err instanceof BuildCancelledError) {
|
|
324
|
+
return { status: "cancelled", durationMs, errorMessage: err.message };
|
|
325
|
+
}
|
|
326
|
+
const errorMessage = safeErrorMessage(err);
|
|
327
|
+
return { status: "failed", failedStep: currentStep, durationMs, errorMessage };
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
function resolveBuildDirectory(projectDir, rootDirectory) {
|
|
331
|
+
const normalized = rootDirectory?.trim().replace(/^\/+|\/+$/g, "");
|
|
332
|
+
if (!normalized || normalized === ".") {
|
|
333
|
+
return projectDir;
|
|
334
|
+
}
|
|
335
|
+
return `${projectDir}/${normalized}`;
|
|
336
|
+
}
|
|
337
|
+
function parseLogLevel(message) {
|
|
338
|
+
if (/\b(error|fatal|panic)\b/i.test(message)) return "error";
|
|
339
|
+
if (/\bwarn(ing)?\b/i.test(message)) return "warn";
|
|
340
|
+
return "info";
|
|
341
|
+
}
|
|
342
|
+
function detectBuildKillHint(output) {
|
|
343
|
+
if (!output) return null;
|
|
344
|
+
const tail = output.slice(-4096);
|
|
345
|
+
if (/\bsigkill\b|\bKilled\b|out of memory|JavaScript heap out of memory|Allocation failed/i.test(tail)) {
|
|
346
|
+
return "Build process was killed - typically because the target ran out of memory during the build. Increase RAM on the target, add swap, or build locally and ship the dist.";
|
|
347
|
+
}
|
|
348
|
+
return null;
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
// ../../packages/adapters/src/runtime/docker-paths.ts
|
|
352
|
+
import { access, readdir } from "fs/promises";
|
|
353
|
+
import { isAbsolute, join, relative, resolve, sep } from "path";
|
|
354
|
+
function toPosixPath(value) {
|
|
355
|
+
return value.split(sep).filter(Boolean).join("/");
|
|
356
|
+
}
|
|
357
|
+
function toSafeSegments(value, label) {
|
|
358
|
+
const segments = value.split(/[\\/]/).filter((segment) => segment && segment !== ".");
|
|
359
|
+
if (segments.includes("..")) {
|
|
360
|
+
throw new Error(
|
|
361
|
+
`Invalid ${label} ${JSON.stringify(value)}: path segments must not contain "..".`
|
|
362
|
+
);
|
|
363
|
+
}
|
|
364
|
+
return segments;
|
|
365
|
+
}
|
|
366
|
+
function normalizeDockerRelativePath(value) {
|
|
367
|
+
const normalized = value?.trim().replace(/^\.\//, "").replace(/^\/+|\/+$/g, "");
|
|
368
|
+
if (!normalized || normalized === ".") {
|
|
369
|
+
return "";
|
|
370
|
+
}
|
|
371
|
+
return toSafeSegments(normalized, "path").join("/");
|
|
372
|
+
}
|
|
373
|
+
function resolveWithinDirectory(baseDir, relativePath) {
|
|
374
|
+
const base = resolve(baseDir);
|
|
375
|
+
const target = resolve(base, ...toSafeSegments(relativePath?.trim() ?? "", "path"));
|
|
376
|
+
const escape = relative(base, target);
|
|
377
|
+
if (escape === ".." || escape.startsWith(`..${sep}`) || isAbsolute(escape)) {
|
|
378
|
+
throw new Error(`Resolved path ${JSON.stringify(target)} escapes ${JSON.stringify(base)}.`);
|
|
379
|
+
}
|
|
380
|
+
return target;
|
|
381
|
+
}
|
|
382
|
+
function normalizeDockerRootDirectory(rootDirectory, localPath) {
|
|
383
|
+
let normalized = rootDirectory?.trim() ?? "";
|
|
384
|
+
if (!normalized) {
|
|
385
|
+
return "";
|
|
386
|
+
}
|
|
387
|
+
if (localPath && isAbsolute(normalized)) {
|
|
388
|
+
const relativePath = relative(localPath, normalized);
|
|
389
|
+
if (!relativePath || relativePath === ".") {
|
|
390
|
+
return "";
|
|
391
|
+
}
|
|
392
|
+
if (!relativePath.startsWith("..") && !isAbsolute(relativePath)) {
|
|
393
|
+
normalized = relativePath;
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
normalized = normalized.replace(/^\.\//, "").replace(/^\/+|\/+$/g, "");
|
|
397
|
+
if (!normalized || normalized === ".") {
|
|
398
|
+
return "";
|
|
399
|
+
}
|
|
400
|
+
return toSafeSegments(normalized, "rootDirectory").join("/");
|
|
401
|
+
}
|
|
402
|
+
function resolveExplicitDockerfileCandidate(rootDirectory, dockerfilePath) {
|
|
403
|
+
const normalizedRootDirectory = normalizeDockerRelativePath(rootDirectory);
|
|
404
|
+
const normalizedDockerfilePath = normalizeDockerRelativePath(dockerfilePath);
|
|
405
|
+
if (!normalizedDockerfilePath) {
|
|
406
|
+
return "";
|
|
407
|
+
}
|
|
408
|
+
if (!normalizedRootDirectory) {
|
|
409
|
+
return normalizedDockerfilePath;
|
|
410
|
+
}
|
|
411
|
+
if (normalizedDockerfilePath.startsWith(`${normalizedRootDirectory}/`)) {
|
|
412
|
+
return normalizedDockerfilePath;
|
|
413
|
+
}
|
|
414
|
+
return `${normalizedRootDirectory}/${normalizedDockerfilePath}`;
|
|
415
|
+
}
|
|
416
|
+
function resolveDockerfileCandidates(rootDirectory, explicitDockerfilePath) {
|
|
417
|
+
const normalizedRootDirectory = normalizeDockerRelativePath(rootDirectory);
|
|
418
|
+
return [
|
|
419
|
+
resolveExplicitDockerfileCandidate(rootDirectory, explicitDockerfilePath),
|
|
420
|
+
normalizedRootDirectory ? `${normalizedRootDirectory}/Dockerfile` : "Dockerfile",
|
|
421
|
+
"Dockerfile"
|
|
422
|
+
].filter(
|
|
423
|
+
(candidate, index, values) => Boolean(candidate) && values.indexOf(candidate) === index
|
|
424
|
+
);
|
|
425
|
+
}
|
|
426
|
+
function dockerBuildContextDirectory(config) {
|
|
427
|
+
return normalizeDockerRelativePath(config.buildContextDirectory);
|
|
428
|
+
}
|
|
429
|
+
function resolveContextDockerfileCandidates(contextDirectory, explicitDockerfilePath) {
|
|
430
|
+
const context = normalizeDockerRelativePath(contextDirectory);
|
|
431
|
+
const explicit = normalizeDockerRelativePath(explicitDockerfilePath);
|
|
432
|
+
const candidates = [];
|
|
433
|
+
if (explicit) {
|
|
434
|
+
candidates.push(explicit);
|
|
435
|
+
if (context && explicit.startsWith(`${context}/`)) {
|
|
436
|
+
candidates.push(explicit.slice(context.length + 1));
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
candidates.push("Dockerfile");
|
|
440
|
+
return candidates.filter(
|
|
441
|
+
(candidate, index, values) => Boolean(candidate) && values.indexOf(candidate) === index
|
|
442
|
+
);
|
|
443
|
+
}
|
|
444
|
+
var ROOT_MANIFESTS = [
|
|
445
|
+
"package.json",
|
|
446
|
+
"pyproject.toml",
|
|
447
|
+
"requirements.txt",
|
|
448
|
+
"Pipfile",
|
|
449
|
+
"go.mod",
|
|
450
|
+
"Cargo.toml",
|
|
451
|
+
"Gemfile",
|
|
452
|
+
"composer.json",
|
|
453
|
+
"pom.xml",
|
|
454
|
+
"build.gradle",
|
|
455
|
+
"build.gradle.kts",
|
|
456
|
+
"mix.exs",
|
|
457
|
+
"Dockerfile"
|
|
458
|
+
];
|
|
459
|
+
var IGNORED_DIRS = /* @__PURE__ */ new Set([
|
|
460
|
+
"node_modules",
|
|
461
|
+
".git",
|
|
462
|
+
".next",
|
|
463
|
+
".nuxt",
|
|
464
|
+
".output",
|
|
465
|
+
".turbo",
|
|
466
|
+
".venv",
|
|
467
|
+
"__pycache__",
|
|
468
|
+
"build",
|
|
469
|
+
"dist",
|
|
470
|
+
"target",
|
|
471
|
+
"vendor"
|
|
472
|
+
]);
|
|
473
|
+
async function pathExists(path) {
|
|
474
|
+
return access(path).then(() => true).catch(() => false);
|
|
475
|
+
}
|
|
476
|
+
async function manifestCount(dir) {
|
|
477
|
+
let count = 0;
|
|
478
|
+
for (const file of ROOT_MANIFESTS) {
|
|
479
|
+
if (await pathExists(join(dir, file))) {
|
|
480
|
+
count += 1;
|
|
481
|
+
}
|
|
482
|
+
}
|
|
483
|
+
return count;
|
|
484
|
+
}
|
|
485
|
+
function pathSegments(path) {
|
|
486
|
+
return path.toLowerCase().split("/").filter(Boolean);
|
|
487
|
+
}
|
|
488
|
+
function scorePathHints(relativePath) {
|
|
489
|
+
const segments = pathSegments(relativePath);
|
|
490
|
+
let score = 0;
|
|
491
|
+
for (const segment of segments) {
|
|
492
|
+
if (["app", "apps", "site", "sites", "web", "www", "service", "services"].includes(segment)) {
|
|
493
|
+
score += 12;
|
|
494
|
+
}
|
|
495
|
+
if (["docs", "example", "examples", "test", "tests", "storybook"].includes(segment)) {
|
|
496
|
+
score -= 18;
|
|
497
|
+
}
|
|
498
|
+
}
|
|
499
|
+
return score;
|
|
500
|
+
}
|
|
501
|
+
async function scoreCandidate(dir, relativePath) {
|
|
502
|
+
let score = scorePathHints(relativePath);
|
|
503
|
+
score += await manifestCount(dir) * 20;
|
|
504
|
+
if (relativePath) {
|
|
505
|
+
score += 6;
|
|
506
|
+
}
|
|
507
|
+
return score;
|
|
508
|
+
}
|
|
509
|
+
async function collectCandidates(rootDir, currentDir, depth, candidates) {
|
|
510
|
+
const relativePath = toPosixPath(relative(rootDir, currentDir));
|
|
511
|
+
if (await manifestCount(currentDir) > 0) {
|
|
512
|
+
candidates.push({
|
|
513
|
+
path: relativePath,
|
|
514
|
+
score: await scoreCandidate(currentDir, relativePath)
|
|
515
|
+
});
|
|
516
|
+
}
|
|
517
|
+
if (depth >= 6) {
|
|
518
|
+
return;
|
|
519
|
+
}
|
|
520
|
+
const entries = await readdir(currentDir, { withFileTypes: true }).catch(() => []);
|
|
521
|
+
for (const entry of entries) {
|
|
522
|
+
if (!entry.isDirectory() || IGNORED_DIRS.has(entry.name)) {
|
|
523
|
+
continue;
|
|
524
|
+
}
|
|
525
|
+
await collectCandidates(rootDir, join(currentDir, entry.name), depth + 1, candidates);
|
|
526
|
+
}
|
|
527
|
+
}
|
|
528
|
+
async function resolveDockerRootDirectory(contextDir, rootDirectory, localPath) {
|
|
529
|
+
const hasExplicitRootDirectory = typeof rootDirectory === "string";
|
|
530
|
+
const normalized = normalizeDockerRootDirectory(rootDirectory, localPath);
|
|
531
|
+
if (hasExplicitRootDirectory) {
|
|
532
|
+
return normalized;
|
|
533
|
+
}
|
|
534
|
+
if (normalized) {
|
|
535
|
+
return normalized;
|
|
536
|
+
}
|
|
537
|
+
const candidates = [];
|
|
538
|
+
await collectCandidates(contextDir, contextDir, 0, candidates);
|
|
539
|
+
if (candidates.length === 0) {
|
|
540
|
+
return "";
|
|
541
|
+
}
|
|
542
|
+
candidates.sort((left, right) => right.score - left.score);
|
|
543
|
+
const bestNonRoot = candidates.find((candidate) => candidate.path);
|
|
544
|
+
if (bestNonRoot && bestNonRoot.score > 0) {
|
|
545
|
+
return bestNonRoot.path;
|
|
546
|
+
}
|
|
547
|
+
return candidates[0]?.path ?? "";
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
// ../../packages/adapters/src/runtime/docker-build-plan.ts
|
|
551
|
+
var DOCKER_BUILD_EVENT_PREFIX = "[openship-build]";
|
|
552
|
+
var INLINE_BUILD_ENV_EXCLUDES = /* @__PURE__ */ new Set(["FORCE_COLOR", "TERM", "PATH"]);
|
|
553
|
+
function formatDockerBuildEvent(step, status) {
|
|
554
|
+
return `${DOCKER_BUILD_EVENT_PREFIX} step=${step} status=${status}`;
|
|
555
|
+
}
|
|
556
|
+
function normalizeRelativePath(value) {
|
|
557
|
+
const normalized = value?.trim().replace(/^\.\//, "").replace(/^\/+|\/+$/g, "");
|
|
558
|
+
if (!normalized || normalized === ".") {
|
|
559
|
+
return "";
|
|
560
|
+
}
|
|
561
|
+
return normalized;
|
|
562
|
+
}
|
|
563
|
+
function builderSourceDir(rootDirectory) {
|
|
564
|
+
const normalized = normalizeDockerRootDirectory(rootDirectory);
|
|
565
|
+
return normalized ? `/workspace/${normalized}` : "/workspace";
|
|
566
|
+
}
|
|
567
|
+
function buildEnvPrefix(envVars) {
|
|
568
|
+
const assignments = Object.entries(envVars).filter(([key]) => !INLINE_BUILD_ENV_EXCLUDES.has(key)).filter(([key]) => /^[A-Za-z_][A-Za-z0-9_]*$/.test(key)).map(([key, value]) => `${key}=${sq(value)}`);
|
|
569
|
+
if (!envVars.NO_COLOR) {
|
|
570
|
+
assignments.push("NO_COLOR='1'");
|
|
571
|
+
}
|
|
572
|
+
if (assignments.length === 0) {
|
|
573
|
+
return "";
|
|
574
|
+
}
|
|
575
|
+
return `export ${assignments.join(" ")} && `;
|
|
576
|
+
}
|
|
577
|
+
function buildRunCommand(command, envPrefix) {
|
|
578
|
+
return `${envPrefix}${command}`;
|
|
579
|
+
}
|
|
580
|
+
function runtimeCopyDirectives(config, sourceDir) {
|
|
581
|
+
if (config.productionPaths && config.productionPaths.length > 0) {
|
|
582
|
+
return config.productionPaths.map((path) => {
|
|
583
|
+
const normalized = normalizeRelativePath(path);
|
|
584
|
+
const source = normalized ? `${sourceDir}/${normalized}` : sourceDir;
|
|
585
|
+
const target = normalized ? `/app/${normalized}` : "/app";
|
|
586
|
+
return `COPY --from=builder ${source} ${target}`;
|
|
587
|
+
});
|
|
588
|
+
}
|
|
589
|
+
return [`COPY --from=builder ${sourceDir} /app`];
|
|
590
|
+
}
|
|
591
|
+
function needsMultiStage(config) {
|
|
592
|
+
return config.buildImage !== config.runtimeImage;
|
|
593
|
+
}
|
|
594
|
+
function nodeBinPathEnvLine(packageManager, roots) {
|
|
595
|
+
const dirs = nodeBinDirs(packageManager, roots);
|
|
596
|
+
return dirs.length > 0 ? `ENV PATH="${dirs.join(":")}:$PATH"` : null;
|
|
597
|
+
}
|
|
598
|
+
function workspacePrepareRunLine(config, envPrefix, workspacePrepare) {
|
|
599
|
+
const pmEnsure = packageManagerEnsureCommand(config.packageManager);
|
|
600
|
+
const body = pmEnsure ? `${pmEnsure} && ${workspacePrepare}` : workspacePrepare;
|
|
601
|
+
return `RUN ${envPrefix}${body}`;
|
|
602
|
+
}
|
|
603
|
+
function stepsRunLine(config, envPrefix, opts) {
|
|
604
|
+
const install = opts.install ? config.installCommand : "";
|
|
605
|
+
const build = opts.build ? config.buildCommand : "";
|
|
606
|
+
const steps = [];
|
|
607
|
+
const pmEnsure = packageManagerEnsureCommand(opts.packageManager ?? config.packageManager);
|
|
608
|
+
if (pmEnsure && (install || build)) {
|
|
609
|
+
steps.push(pmEnsure);
|
|
610
|
+
}
|
|
611
|
+
if (install) {
|
|
612
|
+
steps.push(`printf '${formatDockerBuildEvent("install", "running")}\\n'`);
|
|
613
|
+
steps.push(buildRunCommand(install, envPrefix));
|
|
614
|
+
steps.push(`printf '${formatDockerBuildEvent("install", "completed")}\\n'`);
|
|
615
|
+
}
|
|
616
|
+
if (build) {
|
|
617
|
+
steps.push(`printf '${formatDockerBuildEvent("build", "running")}\\n'`);
|
|
618
|
+
steps.push(buildRunCommand(build, envPrefix));
|
|
619
|
+
steps.push(`printf '${formatDockerBuildEvent("build", "completed")}\\n'`);
|
|
620
|
+
}
|
|
621
|
+
return steps.length > 0 ? `RUN ${steps.join(" && ")}` : null;
|
|
622
|
+
}
|
|
623
|
+
function installBuildRunLine(config, envPrefix) {
|
|
624
|
+
return stepsRunLine(config, envPrefix, { install: true, build: true });
|
|
625
|
+
}
|
|
626
|
+
function installRunLine(config, envPrefix) {
|
|
627
|
+
return stepsRunLine(config, envPrefix, { install: true });
|
|
628
|
+
}
|
|
629
|
+
var JS_PACKAGE_MANAGERS = ["pnpm", "yarn", "bun", "npm"];
|
|
630
|
+
function assetStagePackageManager(config) {
|
|
631
|
+
const command = config.buildCommand?.trim() ?? "";
|
|
632
|
+
return JS_PACKAGE_MANAGERS.find((pm) => new RegExp(`(^|\\s|&&\\s*)${pm}\\s`).test(command)) ?? "npm";
|
|
633
|
+
}
|
|
634
|
+
function buildOnlyRunLine(config, envPrefix) {
|
|
635
|
+
const command = config.buildCommand?.trim() ?? "";
|
|
636
|
+
const packageManager = JS_PACKAGE_MANAGERS.find(
|
|
637
|
+
(pm) => new RegExp(`(^|\\s|&&\\s*)${pm}\\s`).test(command)
|
|
638
|
+
);
|
|
639
|
+
return stepsRunLine(config, envPrefix, { build: true, packageManager });
|
|
640
|
+
}
|
|
641
|
+
function isPhpRuntime(config) {
|
|
642
|
+
return /^php:/.test(config.runtimeImage) || /frankenphp/i.test(config.runtimeImage);
|
|
643
|
+
}
|
|
644
|
+
var PHP_EXTENSIONS = [
|
|
645
|
+
"pdo_mysql",
|
|
646
|
+
"pdo_pgsql",
|
|
647
|
+
"redis",
|
|
648
|
+
"pcntl",
|
|
649
|
+
"bcmath",
|
|
650
|
+
"intl",
|
|
651
|
+
"zip",
|
|
652
|
+
"gd",
|
|
653
|
+
"opcache"
|
|
654
|
+
];
|
|
655
|
+
var PHP_EXT_INSTALLER_IMAGE = "mlocati/php-extension-installer:2";
|
|
656
|
+
var PHP_ASSET_BUILD_IMAGE = "node:22-bookworm-slim";
|
|
657
|
+
var PHP_EXT_INSTALL_LINE = `RUN install-php-extensions ${PHP_EXTENSIONS.join(" ")} > /dev/null`;
|
|
658
|
+
function generatePhpDockerfile(config) {
|
|
659
|
+
const sourceDir = builderSourceDir(
|
|
660
|
+
normalizeDockerRootDirectory(config.rootDirectory, config.localPath)
|
|
661
|
+
);
|
|
662
|
+
const envPrefix = buildEnvPrefix(config.envVars);
|
|
663
|
+
const installLine = installRunLine(config, envPrefix);
|
|
664
|
+
const assetBuildLine = buildOnlyRunLine(config, envPrefix);
|
|
665
|
+
const lines = [
|
|
666
|
+
`FROM ${config.buildImage} AS builder`,
|
|
667
|
+
// php:*-cli ships no Composer — pull the binary from the official image.
|
|
668
|
+
`COPY --from=composer:2 /usr/bin/composer /usr/bin/composer`,
|
|
669
|
+
`COPY --from=${PHP_EXT_INSTALLER_IMAGE} /usr/bin/install-php-extensions /usr/bin/install-php-extensions`,
|
|
670
|
+
PHP_EXT_INSTALL_LINE,
|
|
671
|
+
`WORKDIR /workspace`,
|
|
672
|
+
`COPY . /workspace`,
|
|
673
|
+
`WORKDIR ${sourceDir}`
|
|
674
|
+
];
|
|
675
|
+
if (installLine) lines.push(installLine);
|
|
676
|
+
if (assetBuildLine) {
|
|
677
|
+
const assetBinPath = nodeBinPathEnvLine(assetStagePackageManager(config), [sourceDir, "/workspace"]);
|
|
678
|
+
lines.push(
|
|
679
|
+
`FROM ${PHP_ASSET_BUILD_IMAGE} AS assets`,
|
|
680
|
+
...assetBinPath ? [assetBinPath] : [],
|
|
681
|
+
`WORKDIR /workspace`,
|
|
682
|
+
`COPY . /workspace`,
|
|
683
|
+
`WORKDIR ${sourceDir}`,
|
|
684
|
+
assetBuildLine
|
|
685
|
+
);
|
|
686
|
+
}
|
|
687
|
+
const docroot = normalizeRelativePath(config.outputDirectory) || "public";
|
|
688
|
+
lines.push(
|
|
689
|
+
`FROM ${config.runtimeImage} AS runtime`,
|
|
690
|
+
PHP_EXT_INSTALL_LINE,
|
|
691
|
+
// --chown on the COPY instead of a trailing `chown -R`: the recursive form
|
|
692
|
+
// rewrites every file into a second layer, doubling the app's size on disk.
|
|
693
|
+
// Ownership has to be settled HERE, before anything mounts: Docker seeds a
|
|
694
|
+
// fresh named volume from the image directory it covers, ownership included,
|
|
695
|
+
// so `/app/storage` is only writable by the runtime user if it was already
|
|
696
|
+
// owned by them in the image (see @repo/core volumes.ts).
|
|
697
|
+
`COPY --from=builder --chown=www-data:www-data ${sourceDir} /app`
|
|
698
|
+
);
|
|
699
|
+
if (assetBuildLine) {
|
|
700
|
+
lines.push(
|
|
701
|
+
`COPY --from=assets --chown=www-data:www-data ${sourceDir}/${docroot} /app/${docroot}`
|
|
702
|
+
);
|
|
703
|
+
}
|
|
704
|
+
lines.push(
|
|
705
|
+
`WORKDIR /app`,
|
|
706
|
+
// FrankenPHP's bundled Caddyfile reads the docroot from SERVER_ROOT (default
|
|
707
|
+
// `public/`, resolved against WORKDIR). Setting it keeps the project's
|
|
708
|
+
// outputDirectory authoritative for BOTH what gets served and where the asset
|
|
709
|
+
// stage's output lands — otherwise a project that renamed its docroot builds
|
|
710
|
+
// assets into one directory and serves another.
|
|
711
|
+
`ENV SERVER_ROOT=${docroot}/`,
|
|
712
|
+
// Caddy's config + data dirs are root-owned in the base image, and it writes
|
|
713
|
+
// to both on startup — without this the non-root switch below fails at boot.
|
|
714
|
+
`RUN mkdir -p /data/caddy /config/caddy && chown -R www-data:www-data /data/caddy /config/caddy`,
|
|
715
|
+
// The frankenphp binary carries cap_net_bind_service, so dropping root here
|
|
716
|
+
// still leaves any port bindable.
|
|
717
|
+
`USER www-data`,
|
|
718
|
+
`EXPOSE ${config.port}`
|
|
719
|
+
);
|
|
720
|
+
if (config.startCommand) {
|
|
721
|
+
lines.push(`CMD ["sh", "-c", ${JSON.stringify(config.startCommand)}]`);
|
|
722
|
+
}
|
|
723
|
+
return lines.join("\n");
|
|
724
|
+
}
|
|
725
|
+
function staticNginxTemplateLines(port) {
|
|
726
|
+
return [
|
|
727
|
+
"server {",
|
|
728
|
+
` listen ${port} default_server;`,
|
|
729
|
+
" root /usr/share/nginx/html;",
|
|
730
|
+
" index index.html;",
|
|
731
|
+
" location / { try_files $uri $uri/ /index.html; }",
|
|
732
|
+
"}"
|
|
733
|
+
];
|
|
734
|
+
}
|
|
735
|
+
function builderContextRoot(config) {
|
|
736
|
+
return builderSourceDir(normalizeDockerRootDirectory(config.rootDirectory, config.localPath));
|
|
737
|
+
}
|
|
738
|
+
function isExcludedDocRootEntry(name) {
|
|
739
|
+
return name === ".dockerignore" || name === ".git" || name === "Dockerfile" || name.startsWith("Dockerfile.");
|
|
740
|
+
}
|
|
741
|
+
function staticBuilderOutputPath(config) {
|
|
742
|
+
const sourceDir = builderSourceDir(
|
|
743
|
+
normalizeDockerRootDirectory(config.rootDirectory, config.localPath)
|
|
744
|
+
);
|
|
745
|
+
const output = normalizeRelativePath(config.outputDirectory);
|
|
746
|
+
return output ? `${sourceDir}/${output}` : sourceDir;
|
|
747
|
+
}
|
|
748
|
+
function generateStaticDockerfile(config) {
|
|
749
|
+
const sourceDir = builderSourceDir(
|
|
750
|
+
normalizeDockerRootDirectory(config.rootDirectory, config.localPath)
|
|
751
|
+
);
|
|
752
|
+
const envPrefix = buildEnvPrefix(config.envVars);
|
|
753
|
+
const workspacePrepare = config.workspacePrepareCommand?.trim();
|
|
754
|
+
const outputPath = staticBuilderOutputPath(config);
|
|
755
|
+
const nginxTemplate = staticNginxTemplateLines(config.port).map((line) => `'${line}'`).join(" ");
|
|
756
|
+
const lines = [`FROM ${config.buildImage} AS builder`];
|
|
757
|
+
const buildBinPath = nodeBinPathEnvLine(config.packageManager, [sourceDir, "/workspace"]);
|
|
758
|
+
if (buildBinPath) lines.push(buildBinPath);
|
|
759
|
+
lines.push(`WORKDIR /workspace`, `COPY . /workspace`);
|
|
760
|
+
if (workspacePrepare) {
|
|
761
|
+
lines.push(workspacePrepareRunLine(config, envPrefix, workspacePrepare));
|
|
762
|
+
}
|
|
763
|
+
lines.push(`WORKDIR ${sourceDir}`);
|
|
764
|
+
const stepsLine = installBuildRunLine(config, envPrefix);
|
|
765
|
+
if (stepsLine) lines.push(stepsLine);
|
|
766
|
+
if (config.staticExtractOnly) return lines.join("\n");
|
|
767
|
+
lines.push(
|
|
768
|
+
`FROM nginx:alpine AS runtime`,
|
|
769
|
+
`RUN rm -f /etc/nginx/conf.d/default.conf`,
|
|
770
|
+
`COPY --from=builder ${outputPath} /usr/share/nginx/html`,
|
|
771
|
+
`RUN printf '%s\\n' ${nginxTemplate} > /etc/nginx/conf.d/app.conf`,
|
|
772
|
+
`EXPOSE ${config.port}`,
|
|
773
|
+
`CMD ["nginx", "-g", "daemon off;"]`
|
|
774
|
+
);
|
|
775
|
+
return lines.join("\n");
|
|
776
|
+
}
|
|
777
|
+
function generateDockerfile(config) {
|
|
778
|
+
if (config.isStatic) {
|
|
779
|
+
return generateStaticDockerfile(config);
|
|
780
|
+
}
|
|
781
|
+
if (isPhpRuntime(config) && needsMultiStage(config)) {
|
|
782
|
+
return generatePhpDockerfile(config);
|
|
783
|
+
}
|
|
784
|
+
const sourceDir = builderSourceDir(
|
|
785
|
+
normalizeDockerRootDirectory(config.rootDirectory, config.localPath)
|
|
786
|
+
);
|
|
787
|
+
const multiStage = needsMultiStage(config);
|
|
788
|
+
const envPrefix = buildEnvPrefix(config.envVars);
|
|
789
|
+
const workspacePrepare = config.workspacePrepareCommand?.trim();
|
|
790
|
+
const lines = [
|
|
791
|
+
multiStage ? `FROM ${config.buildImage} AS builder` : `FROM ${config.runtimeImage}`
|
|
792
|
+
];
|
|
793
|
+
const buildBinPath = nodeBinPathEnvLine(config.packageManager, [sourceDir, "/workspace"]);
|
|
794
|
+
if (buildBinPath) lines.push(buildBinPath);
|
|
795
|
+
lines.push(`WORKDIR /workspace`, `COPY . /workspace`);
|
|
796
|
+
if (workspacePrepare) {
|
|
797
|
+
lines.push(workspacePrepareRunLine(config, envPrefix, workspacePrepare));
|
|
798
|
+
}
|
|
799
|
+
lines.push(`WORKDIR ${sourceDir}`);
|
|
800
|
+
const stepsLine = installBuildRunLine(config, envPrefix);
|
|
801
|
+
if (stepsLine) {
|
|
802
|
+
lines.push(stepsLine);
|
|
803
|
+
}
|
|
804
|
+
if (multiStage) {
|
|
805
|
+
lines.push(`FROM ${config.runtimeImage} AS runtime`);
|
|
806
|
+
lines.push(...runtimeCopyDirectives(config, sourceDir));
|
|
807
|
+
lines.push(`WORKDIR /app`);
|
|
808
|
+
const runtimeBinPath = nodeBinPathEnvLine(config.packageManager, ["/app"]);
|
|
809
|
+
if (runtimeBinPath) lines.push(runtimeBinPath);
|
|
810
|
+
}
|
|
811
|
+
lines.push(`EXPOSE ${config.port}`);
|
|
812
|
+
if (config.startCommand) {
|
|
813
|
+
lines.push(`CMD ["sh", "-c", ${JSON.stringify(config.startCommand)}]`);
|
|
814
|
+
}
|
|
815
|
+
return lines.join("\n");
|
|
816
|
+
}
|
|
817
|
+
|
|
818
|
+
// ../../packages/adapters/src/runtime/transfer.ts
|
|
819
|
+
import { execFile } from "child_process";
|
|
820
|
+
var TAR_MAX_BUFFER = 500 * 1024 * 1024;
|
|
821
|
+
async function transferLocalDirectory(localPath, target, logger, options) {
|
|
822
|
+
logger.log(`Transferring ${localPath} \u2192 ${target.path}...
|
|
823
|
+
`);
|
|
824
|
+
if (target.kind === "executor") {
|
|
825
|
+
await target.executor.transferIn(localPath, target.path, logger.callback, {
|
|
826
|
+
excludes: options?.excludes,
|
|
827
|
+
includes: options?.includes,
|
|
828
|
+
alsoInclude: options?.alsoInclude
|
|
829
|
+
});
|
|
830
|
+
await verifyExecutorTransfer(target.executor, target.path, logger);
|
|
831
|
+
return;
|
|
832
|
+
}
|
|
833
|
+
const tarBuffer = await createTarball(localPath, options);
|
|
834
|
+
const result = await target.runtime.transfer.upload({
|
|
835
|
+
body: tarBuffer,
|
|
836
|
+
dest: target.path
|
|
837
|
+
});
|
|
838
|
+
if (!result.files_extracted || result.files_extracted === 0) {
|
|
839
|
+
throw new Error("Transfer produced 0 files - upload may have failed silently");
|
|
840
|
+
}
|
|
841
|
+
logger.log(`Uploaded ${result.files_extracted} files.
|
|
842
|
+
`);
|
|
843
|
+
}
|
|
844
|
+
async function verifyExecutorTransfer(executor, targetPath, logger) {
|
|
845
|
+
try {
|
|
846
|
+
const countOutput = await executor.exec(
|
|
847
|
+
`find ${sq(targetPath)} -mindepth 1 -maxdepth 1 | head -5 | wc -l`
|
|
848
|
+
);
|
|
849
|
+
const fileCount = parseInt(countOutput.trim(), 10);
|
|
850
|
+
if (fileCount === 0) {
|
|
851
|
+
throw new Error(
|
|
852
|
+
`Transfer target ${targetPath} is empty - files were not copied`
|
|
853
|
+
);
|
|
854
|
+
}
|
|
855
|
+
logger.log(`Transfer verified (${fileCount}+ entries in target).
|
|
856
|
+
`);
|
|
857
|
+
} catch (err) {
|
|
858
|
+
if (err instanceof Error && err.message.includes("is empty")) throw err;
|
|
859
|
+
throw new Error(
|
|
860
|
+
`Transfer verification failed: ${safeErrorMessage(err)}`
|
|
861
|
+
);
|
|
862
|
+
}
|
|
863
|
+
}
|
|
864
|
+
async function createTarball(localPath, options) {
|
|
865
|
+
const { args, cleanup } = await prepareSourceTarArgs(localPath, {
|
|
866
|
+
excludes: options?.excludes ?? [...TRANSFER_EXCLUDES],
|
|
867
|
+
includes: options?.includes,
|
|
868
|
+
alsoInclude: options?.alsoInclude
|
|
869
|
+
});
|
|
870
|
+
try {
|
|
871
|
+
return await new Promise((resolve2, reject) => {
|
|
872
|
+
execFile(
|
|
873
|
+
"tar",
|
|
874
|
+
args,
|
|
875
|
+
{
|
|
876
|
+
encoding: "buffer",
|
|
877
|
+
maxBuffer: TAR_MAX_BUFFER,
|
|
878
|
+
env: getTarCreateEnv()
|
|
879
|
+
},
|
|
880
|
+
(err, stdout, stderr) => {
|
|
881
|
+
if (err) {
|
|
882
|
+
const stderrText = Buffer.isBuffer(stderr) ? stderr.toString().trim() : String(stderr ?? "").trim();
|
|
883
|
+
reject(new Error(stderrText || err.message));
|
|
884
|
+
return;
|
|
885
|
+
}
|
|
886
|
+
resolve2(Buffer.isBuffer(stdout) ? stdout : Buffer.from(stdout));
|
|
887
|
+
}
|
|
888
|
+
);
|
|
889
|
+
});
|
|
890
|
+
} finally {
|
|
891
|
+
await cleanup();
|
|
892
|
+
}
|
|
893
|
+
}
|
|
894
|
+
|
|
895
|
+
// ../../packages/adapters/src/runtime/runtime-env.ts
|
|
896
|
+
var RUNTIME_ENV_EXCLUDES = /* @__PURE__ */ new Set(["PATH"]);
|
|
897
|
+
function splitRuntimeEnv(envVars) {
|
|
898
|
+
const entries = [];
|
|
899
|
+
const dropped = [];
|
|
900
|
+
for (const [key, value] of Object.entries(envVars)) {
|
|
901
|
+
if (RUNTIME_ENV_EXCLUDES.has(key)) dropped.push(key);
|
|
902
|
+
else entries.push([key, value]);
|
|
903
|
+
}
|
|
904
|
+
return { entries, dropped };
|
|
905
|
+
}
|
|
906
|
+
function droppedRuntimeEnvMessage(dropped) {
|
|
907
|
+
if (dropped.length === 0) return "";
|
|
908
|
+
return `Ignoring ${dropped.join(", ")} from this project's environment: it would replace the runtime's own value and stop the start command from resolving. Remove it to silence this.
|
|
909
|
+
`;
|
|
910
|
+
}
|
|
911
|
+
|
|
912
|
+
export {
|
|
913
|
+
sq,
|
|
914
|
+
injectGitToken,
|
|
915
|
+
assembleGitClone,
|
|
916
|
+
materializeGitSsh,
|
|
917
|
+
shellGitSshWriter,
|
|
918
|
+
localGitSshWriter,
|
|
919
|
+
BuildLogger,
|
|
920
|
+
BuildCancelledError,
|
|
921
|
+
killProcessesUnderDir,
|
|
922
|
+
runBuildPipeline,
|
|
923
|
+
parseLogLevel,
|
|
924
|
+
detectBuildKillHint,
|
|
925
|
+
normalizeDockerRelativePath,
|
|
926
|
+
resolveWithinDirectory,
|
|
927
|
+
resolveDockerfileCandidates,
|
|
928
|
+
dockerBuildContextDirectory,
|
|
929
|
+
resolveContextDockerfileCandidates,
|
|
930
|
+
resolveDockerRootDirectory,
|
|
931
|
+
builderContextRoot,
|
|
932
|
+
isExcludedDocRootEntry,
|
|
933
|
+
staticBuilderOutputPath,
|
|
934
|
+
generateDockerfile,
|
|
935
|
+
transferLocalDirectory,
|
|
936
|
+
splitRuntimeEnv,
|
|
937
|
+
droppedRuntimeEnvMessage
|
|
938
|
+
};
|