@forgezero/agent 0.1.29 → 0.1.31
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/README.md +52 -4
- package/dist/agent-heartbeat.js +1 -1
- package/dist/cli/agent-install.d.ts +1 -1
- package/dist/definition.d.ts +19 -10
- package/dist/definition.js +77 -31
- package/dist/deploy-file.d.ts +40 -0
- package/dist/deploy-file.js +378 -0
- package/dist/deployment-pull.d.ts +3 -3
- package/dist/deployment.d.ts +9 -5
- package/dist/fz-agent.js +123 -58
- package/dist/fz.js +729 -19
- package/dist/index.d.ts +2 -2
- package/dist/project-context.d.ts +37 -0
- package/dist/project-context.js +266 -0
- package/dist/provision.d.ts +1 -1
- package/dist/provision.js +19 -4
- package/dist/software-helper.js +17 -2
- package/dist/software.d.ts +24 -2
- package/dist/software.js +20 -3
- package/dist/version.d.ts +1 -1
- package/package.json +12 -2
- package/schema/deploy-v2.json +70 -0
|
@@ -0,0 +1,378 @@
|
|
|
1
|
+
// src/software.ts
|
|
2
|
+
import { readFileSync } from "node:fs";
|
|
3
|
+
var BUN_INSTALLER_SHA256 = "bab8acfb046aac8c72407bdcce903957665d655d7acaa3e11c7c4616beae68dd";
|
|
4
|
+
var ARANGO_SHA256 = "b5a9197b4343f2ed554e1ebc1ef8e6529c7c39cde0035cdc311a4747a3355066";
|
|
5
|
+
var CLOUDFLARED_SHA256 = "9d71c677db00134c1bd4144b7783486b654ad281b1ea62b4972098d19f770f17";
|
|
6
|
+
var OS_CATALOG = [
|
|
7
|
+
{ id: "ubuntu", version: "26.04", architecture: "x64", status: "active" }
|
|
8
|
+
];
|
|
9
|
+
var SOFTWARE_CATALOG = [
|
|
10
|
+
{ id: "bun", version: "1.3.14", status: "active", os: "ubuntu", osVersion: "26.04", architecture: "x64", evidence: "reviewed-strategy-and-tests" },
|
|
11
|
+
{ id: "nginx", version: "ubuntu-26.04", status: "active", os: "ubuntu", osVersion: "26.04", architecture: "x64", evidence: "reviewed-strategy-and-tests" },
|
|
12
|
+
{ id: "arangodb", version: "3.11.14", status: "active", os: "ubuntu", osVersion: "26.04", architecture: "x64", evidence: "reviewed-strategy-and-tests" },
|
|
13
|
+
{ id: "cloudflared", version: "2026.7.3", status: "active", os: "ubuntu", osVersion: "26.04", architecture: "x64", evidence: "reviewed-strategy-and-tests" },
|
|
14
|
+
{ id: "ufw", version: "ubuntu-26.04", status: "active", os: "ubuntu", osVersion: "26.04", architecture: "x64", evidence: "reviewed-strategy-and-tests" }
|
|
15
|
+
];
|
|
16
|
+
var UBUNTU_2604_X64 = [
|
|
17
|
+
{
|
|
18
|
+
requirement: { id: "bun", version: "1.3.14" },
|
|
19
|
+
check: 'test "$(/usr/local/bin/bun --version 2>/dev/null)" = 1.3.14',
|
|
20
|
+
install: `tmp=$(mktemp -d); trap 'rm -rf "$tmp"' EXIT; ` + `curl -fsSL https://bun.sh/install -o "$tmp/install"; ` + `echo "${BUN_INSTALLER_SHA256} $tmp/install" | sha256sum -c -; ` + `BUN_INSTALL="$tmp/bun" BUN_VERSION=1.3.14 bash "$tmp/install" >/dev/null; ` + `install -d -m 0755 /usr/local/lib/forgezero/runtime; ` + `install -m 0755 "$tmp/bun/bin/bun" /usr/local/lib/forgezero/runtime/bun.next; ` + `mv -Tf /usr/local/lib/forgezero/runtime/bun.next /usr/local/lib/forgezero/runtime/bun; ` + `ln -sfn /usr/local/lib/forgezero/runtime/bun /usr/local/bin/bun`
|
|
21
|
+
},
|
|
22
|
+
{
|
|
23
|
+
requirement: { id: "nginx", version: "ubuntu-26.04" },
|
|
24
|
+
check: "command -v nginx >/dev/null && systemctl is-active --quiet nginx",
|
|
25
|
+
install: "DEBIAN_FRONTEND=noninteractive apt-get update -qq && apt-get install -y nginx && systemctl enable --now nginx"
|
|
26
|
+
},
|
|
27
|
+
{
|
|
28
|
+
requirement: { id: "arangodb", version: "3.11.14" },
|
|
29
|
+
check: `arangod --version 2>/dev/null | head -1 | grep -q '3.11.14'`,
|
|
30
|
+
install: `tmp=$(mktemp -d); trap 'rm -rf "$tmp"' EXIT; ` + `curl -fsSL 'https://download.arangodb.com/arangodb311/DEBIAN/amd64/arangodb3_3.11.14-1_amd64.deb' -o "$tmp/arangodb.deb"; ` + `echo "${ARANGO_SHA256} $tmp/arangodb.deb" | sha256sum -c -; ` + `DEBIAN_FRONTEND=noninteractive dpkg -i "$tmp/arangodb.deb" >/dev/null 2>&1 || ` + `DEBIAN_FRONTEND=noninteractive apt-get -y -f install`
|
|
31
|
+
},
|
|
32
|
+
{
|
|
33
|
+
requirement: { id: "cloudflared", version: "2026.7.3" },
|
|
34
|
+
check: `cloudflared --version 2>/dev/null | grep -q '2026.7.3'`,
|
|
35
|
+
install: `tmp=$(mktemp -d); trap 'rm -rf "$tmp"' EXIT; ` + `curl -fsSL 'https://github.com/cloudflare/cloudflared/releases/download/2026.7.3/cloudflared-linux-amd64' -o "$tmp/cloudflared"; ` + `echo "${CLOUDFLARED_SHA256} $tmp/cloudflared" | sha256sum -c -; ` + `install -m 0755 "$tmp/cloudflared" /usr/local/bin/cloudflared`
|
|
36
|
+
},
|
|
37
|
+
{
|
|
38
|
+
requirement: { id: "ufw", version: "ubuntu-26.04" },
|
|
39
|
+
check: "command -v ufw >/dev/null",
|
|
40
|
+
install: "DEBIAN_FRONTEND=noninteractive apt-get update -qq && apt-get install -y ufw"
|
|
41
|
+
}
|
|
42
|
+
];
|
|
43
|
+
function observeSoftwareHost(osRelease = readFileSync("/etc/os-release", "utf8"), architecture = process.arch) {
|
|
44
|
+
const values = Object.fromEntries(osRelease.split(`
|
|
45
|
+
`).flatMap((line) => {
|
|
46
|
+
const separator = line.indexOf("=");
|
|
47
|
+
return separator > 0 ? [[line.slice(0, separator), line.slice(separator + 1).replace(/^['"]|['"]$/g, "")]] : [];
|
|
48
|
+
}));
|
|
49
|
+
return {
|
|
50
|
+
os: { id: (values.ID ?? "unknown").toLowerCase(), versionId: values.VERSION_ID ?? "unknown" },
|
|
51
|
+
architecture
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
function validateSoftwareRequirements(value, _options = {}) {
|
|
55
|
+
if (!Array.isArray(value) || value.length > 32)
|
|
56
|
+
throw new Error("software requirements must be an array of at most 32 entries");
|
|
57
|
+
const seen = new Set;
|
|
58
|
+
return value.map((item) => {
|
|
59
|
+
if (!item || typeof item !== "object" || Array.isArray(item))
|
|
60
|
+
throw new Error("software requirement must be an object");
|
|
61
|
+
const row = item;
|
|
62
|
+
if (Object.keys(row).some((key) => key !== "id" && key !== "version")) {
|
|
63
|
+
throw new Error("software requirement contains an unknown field");
|
|
64
|
+
}
|
|
65
|
+
if (!["bun", "nginx", "arangodb", "cloudflared", "ufw"].includes(String(row.id)) || typeof row.version !== "string" || !/^[A-Za-z0-9][A-Za-z0-9.-]{0,31}$/.test(row.version)) {
|
|
66
|
+
throw new Error("software requirement coordinate is invalid");
|
|
67
|
+
}
|
|
68
|
+
const requirement = { id: row.id, version: row.version };
|
|
69
|
+
if (seen.has(requirement.id))
|
|
70
|
+
throw new Error(`duplicate software requirement: ${requirement.id}`);
|
|
71
|
+
seen.add(requirement.id);
|
|
72
|
+
const catalog = SOFTWARE_CATALOG.find((candidate) => candidate.id === requirement.id && candidate.version === requirement.version);
|
|
73
|
+
if (!catalog || catalog.status !== "active") {
|
|
74
|
+
throw new Error(`software requirement is not active: ${requirement.id}@${requirement.version}`);
|
|
75
|
+
}
|
|
76
|
+
return requirement;
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
async function ensureSoftwareRequirements(requirementsInput, options) {
|
|
80
|
+
const requirements = validateSoftwareRequirements(requirementsInput);
|
|
81
|
+
const observation = options.observation ?? observeSoftwareHost();
|
|
82
|
+
const os = OS_CATALOG.find((candidate) => candidate.id === observation.os.id && candidate.version === observation.os.versionId && candidate.architecture === observation.architecture);
|
|
83
|
+
if (!os || os.status !== "active") {
|
|
84
|
+
throw new Error(`unsupported software strategy: ${observation.os.id} ${observation.os.versionId} ${observation.architecture}`);
|
|
85
|
+
}
|
|
86
|
+
const results = [];
|
|
87
|
+
for (const requirement of requirements) {
|
|
88
|
+
const strategy = UBUNTU_2604_X64.find(({ requirement: candidate }) => candidate.id === requirement.id && candidate.version === requirement.version);
|
|
89
|
+
if (!strategy)
|
|
90
|
+
throw new Error(`unsupported software requirement: ${requirement.id}@${requirement.version}`);
|
|
91
|
+
const before = await options.exec(strategy.check);
|
|
92
|
+
if (before.exitCode === 0) {
|
|
93
|
+
results.push({ ...requirement, changed: false });
|
|
94
|
+
continue;
|
|
95
|
+
}
|
|
96
|
+
const installed = await options.exec(strategy.install);
|
|
97
|
+
if (installed.exitCode !== 0)
|
|
98
|
+
throw new Error(`could not install ${requirement.id}@${requirement.version}: ${installed.output.trim()}`);
|
|
99
|
+
const after = await options.exec(strategy.check);
|
|
100
|
+
if (after.exitCode !== 0)
|
|
101
|
+
throw new Error(`${requirement.id}@${requirement.version} did not pass its post-install check`);
|
|
102
|
+
results.push({ ...requirement, changed: true });
|
|
103
|
+
}
|
|
104
|
+
return results;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// src/definition.ts
|
|
108
|
+
var PIPELINE_VERSION = 2;
|
|
109
|
+
var DEPLOY_SCHEMA_URL = "https://www.forgezero.net/schemas/deploy-v2.json";
|
|
110
|
+
|
|
111
|
+
class DefinitionError extends Error {
|
|
112
|
+
constructor(message) {
|
|
113
|
+
super(message);
|
|
114
|
+
this.name = "DefinitionError";
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
var record = (value, where) => {
|
|
118
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
119
|
+
throw new DefinitionError(`${where} must be an object.`);
|
|
120
|
+
}
|
|
121
|
+
return value;
|
|
122
|
+
};
|
|
123
|
+
var text = (value, where) => {
|
|
124
|
+
if (typeof value !== "string" || value.trim() === "") {
|
|
125
|
+
throw new DefinitionError(`${where} must be a non-empty string.`);
|
|
126
|
+
}
|
|
127
|
+
return value;
|
|
128
|
+
};
|
|
129
|
+
var exactKeys = (value, allowed, where) => {
|
|
130
|
+
const unknown = Object.keys(value).filter((key) => !allowed.includes(key));
|
|
131
|
+
if (unknown.length > 0)
|
|
132
|
+
throw new DefinitionError(`${where} contains unknown field(s): ${unknown.join(", ")}.`);
|
|
133
|
+
};
|
|
134
|
+
var NAME = /^[a-z][a-z0-9-]{0,62}$/;
|
|
135
|
+
var RESERVED_STEP_ENV = new Set([
|
|
136
|
+
"PATH",
|
|
137
|
+
"HOME",
|
|
138
|
+
"SHELL",
|
|
139
|
+
"PWD",
|
|
140
|
+
"BUN_INSTALL",
|
|
141
|
+
"NODE_OPTIONS",
|
|
142
|
+
"LD_PRELOAD",
|
|
143
|
+
"LD_LIBRARY_PATH",
|
|
144
|
+
"GIT_SSH",
|
|
145
|
+
"GIT_SSH_COMMAND"
|
|
146
|
+
]);
|
|
147
|
+
function parseDeployDefinition(value, options = {}) {
|
|
148
|
+
const root = record(value, "pipeline");
|
|
149
|
+
exactKeys(root, ["$schema", "version", "name", "requireAttestation", "profiles", "steps"], "pipeline");
|
|
150
|
+
if (root.$schema !== undefined && root.$schema !== DEPLOY_SCHEMA_URL) {
|
|
151
|
+
throw new DefinitionError(`pipeline.$schema must be ${DEPLOY_SCHEMA_URL}.`);
|
|
152
|
+
}
|
|
153
|
+
if (root.version !== PIPELINE_VERSION) {
|
|
154
|
+
throw new DefinitionError(`pipeline.version must be ${PIPELINE_VERSION}.`);
|
|
155
|
+
}
|
|
156
|
+
if (root.requireAttestation !== undefined && typeof root.requireAttestation !== "boolean") {
|
|
157
|
+
throw new DefinitionError("pipeline.requireAttestation must be a boolean.");
|
|
158
|
+
}
|
|
159
|
+
const rawProfiles = record(root.profiles, "pipeline.profiles");
|
|
160
|
+
const profileEntries = Object.entries(rawProfiles);
|
|
161
|
+
if (profileEntries.length === 0 || profileEntries.length > 32) {
|
|
162
|
+
throw new DefinitionError("pipeline.profiles must contain from 1 to 32 named profiles.");
|
|
163
|
+
}
|
|
164
|
+
if (!Array.isArray(root.steps) || root.steps.length === 0) {
|
|
165
|
+
throw new DefinitionError("pipeline.steps must contain at least one step.");
|
|
166
|
+
}
|
|
167
|
+
const profiles = {};
|
|
168
|
+
for (const [name2, raw] of profileEntries) {
|
|
169
|
+
if (!NAME.test(name2))
|
|
170
|
+
throw new DefinitionError(`pipeline profile name is invalid: ${name2}.`);
|
|
171
|
+
const profile = record(raw, `profiles.${name2}`);
|
|
172
|
+
exactKeys(profile, ["software"], `profiles.${name2}`);
|
|
173
|
+
if (!Array.isArray(profile.software)) {
|
|
174
|
+
throw new DefinitionError(`profiles.${name2}.software must be an array.`);
|
|
175
|
+
}
|
|
176
|
+
profiles[name2] = { software: validateSoftwareRequirements(profile.software, options) };
|
|
177
|
+
}
|
|
178
|
+
const phases = new Set(["build", "release", "migrate", "health"]);
|
|
179
|
+
const steps = root.steps.map((raw, index) => {
|
|
180
|
+
const step = record(raw, `steps[${index}]`);
|
|
181
|
+
exactKeys(step, ["name", "run", "phase", "scope", "profiles", "secrets", "always", "timeoutMs", "when"], `steps[${index}]`);
|
|
182
|
+
const phase = text(step.phase, `steps[${index}].phase`);
|
|
183
|
+
if (!phases.has(phase))
|
|
184
|
+
throw new DefinitionError(`steps[${index}].phase is not supported.`);
|
|
185
|
+
if (step.scope !== "target" && step.scope !== "release") {
|
|
186
|
+
throw new DefinitionError(`steps[${index}].scope must be target or release.`);
|
|
187
|
+
}
|
|
188
|
+
if (step.always !== undefined && typeof step.always !== "boolean") {
|
|
189
|
+
throw new DefinitionError(`steps[${index}].always must be a boolean.`);
|
|
190
|
+
}
|
|
191
|
+
let selectedProfiles;
|
|
192
|
+
if (step.profiles !== undefined) {
|
|
193
|
+
if (!Array.isArray(step.profiles) || step.profiles.length === 0 || step.profiles.some((name2) => typeof name2 !== "string" || !Object.hasOwn(profiles, name2))) {
|
|
194
|
+
throw new DefinitionError(`steps[${index}].profiles must name existing profiles.`);
|
|
195
|
+
}
|
|
196
|
+
selectedProfiles = [...step.profiles];
|
|
197
|
+
if (new Set(selectedProfiles).size !== selectedProfiles.length) {
|
|
198
|
+
throw new DefinitionError(`steps[${index}].profiles must not contain duplicates.`);
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
if (step.secrets !== undefined && (!Array.isArray(step.secrets) || step.secrets.some((name2) => typeof name2 !== "string" || !/^[A-Z_][A-Z0-9_]*$/.test(name2)))) {
|
|
202
|
+
throw new DefinitionError(`steps[${index}].secrets must contain names only.`);
|
|
203
|
+
}
|
|
204
|
+
if (Array.isArray(step.secrets) && new Set(step.secrets).size !== step.secrets.length) {
|
|
205
|
+
throw new DefinitionError(`steps[${index}].secrets must not contain duplicates.`);
|
|
206
|
+
}
|
|
207
|
+
if (Array.isArray(step.secrets) && step.secrets.some((name2) => RESERVED_STEP_ENV.has(String(name2)))) {
|
|
208
|
+
throw new DefinitionError(`steps[${index}].secrets may not replace process-control environment variables.`);
|
|
209
|
+
}
|
|
210
|
+
const timeoutMs = step.timeoutMs === undefined ? undefined : Number(step.timeoutMs);
|
|
211
|
+
if (timeoutMs !== undefined && (!Number.isSafeInteger(timeoutMs) || timeoutMs < 1 || timeoutMs > 86400000)) {
|
|
212
|
+
throw new DefinitionError(`steps[${index}].timeoutMs must be an integer from 1 to 86400000.`);
|
|
213
|
+
}
|
|
214
|
+
let when;
|
|
215
|
+
if (step.when !== undefined) {
|
|
216
|
+
const conditions = record(step.when, `steps[${index}].when`);
|
|
217
|
+
when = {};
|
|
218
|
+
for (const [name2, expected] of Object.entries(conditions)) {
|
|
219
|
+
if (!/^[A-Z_][A-Z0-9_]*$/.test(name2) || typeof expected !== "string" || expected.length === 0) {
|
|
220
|
+
throw new DefinitionError(`steps[${index}].when must map environment names to non-empty strings.`);
|
|
221
|
+
}
|
|
222
|
+
when[name2] = expected;
|
|
223
|
+
}
|
|
224
|
+
if (Object.keys(when).length === 0)
|
|
225
|
+
throw new DefinitionError(`steps[${index}].when must not be empty.`);
|
|
226
|
+
}
|
|
227
|
+
return {
|
|
228
|
+
name: text(step.name, `steps[${index}].name`),
|
|
229
|
+
run: text(step.run, `steps[${index}].run`),
|
|
230
|
+
phase,
|
|
231
|
+
scope: step.scope,
|
|
232
|
+
profiles: selectedProfiles,
|
|
233
|
+
secrets: step.secrets,
|
|
234
|
+
always: step.always === true,
|
|
235
|
+
timeoutMs,
|
|
236
|
+
when
|
|
237
|
+
};
|
|
238
|
+
});
|
|
239
|
+
if (new Set(steps.map((step) => step.name)).size !== steps.length) {
|
|
240
|
+
throw new DefinitionError("pipeline.steps must have unique names.");
|
|
241
|
+
}
|
|
242
|
+
const name = text(root.name, "pipeline.name");
|
|
243
|
+
if (name.length > 120)
|
|
244
|
+
throw new DefinitionError("pipeline.name must be at most 120 characters.");
|
|
245
|
+
return {
|
|
246
|
+
version: PIPELINE_VERSION,
|
|
247
|
+
name,
|
|
248
|
+
requireAttestation: root.requireAttestation === true,
|
|
249
|
+
profiles,
|
|
250
|
+
steps
|
|
251
|
+
};
|
|
252
|
+
}
|
|
253
|
+
function phasePipeline(definition, phase, profile, executeRelease = false) {
|
|
254
|
+
if (!Object.hasOwn(definition.profiles, profile)) {
|
|
255
|
+
throw new DefinitionError(`pipeline profile does not exist: ${profile}.`);
|
|
256
|
+
}
|
|
257
|
+
return {
|
|
258
|
+
name: `${definition.name}:${phase}`,
|
|
259
|
+
requireAttestation: definition.requireAttestation,
|
|
260
|
+
steps: definition.steps.filter((step) => step.phase === phase && (!step.profiles || step.profiles.includes(profile)) && (step.scope === "target" || executeRelease))
|
|
261
|
+
};
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
// src/deploy-file.ts
|
|
265
|
+
import { createHash } from "node:crypto";
|
|
266
|
+
import { existsSync, mkdirSync, readFileSync as readFileSync2, writeFileSync } from "node:fs";
|
|
267
|
+
import { basename, join } from "node:path";
|
|
268
|
+
var DEPLOY_FILE = ".fz/deploy.json";
|
|
269
|
+
var DEPLOY_TODO_PREFIX = "ForgeZero pipeline TODO:";
|
|
270
|
+
var stable = (value) => {
|
|
271
|
+
if (Array.isArray(value))
|
|
272
|
+
return `[${value.map(stable).join(",")}]`;
|
|
273
|
+
if (value && typeof value === "object") {
|
|
274
|
+
return `{${Object.entries(value).filter(([, entry]) => entry !== undefined).sort(([left], [right]) => left.localeCompare(right)).map(([key, entry]) => `${JSON.stringify(key)}:${stable(entry)}`).join(",")}}`;
|
|
275
|
+
}
|
|
276
|
+
return JSON.stringify(value);
|
|
277
|
+
};
|
|
278
|
+
function deployDefinitionDigest(definition) {
|
|
279
|
+
return `sha256:${createHash("sha256").update(stable(definition)).digest("hex")}`;
|
|
280
|
+
}
|
|
281
|
+
var safeName = (value) => {
|
|
282
|
+
const normalized = value.toLowerCase().replace(/^@[^/]+\//, "").replace(/[^a-z0-9-]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 63);
|
|
283
|
+
return /^[a-z]/.test(normalized) ? normalized : `app-${normalized || "service"}`.slice(0, 63);
|
|
284
|
+
};
|
|
285
|
+
function packageHints(root) {
|
|
286
|
+
const packagePath = join(root, "package.json");
|
|
287
|
+
if (!existsSync(packagePath))
|
|
288
|
+
return { bun: existsSync(join(root, "bun.lock")) };
|
|
289
|
+
try {
|
|
290
|
+
const manifest = JSON.parse(readFileSync2(packagePath, "utf8"));
|
|
291
|
+
return {
|
|
292
|
+
name: typeof manifest.name === "string" ? manifest.name : undefined,
|
|
293
|
+
build: typeof manifest.scripts?.build === "string" ? "bun run build" : undefined,
|
|
294
|
+
bun: existsSync(join(root, "bun.lock")) || existsSync(join(root, "bun.lockb"))
|
|
295
|
+
};
|
|
296
|
+
} catch {
|
|
297
|
+
return { bun: existsSync(join(root, "bun.lock")) };
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
var blocker = (instruction) => `printf '%s\\n' '${DEPLOY_TODO_PREFIX} ${instruction}' >&2; exit 78`;
|
|
301
|
+
function defaultDeployFile(root, options = {}) {
|
|
302
|
+
const hints = packageHints(root);
|
|
303
|
+
const software = options.software ?? (hints.bun ? SOFTWARE_CATALOG.filter((entry) => entry.id === "bun" && entry.status === "active").map(({ id, version }) => ({ id, version })) : []);
|
|
304
|
+
validateSoftwareRequirements(software, { channel: options.channel });
|
|
305
|
+
const profile = options.profile ?? "app";
|
|
306
|
+
const build = hints.build ?? blocker("replace the build step with the project build command");
|
|
307
|
+
return {
|
|
308
|
+
$schema: DEPLOY_SCHEMA_URL,
|
|
309
|
+
version: 2,
|
|
310
|
+
name: safeName(options.name ?? hints.name ?? basename(root)),
|
|
311
|
+
...options.requireAttestation ? { requireAttestation: true } : {},
|
|
312
|
+
profiles: { [profile]: { software } },
|
|
313
|
+
steps: [
|
|
314
|
+
{ name: "build", phase: "build", scope: "target", run: build, timeoutMs: 600000 },
|
|
315
|
+
{
|
|
316
|
+
name: "promote release",
|
|
317
|
+
phase: "release",
|
|
318
|
+
scope: "target",
|
|
319
|
+
run: blocker("replace the release step with an atomic promotion command"),
|
|
320
|
+
timeoutMs: 120000
|
|
321
|
+
},
|
|
322
|
+
{
|
|
323
|
+
name: "health check",
|
|
324
|
+
phase: "health",
|
|
325
|
+
scope: "target",
|
|
326
|
+
run: blocker("replace the health step with a bounded local health check"),
|
|
327
|
+
timeoutMs: 30000
|
|
328
|
+
}
|
|
329
|
+
]
|
|
330
|
+
};
|
|
331
|
+
}
|
|
332
|
+
function inspectDeployFile(root, options = {}) {
|
|
333
|
+
const path = join(root, DEPLOY_FILE);
|
|
334
|
+
if (!existsSync(path))
|
|
335
|
+
throw new Error(`${DEPLOY_FILE} does not exist; run \`fz deploy init\`.`);
|
|
336
|
+
const raw = JSON.parse(readFileSync2(path, "utf8"));
|
|
337
|
+
const definition = parseDeployDefinition(raw, options);
|
|
338
|
+
const problems = definition.steps.filter((step) => step.run.includes(DEPLOY_TODO_PREFIX)).map((step) => `${step.name} still contains the safe initialization blocker`);
|
|
339
|
+
const profiles = Object.keys(definition.profiles).sort();
|
|
340
|
+
return {
|
|
341
|
+
path,
|
|
342
|
+
definition,
|
|
343
|
+
summary: {
|
|
344
|
+
path,
|
|
345
|
+
digest: deployDefinitionDigest(definition),
|
|
346
|
+
version: definition.version,
|
|
347
|
+
name: definition.name,
|
|
348
|
+
profiles,
|
|
349
|
+
software: Object.fromEntries(profiles.map((profile) => [
|
|
350
|
+
profile,
|
|
351
|
+
definition.profiles[profile].software
|
|
352
|
+
])),
|
|
353
|
+
ready: problems.length === 0,
|
|
354
|
+
problems
|
|
355
|
+
}
|
|
356
|
+
};
|
|
357
|
+
}
|
|
358
|
+
function initializeDeployFile(root, options = {}) {
|
|
359
|
+
const path = join(root, DEPLOY_FILE);
|
|
360
|
+
if (existsSync(path) && !options.force) {
|
|
361
|
+
throw new Error(`${DEPLOY_FILE} already exists; use --force only when replacing it deliberately.`);
|
|
362
|
+
}
|
|
363
|
+
const raw = defaultDeployFile(root, options);
|
|
364
|
+
parseDeployDefinition(raw, { channel: options.channel });
|
|
365
|
+
mkdirSync(join(root, ".fz"), { recursive: true });
|
|
366
|
+
writeFileSync(path, `${JSON.stringify(raw, null, 2)}
|
|
367
|
+
`, { mode: 420 });
|
|
368
|
+
return inspectDeployFile(root, { channel: options.channel });
|
|
369
|
+
}
|
|
370
|
+
export {
|
|
371
|
+
inspectDeployFile,
|
|
372
|
+
initializeDeployFile,
|
|
373
|
+
deployDefinitionDigest,
|
|
374
|
+
defaultDeployFile,
|
|
375
|
+
DEPLOY_TODO_PREFIX,
|
|
376
|
+
DEPLOY_SCHEMA_URL,
|
|
377
|
+
DEPLOY_FILE
|
|
378
|
+
};
|
|
@@ -7,12 +7,12 @@ export interface RemoteDeploymentClaim {
|
|
|
7
7
|
claimToken: string;
|
|
8
8
|
claimExpiresAtTs: number;
|
|
9
9
|
attempt: number;
|
|
10
|
-
/** True on
|
|
11
|
-
|
|
10
|
+
/** True on the one target deterministically elected for this release. */
|
|
11
|
+
releaseExecutor: boolean;
|
|
12
12
|
source: {
|
|
13
13
|
repository: string;
|
|
14
14
|
branch: string;
|
|
15
|
-
|
|
15
|
+
profile: string;
|
|
16
16
|
knownHosts?: string;
|
|
17
17
|
auth?: GitSourceAuth;
|
|
18
18
|
};
|
package/dist/deployment.d.ts
CHANGED
|
@@ -16,14 +16,18 @@ export interface CommandResult {
|
|
|
16
16
|
export interface DeploymentRequest {
|
|
17
17
|
/** Optional exact commit from a verified webhook. Never a branch name. */
|
|
18
18
|
revision?: string;
|
|
19
|
-
/**
|
|
20
|
-
|
|
19
|
+
/** True only for the target deterministically elected for release-scoped steps. */
|
|
20
|
+
releaseExecutor?: boolean;
|
|
21
21
|
}
|
|
22
22
|
export interface DeploymentResult {
|
|
23
23
|
key: string;
|
|
24
24
|
repository: string;
|
|
25
25
|
branch: string;
|
|
26
26
|
revision: string;
|
|
27
|
+
/** Semantic digest of the validated definition from this exact commit. */
|
|
28
|
+
definitionDigest: string;
|
|
29
|
+
definitionVersion: number;
|
|
30
|
+
profile: string;
|
|
27
31
|
release: string;
|
|
28
32
|
ok: boolean;
|
|
29
33
|
phases: readonly RunResult[];
|
|
@@ -46,7 +50,7 @@ export interface DeploymentOptions {
|
|
|
46
50
|
key: string;
|
|
47
51
|
repository: string;
|
|
48
52
|
branch: string;
|
|
49
|
-
|
|
53
|
+
profile: string;
|
|
50
54
|
root: string;
|
|
51
55
|
publicApiUrl?: string;
|
|
52
56
|
sourceAuth?: GitSourceAuth;
|
|
@@ -74,8 +78,8 @@ export declare class DeploymentError extends Error {
|
|
|
74
78
|
/**
|
|
75
79
|
* One source and one pipeline owner.
|
|
76
80
|
*
|
|
77
|
-
* A request may choose an exact commit and
|
|
78
|
-
*
|
|
81
|
+
* A request may choose an exact commit and carry the control plane's
|
|
82
|
+
* deterministic release-executor decision. It cannot choose a repository, branch, working directory or
|
|
79
83
|
* command: those are sealed into the agent unit and the checked-out definition.
|
|
80
84
|
*/
|
|
81
85
|
export declare function createDeploymentManager(options: DeploymentOptions): {
|