@forgezero/agent 0.1.30 → 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 +28 -0
- package/dist/agent-heartbeat.js +1 -1
- package/dist/definition.d.ts +5 -2
- package/dist/definition.js +32 -21
- package/dist/deploy-file.d.ts +40 -0
- package/dist/deploy-file.js +378 -0
- package/dist/deployment.d.ts +4 -0
- package/dist/fz-agent.js +59 -28
- package/dist/fz.js +417 -19
- package/dist/provision.js +4 -4
- package/dist/software-helper.js +3 -3
- package/dist/software.d.ts +1 -1
- package/dist/software.js +3 -3
- package/dist/version.d.ts +1 -1
- package/package.json +8 -2
- package/schema/deploy-v2.json +70 -0
package/dist/fz-agent.js
CHANGED
|
@@ -4934,7 +4934,7 @@ function observeSoftwareHost(osRelease = readFileSync("/etc/os-release", "utf8")
|
|
|
4934
4934
|
architecture
|
|
4935
4935
|
};
|
|
4936
4936
|
}
|
|
4937
|
-
function validateSoftwareRequirements(value,
|
|
4937
|
+
function validateSoftwareRequirements(value, _options = {}) {
|
|
4938
4938
|
if (!Array.isArray(value) || value.length > 32)
|
|
4939
4939
|
throw new Error("software requirements must be an array of at most 32 entries");
|
|
4940
4940
|
const seen = new Set;
|
|
@@ -4953,8 +4953,8 @@ function validateSoftwareRequirements(value, options = {}) {
|
|
|
4953
4953
|
throw new Error(`duplicate software requirement: ${requirement.id}`);
|
|
4954
4954
|
seen.add(requirement.id);
|
|
4955
4955
|
const catalog = SOFTWARE_CATALOG.find((candidate) => candidate.id === requirement.id && candidate.version === requirement.version);
|
|
4956
|
-
if (!catalog || catalog.status
|
|
4957
|
-
throw new Error(`software requirement is not
|
|
4956
|
+
if (!catalog || catalog.status !== "active") {
|
|
4957
|
+
throw new Error(`software requirement is not active: ${requirement.id}@${requirement.version}`);
|
|
4958
4958
|
}
|
|
4959
4959
|
return requirement;
|
|
4960
4960
|
});
|
|
@@ -4989,6 +4989,7 @@ async function ensureSoftwareRequirements(requirementsInput, options) {
|
|
|
4989
4989
|
|
|
4990
4990
|
// src/definition.ts
|
|
4991
4991
|
var PIPELINE_VERSION = 2;
|
|
4992
|
+
var DEPLOY_SCHEMA_URL = "https://www.forgezero.net/schemas/deploy-v2.json";
|
|
4992
4993
|
|
|
4993
4994
|
class DefinitionError extends Error {
|
|
4994
4995
|
constructor(message) {
|
|
@@ -5026,15 +5027,18 @@ var RESERVED_STEP_ENV = new Set([
|
|
|
5026
5027
|
"GIT_SSH",
|
|
5027
5028
|
"GIT_SSH_COMMAND"
|
|
5028
5029
|
]);
|
|
5029
|
-
function parseDeployDefinition(value) {
|
|
5030
|
+
function parseDeployDefinition(value, options = {}) {
|
|
5030
5031
|
const root = record(value, "pipeline");
|
|
5031
5032
|
exactKeys(root, ["$schema", "version", "name", "requireAttestation", "profiles", "steps"], "pipeline");
|
|
5032
|
-
if (root.$schema !== undefined &&
|
|
5033
|
-
throw new DefinitionError(
|
|
5033
|
+
if (root.$schema !== undefined && root.$schema !== DEPLOY_SCHEMA_URL) {
|
|
5034
|
+
throw new DefinitionError(`pipeline.$schema must be ${DEPLOY_SCHEMA_URL}.`);
|
|
5034
5035
|
}
|
|
5035
5036
|
if (root.version !== PIPELINE_VERSION) {
|
|
5036
5037
|
throw new DefinitionError(`pipeline.version must be ${PIPELINE_VERSION}.`);
|
|
5037
5038
|
}
|
|
5039
|
+
if (root.requireAttestation !== undefined && typeof root.requireAttestation !== "boolean") {
|
|
5040
|
+
throw new DefinitionError("pipeline.requireAttestation must be a boolean.");
|
|
5041
|
+
}
|
|
5038
5042
|
const rawProfiles = record(root.profiles, "pipeline.profiles");
|
|
5039
5043
|
const profileEntries = Object.entries(rawProfiles);
|
|
5040
5044
|
if (profileEntries.length === 0 || profileEntries.length > 32) {
|
|
@@ -5044,15 +5048,15 @@ function parseDeployDefinition(value) {
|
|
|
5044
5048
|
throw new DefinitionError("pipeline.steps must contain at least one step.");
|
|
5045
5049
|
}
|
|
5046
5050
|
const profiles = {};
|
|
5047
|
-
for (const [
|
|
5048
|
-
if (!NAME.test(
|
|
5049
|
-
throw new DefinitionError(`pipeline profile name is invalid: ${
|
|
5050
|
-
const profile = record(raw, `profiles.${
|
|
5051
|
-
exactKeys(profile, ["software"], `profiles.${
|
|
5051
|
+
for (const [name2, raw] of profileEntries) {
|
|
5052
|
+
if (!NAME.test(name2))
|
|
5053
|
+
throw new DefinitionError(`pipeline profile name is invalid: ${name2}.`);
|
|
5054
|
+
const profile = record(raw, `profiles.${name2}`);
|
|
5055
|
+
exactKeys(profile, ["software"], `profiles.${name2}`);
|
|
5052
5056
|
if (!Array.isArray(profile.software)) {
|
|
5053
|
-
throw new DefinitionError(`profiles.${
|
|
5057
|
+
throw new DefinitionError(`profiles.${name2}.software must be an array.`);
|
|
5054
5058
|
}
|
|
5055
|
-
profiles[
|
|
5059
|
+
profiles[name2] = { software: validateSoftwareRequirements(profile.software, options) };
|
|
5056
5060
|
}
|
|
5057
5061
|
const phases = new Set(["build", "release", "migrate", "health"]);
|
|
5058
5062
|
const steps = root.steps.map((raw, index) => {
|
|
@@ -5064,9 +5068,12 @@ function parseDeployDefinition(value) {
|
|
|
5064
5068
|
if (step.scope !== "target" && step.scope !== "release") {
|
|
5065
5069
|
throw new DefinitionError(`steps[${index}].scope must be target or release.`);
|
|
5066
5070
|
}
|
|
5071
|
+
if (step.always !== undefined && typeof step.always !== "boolean") {
|
|
5072
|
+
throw new DefinitionError(`steps[${index}].always must be a boolean.`);
|
|
5073
|
+
}
|
|
5067
5074
|
let selectedProfiles;
|
|
5068
5075
|
if (step.profiles !== undefined) {
|
|
5069
|
-
if (!Array.isArray(step.profiles) || step.profiles.length === 0 || step.profiles.some((
|
|
5076
|
+
if (!Array.isArray(step.profiles) || step.profiles.length === 0 || step.profiles.some((name2) => typeof name2 !== "string" || !Object.hasOwn(profiles, name2))) {
|
|
5070
5077
|
throw new DefinitionError(`steps[${index}].profiles must name existing profiles.`);
|
|
5071
5078
|
}
|
|
5072
5079
|
selectedProfiles = [...step.profiles];
|
|
@@ -5074,13 +5081,13 @@ function parseDeployDefinition(value) {
|
|
|
5074
5081
|
throw new DefinitionError(`steps[${index}].profiles must not contain duplicates.`);
|
|
5075
5082
|
}
|
|
5076
5083
|
}
|
|
5077
|
-
if (step.secrets !== undefined && (!Array.isArray(step.secrets) || step.secrets.some((
|
|
5084
|
+
if (step.secrets !== undefined && (!Array.isArray(step.secrets) || step.secrets.some((name2) => typeof name2 !== "string" || !/^[A-Z_][A-Z0-9_]*$/.test(name2)))) {
|
|
5078
5085
|
throw new DefinitionError(`steps[${index}].secrets must contain names only.`);
|
|
5079
5086
|
}
|
|
5080
5087
|
if (Array.isArray(step.secrets) && new Set(step.secrets).size !== step.secrets.length) {
|
|
5081
5088
|
throw new DefinitionError(`steps[${index}].secrets must not contain duplicates.`);
|
|
5082
5089
|
}
|
|
5083
|
-
if (Array.isArray(step.secrets) && step.secrets.some((
|
|
5090
|
+
if (Array.isArray(step.secrets) && step.secrets.some((name2) => RESERVED_STEP_ENV.has(String(name2)))) {
|
|
5084
5091
|
throw new DefinitionError(`steps[${index}].secrets may not replace process-control environment variables.`);
|
|
5085
5092
|
}
|
|
5086
5093
|
const timeoutMs = step.timeoutMs === undefined ? undefined : Number(step.timeoutMs);
|
|
@@ -5091,11 +5098,11 @@ function parseDeployDefinition(value) {
|
|
|
5091
5098
|
if (step.when !== undefined) {
|
|
5092
5099
|
const conditions = record(step.when, `steps[${index}].when`);
|
|
5093
5100
|
when = {};
|
|
5094
|
-
for (const [
|
|
5095
|
-
if (!/^[A-Z_][A-Z0-9_]*$/.test(
|
|
5101
|
+
for (const [name2, expected] of Object.entries(conditions)) {
|
|
5102
|
+
if (!/^[A-Z_][A-Z0-9_]*$/.test(name2) || typeof expected !== "string" || expected.length === 0) {
|
|
5096
5103
|
throw new DefinitionError(`steps[${index}].when must map environment names to non-empty strings.`);
|
|
5097
5104
|
}
|
|
5098
|
-
when[
|
|
5105
|
+
when[name2] = expected;
|
|
5099
5106
|
}
|
|
5100
5107
|
if (Object.keys(when).length === 0)
|
|
5101
5108
|
throw new DefinitionError(`steps[${index}].when must not be empty.`);
|
|
@@ -5115,15 +5122,32 @@ function parseDeployDefinition(value) {
|
|
|
5115
5122
|
if (new Set(steps.map((step) => step.name)).size !== steps.length) {
|
|
5116
5123
|
throw new DefinitionError("pipeline.steps must have unique names.");
|
|
5117
5124
|
}
|
|
5125
|
+
const name = text(root.name, "pipeline.name");
|
|
5126
|
+
if (name.length > 120)
|
|
5127
|
+
throw new DefinitionError("pipeline.name must be at most 120 characters.");
|
|
5118
5128
|
return {
|
|
5119
5129
|
version: PIPELINE_VERSION,
|
|
5120
|
-
name
|
|
5130
|
+
name,
|
|
5121
5131
|
requireAttestation: root.requireAttestation === true,
|
|
5122
5132
|
profiles,
|
|
5123
5133
|
steps
|
|
5124
5134
|
};
|
|
5125
5135
|
}
|
|
5126
5136
|
|
|
5137
|
+
// src/deploy-file.ts
|
|
5138
|
+
import { createHash } from "crypto";
|
|
5139
|
+
var stable = (value) => {
|
|
5140
|
+
if (Array.isArray(value))
|
|
5141
|
+
return `[${value.map(stable).join(",")}]`;
|
|
5142
|
+
if (value && typeof value === "object") {
|
|
5143
|
+
return `{${Object.entries(value).filter(([, entry]) => entry !== undefined).sort(([left], [right]) => left.localeCompare(right)).map(([key, entry]) => `${JSON.stringify(key)}:${stable(entry)}`).join(",")}}`;
|
|
5144
|
+
}
|
|
5145
|
+
return JSON.stringify(value);
|
|
5146
|
+
};
|
|
5147
|
+
function deployDefinitionDigest(definition) {
|
|
5148
|
+
return `sha256:${createHash("sha256").update(stable(definition)).digest("hex")}`;
|
|
5149
|
+
}
|
|
5150
|
+
|
|
5127
5151
|
// src/pipeline.ts
|
|
5128
5152
|
class PipelineError extends Error {
|
|
5129
5153
|
code;
|
|
@@ -5365,6 +5389,7 @@ function createDeploymentManager(options) {
|
|
|
5365
5389
|
throw new DeploymentError("SOURCE_FAILED", `Git checked out ${head}, not requested ${request.revision}.`);
|
|
5366
5390
|
}
|
|
5367
5391
|
const definition = parseDeployDefinition(readDefinition(join(release, ".fz", "deploy.json")));
|
|
5392
|
+
const definitionDigest = deployDefinitionDigest(definition);
|
|
5368
5393
|
const selectedProfile = definition.profiles[options.profile];
|
|
5369
5394
|
if (!selectedProfile)
|
|
5370
5395
|
throw new DeploymentError("PIPELINE_FAILED", `pipeline profile does not exist: ${options.profile}.`);
|
|
@@ -5414,6 +5439,9 @@ function createDeploymentManager(options) {
|
|
|
5414
5439
|
repository: options.repository,
|
|
5415
5440
|
branch: options.branch,
|
|
5416
5441
|
revision: head,
|
|
5442
|
+
definitionDigest,
|
|
5443
|
+
definitionVersion: definition.version,
|
|
5444
|
+
profile: options.profile,
|
|
5417
5445
|
release,
|
|
5418
5446
|
ok: true,
|
|
5419
5447
|
phases
|
|
@@ -5677,7 +5705,10 @@ async function pullDeploymentOnce(options) {
|
|
|
5677
5705
|
claimToken: claim.claimToken,
|
|
5678
5706
|
ok: true,
|
|
5679
5707
|
detail: `Deployed ${result.revision}.`,
|
|
5680
|
-
release: result.release
|
|
5708
|
+
release: result.release,
|
|
5709
|
+
definitionDigest: result.definitionDigest,
|
|
5710
|
+
definitionVersion: result.definitionVersion,
|
|
5711
|
+
profile: result.profile
|
|
5681
5712
|
});
|
|
5682
5713
|
return { status: "deployed", claim, result };
|
|
5683
5714
|
}
|
|
@@ -6096,7 +6127,7 @@ import { chmodSync as chmodSync5, existsSync as existsSync6, unlinkSync as unlin
|
|
|
6096
6127
|
import { connect as connect2, createServer as createServer3 } from "net";
|
|
6097
6128
|
|
|
6098
6129
|
// src/metal-provision.ts
|
|
6099
|
-
import { createHash } from "crypto";
|
|
6130
|
+
import { createHash as createHash2 } from "crypto";
|
|
6100
6131
|
import {
|
|
6101
6132
|
existsSync as existsSync5,
|
|
6102
6133
|
mkdirSync as mkdirSync3,
|
|
@@ -6249,8 +6280,8 @@ function membersOfLinuxList(value, label) {
|
|
|
6249
6280
|
throw new MetalProvisionError(`${label} list overlaps itself`);
|
|
6250
6281
|
return members;
|
|
6251
6282
|
}
|
|
6252
|
-
var guestNameFor = (computeKey) => `fzg-${
|
|
6253
|
-
var tapNameFor = (computeKey) => `fzt${
|
|
6283
|
+
var guestNameFor = (computeKey) => `fzg-${createHash2("sha256").update(computeKey).digest("hex").slice(0, 16)}`;
|
|
6284
|
+
var tapNameFor = (computeKey) => `fzt${createHash2("sha256").update(computeKey).digest("hex").slice(0, 12)}`;
|
|
6254
6285
|
var macForAddress = (address) => {
|
|
6255
6286
|
const octets = address.split(".").map(Number);
|
|
6256
6287
|
if (octets.length !== 4 || octets.some((value) => !Number.isInteger(value) || value < 0 || value > 255)) {
|
|
@@ -6332,7 +6363,7 @@ function allocateAddress(profile, computeKey, rows) {
|
|
|
6332
6363
|
return existing.address;
|
|
6333
6364
|
const used = new Set(rows.map((row) => row.address));
|
|
6334
6365
|
const width = profile.addressEnd - profile.addressStart + 1;
|
|
6335
|
-
const start =
|
|
6366
|
+
const start = createHash2("sha256").update(computeKey).digest().readUInt16BE(0) % width;
|
|
6336
6367
|
for (let offset = 0;offset < width; offset += 1) {
|
|
6337
6368
|
const last = profile.addressStart + (start + offset) % width;
|
|
6338
6369
|
const address = `${profile.subnetPrefix}.${last}`;
|
|
@@ -7547,7 +7578,7 @@ function materializeWarpMdm(options) {
|
|
|
7547
7578
|
}
|
|
7548
7579
|
|
|
7549
7580
|
// src/agent-update.ts
|
|
7550
|
-
import { createHash as
|
|
7581
|
+
import { createHash as createHash3, timingSafeEqual, randomUUID as randomUUID2 } from "crypto";
|
|
7551
7582
|
import {
|
|
7552
7583
|
chmodSync as chmodSync9,
|
|
7553
7584
|
existsSync as existsSync10,
|
|
@@ -7665,7 +7696,7 @@ async function stageAgentRelease(releaseInput, options) {
|
|
|
7665
7696
|
throw new Error("agent update tarball is empty or exceeds the size limit");
|
|
7666
7697
|
}
|
|
7667
7698
|
const expected = Buffer.from(release.integrity.slice("sha512-".length), "base64");
|
|
7668
|
-
const actual =
|
|
7699
|
+
const actual = createHash3("sha512").update(bytes).digest();
|
|
7669
7700
|
if (!timingSafeEqual(actual, expected))
|
|
7670
7701
|
throw new Error("agent update integrity mismatch");
|
|
7671
7702
|
writeFileSync6(archive, bytes, { mode: 384, flag: "wx" });
|
|
@@ -7907,7 +7938,7 @@ function requestAgentUpdate(request, socketPath = DEFAULT_AGENT_UPDATE_SOCKET, t
|
|
|
7907
7938
|
import { readFileSync as readFileSync7 } from "fs";
|
|
7908
7939
|
|
|
7909
7940
|
// src/version.ts
|
|
7910
|
-
var VERSION2 = "0.1.
|
|
7941
|
+
var VERSION2 = "0.1.31";
|
|
7911
7942
|
|
|
7912
7943
|
// src/agent-heartbeat.ts
|
|
7913
7944
|
var unquote = (value) => value.replace(/^['"]|['"]$/g, "");
|