@forgezero/agent 0.1.29 → 0.1.30
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 +24 -4
- package/dist/agent-heartbeat.js +1 -1
- package/dist/cli/agent-install.d.ts +1 -1
- package/dist/definition.d.ts +14 -8
- package/dist/definition.js +58 -23
- package/dist/deployment-pull.d.ts +3 -3
- package/dist/deployment.d.ts +5 -5
- package/dist/fz-agent.js +78 -44
- package/dist/fz.js +331 -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 +6 -2
package/README.md
CHANGED
|
@@ -118,12 +118,30 @@ root-only control socket or signed outbound claim path, then the deployment
|
|
|
118
118
|
manager validates the checked-in definition and submits it to the common keyed
|
|
119
119
|
queue.
|
|
120
120
|
|
|
121
|
+
## Project context for any repository
|
|
122
|
+
|
|
123
|
+
Conversation memory is not a project database. Initialize one vendor-neutral,
|
|
124
|
+
Git-owned context and generate the small files each AI product discovers:
|
|
125
|
+
|
|
126
|
+
```bash
|
|
127
|
+
fz project init
|
|
128
|
+
# edit .forgezero/project.json and create its named truth sources
|
|
129
|
+
fz project sync
|
|
130
|
+
fz project check
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
`AGENTS.md`, `CLAUDE.md`, `GEMINI.md`, Copilot instructions and the Cursor rule
|
|
134
|
+
are generated adapters. They contain no independent architecture or progress.
|
|
135
|
+
`fz project check` fails when a truth source is missing or an adapter was edited
|
|
136
|
+
by hand. Tools and skills remain optional execution aids; accepted decisions and
|
|
137
|
+
status live in the repository and therefore survive switching AI agents.
|
|
138
|
+
|
|
121
139
|
## Deployment definitions
|
|
122
140
|
|
|
123
|
-
A repository may commit `.fz/deploy.
|
|
141
|
+
A repository may commit `.fz/deploy.json` with its own profiles, prerequisite
|
|
124
142
|
checks, commands, and the exact secret names each step needs. ForgeZero does not
|
|
125
143
|
choose a tenant's database, framework, or deploy shape. The agent validates the
|
|
126
|
-
file before executing any command, prepares only the selected
|
|
144
|
+
file before executing any command, prepares only the selected profile, and gives a
|
|
127
145
|
step only the vault values it explicitly names. `await` returns the complete
|
|
128
146
|
pipeline result; no polling service or persistent queue is required.
|
|
129
147
|
|
|
@@ -131,7 +149,9 @@ The daemon owns source checkout and command execution. Bootstrap explicitly
|
|
|
131
149
|
awaits release one because the API does not exist yet, then the Agent consumes a
|
|
132
150
|
one-use platform enrolment capability. There is no branch watcher. Every normal
|
|
133
151
|
platform or tenant release is one durable API delivery atomically expanded to
|
|
134
|
-
one row for every
|
|
152
|
+
one row for every independently attached compute target. Release-scoped steps
|
|
153
|
+
run on one target elected by stable target ordering, never on a user-declared
|
|
154
|
+
coordinator:
|
|
135
155
|
`pending` is written before dispatch, `running` and a fenced lease before project
|
|
136
156
|
code, and only an awaited successful pipeline writes `deployed`. An expired
|
|
137
157
|
claim can be recovered; its stale token cannot renew or finish.
|
|
@@ -188,7 +208,7 @@ An operator may also force a deployment and await the complete result over the
|
|
|
188
208
|
private control socket:
|
|
189
209
|
|
|
190
210
|
```bash
|
|
191
|
-
fz-agent deploy --revision=<full-40-character-commit> --
|
|
211
|
+
fz-agent deploy --revision=<full-40-character-commit> --release-executor
|
|
192
212
|
fz-agent status
|
|
193
213
|
fz-agent pause
|
|
194
214
|
fz-agent pause-key --key=project:production
|
package/dist/agent-heartbeat.js
CHANGED
package/dist/definition.d.ts
CHANGED
|
@@ -1,14 +1,20 @@
|
|
|
1
1
|
import type { Pipeline, PipelineStep } from './pipeline';
|
|
2
2
|
import { type SoftwareRequirement } from './software';
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
3
|
+
/**
|
|
4
|
+
* Version two separates a repository's deploy recipe from the computes that use
|
|
5
|
+
* it. A target chooses one named profile in the control plane; compute names,
|
|
6
|
+
* counts and cluster leadership never belong in Git.
|
|
7
|
+
*/
|
|
8
|
+
export declare const PIPELINE_VERSION: 2;
|
|
9
|
+
export interface PipelineProfile {
|
|
6
10
|
software: readonly SoftwareRequirement[];
|
|
7
11
|
}
|
|
8
12
|
export interface DeployStep extends PipelineStep {
|
|
9
13
|
phase: 'build' | 'release' | 'migrate' | 'health';
|
|
10
|
-
/**
|
|
11
|
-
|
|
14
|
+
/** Run on this target, or on the one deterministic release executor. */
|
|
15
|
+
scope: 'target' | 'release';
|
|
16
|
+
/** Optional profile filter. An omitted list applies to every profile. */
|
|
17
|
+
profiles?: readonly string[];
|
|
12
18
|
/** Run only when every named non-secret deployment coordinate has this value. */
|
|
13
19
|
when?: Readonly<Record<string, string>>;
|
|
14
20
|
}
|
|
@@ -16,12 +22,12 @@ export interface DeployDefinition {
|
|
|
16
22
|
version: typeof PIPELINE_VERSION;
|
|
17
23
|
name: string;
|
|
18
24
|
requireAttestation?: boolean;
|
|
19
|
-
|
|
25
|
+
profiles: Readonly<Record<string, PipelineProfile>>;
|
|
20
26
|
steps: readonly DeployStep[];
|
|
21
27
|
}
|
|
22
28
|
export declare class DefinitionError extends Error {
|
|
23
29
|
constructor(message: string);
|
|
24
30
|
}
|
|
25
|
-
/** Validate parsed
|
|
31
|
+
/** Validate parsed JSON before any command from it is allowed to run. */
|
|
26
32
|
export declare function parseDeployDefinition(value: unknown): DeployDefinition;
|
|
27
|
-
export declare function phasePipeline(definition: DeployDefinition, phase: DeployStep['phase']): Pipeline;
|
|
33
|
+
export declare function phasePipeline(definition: DeployDefinition, phase: DeployStep['phase'], profile: string, executeRelease?: boolean): Pipeline;
|
package/dist/definition.js
CHANGED
|
@@ -3,6 +3,16 @@ import { readFileSync } from "node:fs";
|
|
|
3
3
|
var BUN_INSTALLER_SHA256 = "bab8acfb046aac8c72407bdcce903957665d655d7acaa3e11c7c4616beae68dd";
|
|
4
4
|
var ARANGO_SHA256 = "b5a9197b4343f2ed554e1ebc1ef8e6529c7c39cde0035cdc311a4747a3355066";
|
|
5
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
|
+
];
|
|
6
16
|
var UBUNTU_2604_X64 = [
|
|
7
17
|
{
|
|
8
18
|
requirement: { id: "bun", version: "1.3.14" },
|
|
@@ -41,7 +51,7 @@ function observeSoftwareHost(osRelease = readFileSync("/etc/os-release", "utf8")
|
|
|
41
51
|
architecture
|
|
42
52
|
};
|
|
43
53
|
}
|
|
44
|
-
function validateSoftwareRequirements(value) {
|
|
54
|
+
function validateSoftwareRequirements(value, options = {}) {
|
|
45
55
|
if (!Array.isArray(value) || value.length > 32)
|
|
46
56
|
throw new Error("software requirements must be an array of at most 32 entries");
|
|
47
57
|
const seen = new Set;
|
|
@@ -59,13 +69,18 @@ function validateSoftwareRequirements(value) {
|
|
|
59
69
|
if (seen.has(requirement.id))
|
|
60
70
|
throw new Error(`duplicate software requirement: ${requirement.id}`);
|
|
61
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 === "retired" || options.channel !== "development" && catalog.status !== "active") {
|
|
74
|
+
throw new Error(`software requirement is not available for ${options.channel ?? "production"}: ${requirement.id}@${requirement.version}`);
|
|
75
|
+
}
|
|
62
76
|
return requirement;
|
|
63
77
|
});
|
|
64
78
|
}
|
|
65
79
|
async function ensureSoftwareRequirements(requirementsInput, options) {
|
|
66
80
|
const requirements = validateSoftwareRequirements(requirementsInput);
|
|
67
81
|
const observation = options.observation ?? observeSoftwareHost();
|
|
68
|
-
|
|
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") {
|
|
69
84
|
throw new Error(`unsupported software strategy: ${observation.os.id} ${observation.os.versionId} ${observation.architecture}`);
|
|
70
85
|
}
|
|
71
86
|
const results = [];
|
|
@@ -90,7 +105,7 @@ async function ensureSoftwareRequirements(requirementsInput, options) {
|
|
|
90
105
|
}
|
|
91
106
|
|
|
92
107
|
// src/definition.ts
|
|
93
|
-
var PIPELINE_VERSION =
|
|
108
|
+
var PIPELINE_VERSION = 2;
|
|
94
109
|
|
|
95
110
|
class DefinitionError extends Error {
|
|
96
111
|
constructor(message) {
|
|
@@ -115,6 +130,7 @@ var exactKeys = (value, allowed, where) => {
|
|
|
115
130
|
if (unknown.length > 0)
|
|
116
131
|
throw new DefinitionError(`${where} contains unknown field(s): ${unknown.join(", ")}.`);
|
|
117
132
|
};
|
|
133
|
+
var NAME = /^[a-z][a-z0-9-]{0,62}$/;
|
|
118
134
|
var RESERVED_STEP_ENV = new Set([
|
|
119
135
|
"PATH",
|
|
120
136
|
"HOME",
|
|
@@ -129,37 +145,52 @@ var RESERVED_STEP_ENV = new Set([
|
|
|
129
145
|
]);
|
|
130
146
|
function parseDeployDefinition(value) {
|
|
131
147
|
const root = record(value, "pipeline");
|
|
132
|
-
exactKeys(root, ["version", "name", "requireAttestation", "
|
|
148
|
+
exactKeys(root, ["$schema", "version", "name", "requireAttestation", "profiles", "steps"], "pipeline");
|
|
149
|
+
if (root.$schema !== undefined && (typeof root.$schema !== "string" || !root.$schema.startsWith("https://"))) {
|
|
150
|
+
throw new DefinitionError("pipeline.$schema must be an HTTPS URL.");
|
|
151
|
+
}
|
|
133
152
|
if (root.version !== PIPELINE_VERSION) {
|
|
134
153
|
throw new DefinitionError(`pipeline.version must be ${PIPELINE_VERSION}.`);
|
|
135
154
|
}
|
|
136
|
-
|
|
137
|
-
|
|
155
|
+
const rawProfiles = record(root.profiles, "pipeline.profiles");
|
|
156
|
+
const profileEntries = Object.entries(rawProfiles);
|
|
157
|
+
if (profileEntries.length === 0 || profileEntries.length > 32) {
|
|
158
|
+
throw new DefinitionError("pipeline.profiles must contain from 1 to 32 named profiles.");
|
|
138
159
|
}
|
|
139
160
|
if (!Array.isArray(root.steps) || root.steps.length === 0) {
|
|
140
161
|
throw new DefinitionError("pipeline.steps must contain at least one step.");
|
|
141
162
|
}
|
|
142
|
-
const
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
163
|
+
const profiles = {};
|
|
164
|
+
for (const [name, raw] of profileEntries) {
|
|
165
|
+
if (!NAME.test(name))
|
|
166
|
+
throw new DefinitionError(`pipeline profile name is invalid: ${name}.`);
|
|
167
|
+
const profile = record(raw, `profiles.${name}`);
|
|
168
|
+
exactKeys(profile, ["software"], `profiles.${name}`);
|
|
169
|
+
if (!Array.isArray(profile.software)) {
|
|
170
|
+
throw new DefinitionError(`profiles.${name}.software must be an array.`);
|
|
147
171
|
}
|
|
148
|
-
|
|
149
|
-
name: text(role.name, `roles[${index}].name`),
|
|
150
|
-
software: validateSoftwareRequirements(role.software)
|
|
151
|
-
};
|
|
152
|
-
});
|
|
153
|
-
if (new Set(roles.map((role) => role.name)).size !== roles.length) {
|
|
154
|
-
throw new DefinitionError("pipeline.roles must have unique names.");
|
|
172
|
+
profiles[name] = { software: validateSoftwareRequirements(profile.software) };
|
|
155
173
|
}
|
|
156
174
|
const phases = new Set(["build", "release", "migrate", "health"]);
|
|
157
175
|
const steps = root.steps.map((raw, index) => {
|
|
158
176
|
const step = record(raw, `steps[${index}]`);
|
|
159
|
-
exactKeys(step, ["name", "run", "phase", "
|
|
177
|
+
exactKeys(step, ["name", "run", "phase", "scope", "profiles", "secrets", "always", "timeoutMs", "when"], `steps[${index}]`);
|
|
160
178
|
const phase = text(step.phase, `steps[${index}].phase`);
|
|
161
179
|
if (!phases.has(phase))
|
|
162
180
|
throw new DefinitionError(`steps[${index}].phase is not supported.`);
|
|
181
|
+
if (step.scope !== "target" && step.scope !== "release") {
|
|
182
|
+
throw new DefinitionError(`steps[${index}].scope must be target or release.`);
|
|
183
|
+
}
|
|
184
|
+
let selectedProfiles;
|
|
185
|
+
if (step.profiles !== undefined) {
|
|
186
|
+
if (!Array.isArray(step.profiles) || step.profiles.length === 0 || step.profiles.some((name) => typeof name !== "string" || !Object.hasOwn(profiles, name))) {
|
|
187
|
+
throw new DefinitionError(`steps[${index}].profiles must name existing profiles.`);
|
|
188
|
+
}
|
|
189
|
+
selectedProfiles = [...step.profiles];
|
|
190
|
+
if (new Set(selectedProfiles).size !== selectedProfiles.length) {
|
|
191
|
+
throw new DefinitionError(`steps[${index}].profiles must not contain duplicates.`);
|
|
192
|
+
}
|
|
193
|
+
}
|
|
163
194
|
if (step.secrets !== undefined && (!Array.isArray(step.secrets) || step.secrets.some((name) => typeof name !== "string" || !/^[A-Z_][A-Z0-9_]*$/.test(name)))) {
|
|
164
195
|
throw new DefinitionError(`steps[${index}].secrets must contain names only.`);
|
|
165
196
|
}
|
|
@@ -190,8 +221,9 @@ function parseDeployDefinition(value) {
|
|
|
190
221
|
name: text(step.name, `steps[${index}].name`),
|
|
191
222
|
run: text(step.run, `steps[${index}].run`),
|
|
192
223
|
phase,
|
|
224
|
+
scope: step.scope,
|
|
225
|
+
profiles: selectedProfiles,
|
|
193
226
|
secrets: step.secrets,
|
|
194
|
-
once: step.once === true,
|
|
195
227
|
always: step.always === true,
|
|
196
228
|
timeoutMs,
|
|
197
229
|
when
|
|
@@ -204,15 +236,18 @@ function parseDeployDefinition(value) {
|
|
|
204
236
|
version: PIPELINE_VERSION,
|
|
205
237
|
name: text(root.name, "pipeline.name"),
|
|
206
238
|
requireAttestation: root.requireAttestation === true,
|
|
207
|
-
|
|
239
|
+
profiles,
|
|
208
240
|
steps
|
|
209
241
|
};
|
|
210
242
|
}
|
|
211
|
-
function phasePipeline(definition, phase) {
|
|
243
|
+
function phasePipeline(definition, phase, profile, executeRelease = false) {
|
|
244
|
+
if (!Object.hasOwn(definition.profiles, profile)) {
|
|
245
|
+
throw new DefinitionError(`pipeline profile does not exist: ${profile}.`);
|
|
246
|
+
}
|
|
212
247
|
return {
|
|
213
248
|
name: `${definition.name}:${phase}`,
|
|
214
249
|
requireAttestation: definition.requireAttestation,
|
|
215
|
-
steps: definition.steps.filter((step) => step.phase === phase)
|
|
250
|
+
steps: definition.steps.filter((step) => step.phase === phase && (!step.profiles || step.profiles.includes(profile)) && (step.scope === "target" || executeRelease))
|
|
216
251
|
};
|
|
217
252
|
}
|
|
218
253
|
export {
|
|
@@ -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,8 +16,8 @@ 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;
|
|
@@ -46,7 +46,7 @@ export interface DeploymentOptions {
|
|
|
46
46
|
key: string;
|
|
47
47
|
repository: string;
|
|
48
48
|
branch: string;
|
|
49
|
-
|
|
49
|
+
profile: string;
|
|
50
50
|
root: string;
|
|
51
51
|
publicApiUrl?: string;
|
|
52
52
|
sourceAuth?: GitSourceAuth;
|
|
@@ -74,8 +74,8 @@ export declare class DeploymentError extends Error {
|
|
|
74
74
|
/**
|
|
75
75
|
* One source and one pipeline owner.
|
|
76
76
|
*
|
|
77
|
-
* A request may choose an exact commit and
|
|
78
|
-
*
|
|
77
|
+
* A request may choose an exact commit and carry the control plane's
|
|
78
|
+
* deterministic release-executor decision. It cannot choose a repository, branch, working directory or
|
|
79
79
|
* command: those are sealed into the agent unit and the checked-out definition.
|
|
80
80
|
*/
|
|
81
81
|
export declare function createDeploymentManager(options: DeploymentOptions): {
|
package/dist/fz-agent.js
CHANGED
|
@@ -4886,6 +4886,16 @@ import { readFileSync } from "fs";
|
|
|
4886
4886
|
var BUN_INSTALLER_SHA256 = "bab8acfb046aac8c72407bdcce903957665d655d7acaa3e11c7c4616beae68dd";
|
|
4887
4887
|
var ARANGO_SHA256 = "b5a9197b4343f2ed554e1ebc1ef8e6529c7c39cde0035cdc311a4747a3355066";
|
|
4888
4888
|
var CLOUDFLARED_SHA256 = "9d71c677db00134c1bd4144b7783486b654ad281b1ea62b4972098d19f770f17";
|
|
4889
|
+
var OS_CATALOG = [
|
|
4890
|
+
{ id: "ubuntu", version: "26.04", architecture: "x64", status: "active" }
|
|
4891
|
+
];
|
|
4892
|
+
var SOFTWARE_CATALOG = [
|
|
4893
|
+
{ id: "bun", version: "1.3.14", status: "active", os: "ubuntu", osVersion: "26.04", architecture: "x64", evidence: "reviewed-strategy-and-tests" },
|
|
4894
|
+
{ id: "nginx", version: "ubuntu-26.04", status: "active", os: "ubuntu", osVersion: "26.04", architecture: "x64", evidence: "reviewed-strategy-and-tests" },
|
|
4895
|
+
{ id: "arangodb", version: "3.11.14", status: "active", os: "ubuntu", osVersion: "26.04", architecture: "x64", evidence: "reviewed-strategy-and-tests" },
|
|
4896
|
+
{ id: "cloudflared", version: "2026.7.3", status: "active", os: "ubuntu", osVersion: "26.04", architecture: "x64", evidence: "reviewed-strategy-and-tests" },
|
|
4897
|
+
{ id: "ufw", version: "ubuntu-26.04", status: "active", os: "ubuntu", osVersion: "26.04", architecture: "x64", evidence: "reviewed-strategy-and-tests" }
|
|
4898
|
+
];
|
|
4889
4899
|
var UBUNTU_2604_X64 = [
|
|
4890
4900
|
{
|
|
4891
4901
|
requirement: { id: "bun", version: "1.3.14" },
|
|
@@ -4924,7 +4934,7 @@ function observeSoftwareHost(osRelease = readFileSync("/etc/os-release", "utf8")
|
|
|
4924
4934
|
architecture
|
|
4925
4935
|
};
|
|
4926
4936
|
}
|
|
4927
|
-
function validateSoftwareRequirements(value) {
|
|
4937
|
+
function validateSoftwareRequirements(value, options = {}) {
|
|
4928
4938
|
if (!Array.isArray(value) || value.length > 32)
|
|
4929
4939
|
throw new Error("software requirements must be an array of at most 32 entries");
|
|
4930
4940
|
const seen = new Set;
|
|
@@ -4942,13 +4952,18 @@ function validateSoftwareRequirements(value) {
|
|
|
4942
4952
|
if (seen.has(requirement.id))
|
|
4943
4953
|
throw new Error(`duplicate software requirement: ${requirement.id}`);
|
|
4944
4954
|
seen.add(requirement.id);
|
|
4955
|
+
const catalog = SOFTWARE_CATALOG.find((candidate) => candidate.id === requirement.id && candidate.version === requirement.version);
|
|
4956
|
+
if (!catalog || catalog.status === "retired" || options.channel !== "development" && catalog.status !== "active") {
|
|
4957
|
+
throw new Error(`software requirement is not available for ${options.channel ?? "production"}: ${requirement.id}@${requirement.version}`);
|
|
4958
|
+
}
|
|
4945
4959
|
return requirement;
|
|
4946
4960
|
});
|
|
4947
4961
|
}
|
|
4948
4962
|
async function ensureSoftwareRequirements(requirementsInput, options) {
|
|
4949
4963
|
const requirements = validateSoftwareRequirements(requirementsInput);
|
|
4950
4964
|
const observation = options.observation ?? observeSoftwareHost();
|
|
4951
|
-
|
|
4965
|
+
const os = OS_CATALOG.find((candidate) => candidate.id === observation.os.id && candidate.version === observation.os.versionId && candidate.architecture === observation.architecture);
|
|
4966
|
+
if (!os || os.status !== "active") {
|
|
4952
4967
|
throw new Error(`unsupported software strategy: ${observation.os.id} ${observation.os.versionId} ${observation.architecture}`);
|
|
4953
4968
|
}
|
|
4954
4969
|
const results = [];
|
|
@@ -4973,7 +4988,7 @@ async function ensureSoftwareRequirements(requirementsInput, options) {
|
|
|
4973
4988
|
}
|
|
4974
4989
|
|
|
4975
4990
|
// src/definition.ts
|
|
4976
|
-
var PIPELINE_VERSION =
|
|
4991
|
+
var PIPELINE_VERSION = 2;
|
|
4977
4992
|
|
|
4978
4993
|
class DefinitionError extends Error {
|
|
4979
4994
|
constructor(message) {
|
|
@@ -4998,6 +5013,7 @@ var exactKeys = (value, allowed, where) => {
|
|
|
4998
5013
|
if (unknown.length > 0)
|
|
4999
5014
|
throw new DefinitionError(`${where} contains unknown field(s): ${unknown.join(", ")}.`);
|
|
5000
5015
|
};
|
|
5016
|
+
var NAME = /^[a-z][a-z0-9-]{0,62}$/;
|
|
5001
5017
|
var RESERVED_STEP_ENV = new Set([
|
|
5002
5018
|
"PATH",
|
|
5003
5019
|
"HOME",
|
|
@@ -5012,37 +5028,52 @@ var RESERVED_STEP_ENV = new Set([
|
|
|
5012
5028
|
]);
|
|
5013
5029
|
function parseDeployDefinition(value) {
|
|
5014
5030
|
const root = record(value, "pipeline");
|
|
5015
|
-
exactKeys(root, ["version", "name", "requireAttestation", "
|
|
5031
|
+
exactKeys(root, ["$schema", "version", "name", "requireAttestation", "profiles", "steps"], "pipeline");
|
|
5032
|
+
if (root.$schema !== undefined && (typeof root.$schema !== "string" || !root.$schema.startsWith("https://"))) {
|
|
5033
|
+
throw new DefinitionError("pipeline.$schema must be an HTTPS URL.");
|
|
5034
|
+
}
|
|
5016
5035
|
if (root.version !== PIPELINE_VERSION) {
|
|
5017
5036
|
throw new DefinitionError(`pipeline.version must be ${PIPELINE_VERSION}.`);
|
|
5018
5037
|
}
|
|
5019
|
-
|
|
5020
|
-
|
|
5038
|
+
const rawProfiles = record(root.profiles, "pipeline.profiles");
|
|
5039
|
+
const profileEntries = Object.entries(rawProfiles);
|
|
5040
|
+
if (profileEntries.length === 0 || profileEntries.length > 32) {
|
|
5041
|
+
throw new DefinitionError("pipeline.profiles must contain from 1 to 32 named profiles.");
|
|
5021
5042
|
}
|
|
5022
5043
|
if (!Array.isArray(root.steps) || root.steps.length === 0) {
|
|
5023
5044
|
throw new DefinitionError("pipeline.steps must contain at least one step.");
|
|
5024
5045
|
}
|
|
5025
|
-
const
|
|
5026
|
-
|
|
5027
|
-
|
|
5028
|
-
|
|
5029
|
-
|
|
5046
|
+
const profiles = {};
|
|
5047
|
+
for (const [name, raw] of profileEntries) {
|
|
5048
|
+
if (!NAME.test(name))
|
|
5049
|
+
throw new DefinitionError(`pipeline profile name is invalid: ${name}.`);
|
|
5050
|
+
const profile = record(raw, `profiles.${name}`);
|
|
5051
|
+
exactKeys(profile, ["software"], `profiles.${name}`);
|
|
5052
|
+
if (!Array.isArray(profile.software)) {
|
|
5053
|
+
throw new DefinitionError(`profiles.${name}.software must be an array.`);
|
|
5030
5054
|
}
|
|
5031
|
-
|
|
5032
|
-
name: text(role.name, `roles[${index}].name`),
|
|
5033
|
-
software: validateSoftwareRequirements(role.software)
|
|
5034
|
-
};
|
|
5035
|
-
});
|
|
5036
|
-
if (new Set(roles.map((role) => role.name)).size !== roles.length) {
|
|
5037
|
-
throw new DefinitionError("pipeline.roles must have unique names.");
|
|
5055
|
+
profiles[name] = { software: validateSoftwareRequirements(profile.software) };
|
|
5038
5056
|
}
|
|
5039
5057
|
const phases = new Set(["build", "release", "migrate", "health"]);
|
|
5040
5058
|
const steps = root.steps.map((raw, index) => {
|
|
5041
5059
|
const step = record(raw, `steps[${index}]`);
|
|
5042
|
-
exactKeys(step, ["name", "run", "phase", "
|
|
5060
|
+
exactKeys(step, ["name", "run", "phase", "scope", "profiles", "secrets", "always", "timeoutMs", "when"], `steps[${index}]`);
|
|
5043
5061
|
const phase = text(step.phase, `steps[${index}].phase`);
|
|
5044
5062
|
if (!phases.has(phase))
|
|
5045
5063
|
throw new DefinitionError(`steps[${index}].phase is not supported.`);
|
|
5064
|
+
if (step.scope !== "target" && step.scope !== "release") {
|
|
5065
|
+
throw new DefinitionError(`steps[${index}].scope must be target or release.`);
|
|
5066
|
+
}
|
|
5067
|
+
let selectedProfiles;
|
|
5068
|
+
if (step.profiles !== undefined) {
|
|
5069
|
+
if (!Array.isArray(step.profiles) || step.profiles.length === 0 || step.profiles.some((name) => typeof name !== "string" || !Object.hasOwn(profiles, name))) {
|
|
5070
|
+
throw new DefinitionError(`steps[${index}].profiles must name existing profiles.`);
|
|
5071
|
+
}
|
|
5072
|
+
selectedProfiles = [...step.profiles];
|
|
5073
|
+
if (new Set(selectedProfiles).size !== selectedProfiles.length) {
|
|
5074
|
+
throw new DefinitionError(`steps[${index}].profiles must not contain duplicates.`);
|
|
5075
|
+
}
|
|
5076
|
+
}
|
|
5046
5077
|
if (step.secrets !== undefined && (!Array.isArray(step.secrets) || step.secrets.some((name) => typeof name !== "string" || !/^[A-Z_][A-Z0-9_]*$/.test(name)))) {
|
|
5047
5078
|
throw new DefinitionError(`steps[${index}].secrets must contain names only.`);
|
|
5048
5079
|
}
|
|
@@ -5073,8 +5104,9 @@ function parseDeployDefinition(value) {
|
|
|
5073
5104
|
name: text(step.name, `steps[${index}].name`),
|
|
5074
5105
|
run: text(step.run, `steps[${index}].run`),
|
|
5075
5106
|
phase,
|
|
5107
|
+
scope: step.scope,
|
|
5108
|
+
profiles: selectedProfiles,
|
|
5076
5109
|
secrets: step.secrets,
|
|
5077
|
-
once: step.once === true,
|
|
5078
5110
|
always: step.always === true,
|
|
5079
5111
|
timeoutMs,
|
|
5080
5112
|
when
|
|
@@ -5087,7 +5119,7 @@ function parseDeployDefinition(value) {
|
|
|
5087
5119
|
version: PIPELINE_VERSION,
|
|
5088
5120
|
name: text(root.name, "pipeline.name"),
|
|
5089
5121
|
requireAttestation: root.requireAttestation === true,
|
|
5090
|
-
|
|
5122
|
+
profiles,
|
|
5091
5123
|
steps
|
|
5092
5124
|
};
|
|
5093
5125
|
}
|
|
@@ -5217,7 +5249,7 @@ function createDeploymentManager(options) {
|
|
|
5217
5249
|
const exec = options.exec ?? shell;
|
|
5218
5250
|
const projectExec = options.projectExec ?? exec;
|
|
5219
5251
|
const now = options.now ?? Date.now;
|
|
5220
|
-
const readDefinition = options.readDefinition ?? ((path) =>
|
|
5252
|
+
const readDefinition = options.readDefinition ?? ((path) => JSON.parse(readFileSync2(path, "utf8")));
|
|
5221
5253
|
const credentialsDirectory = process.env.CREDENTIALS_DIRECTORY;
|
|
5222
5254
|
const gitCredentialPath = options.gitCredentialPath ?? (credentialsDirectory ? join(credentialsDirectory, "git-deploy-key") : undefined);
|
|
5223
5255
|
const knownHostsPath = options.knownHostsPath ?? "/etc/forgezero/git/known_hosts";
|
|
@@ -5332,22 +5364,22 @@ function createDeploymentManager(options) {
|
|
|
5332
5364
|
if (request.revision && head !== request.revision) {
|
|
5333
5365
|
throw new DeploymentError("SOURCE_FAILED", `Git checked out ${head}, not requested ${request.revision}.`);
|
|
5334
5366
|
}
|
|
5335
|
-
const definition = parseDeployDefinition(readDefinition(join(release, ".fz", "deploy.
|
|
5336
|
-
const
|
|
5337
|
-
if (!
|
|
5338
|
-
throw new DeploymentError("PIPELINE_FAILED", `pipeline
|
|
5339
|
-
if (
|
|
5367
|
+
const definition = parseDeployDefinition(readDefinition(join(release, ".fz", "deploy.json")));
|
|
5368
|
+
const selectedProfile = definition.profiles[options.profile];
|
|
5369
|
+
if (!selectedProfile)
|
|
5370
|
+
throw new DeploymentError("PIPELINE_FAILED", `pipeline profile does not exist: ${options.profile}.`);
|
|
5371
|
+
if (selectedProfile.software.length > 0) {
|
|
5340
5372
|
if (!options.ensureSoftware) {
|
|
5341
5373
|
throw new DeploymentError("PIPELINE_FAILED", "the supervised software helper is unavailable");
|
|
5342
5374
|
}
|
|
5343
|
-
await options.ensureSoftware(
|
|
5375
|
+
await options.ensureSoftware(selectedProfile.software);
|
|
5344
5376
|
}
|
|
5345
5377
|
const phaseEnvironment = {
|
|
5346
5378
|
...options.environment ?? {},
|
|
5347
5379
|
FZ_RELEASE: release,
|
|
5348
5380
|
FZ_DEPLOY_REVISION: head,
|
|
5349
5381
|
FZ_DEPLOY_BRANCH: options.branch,
|
|
5350
|
-
|
|
5382
|
+
FZ_DEPLOY_PROFILE: options.profile,
|
|
5351
5383
|
...options.publicApiUrl ? { PUBLIC_API_URL: options.publicApiUrl } : {}
|
|
5352
5384
|
};
|
|
5353
5385
|
const phaseExec = ({ command, env: secrets, timeoutMs }) => projectExec({ command, cwd: release, env: { ...phaseEnvironment, ...secrets }, timeoutMs });
|
|
@@ -5363,7 +5395,7 @@ function createDeploymentManager(options) {
|
|
|
5363
5395
|
return {
|
|
5364
5396
|
name: `${definition.name}:${phase}`,
|
|
5365
5397
|
requireAttestation: definition.requireAttestation,
|
|
5366
|
-
steps: definition.steps.filter((step) => step.phase === phase && (!step.
|
|
5398
|
+
steps: definition.steps.filter((step) => step.phase === phase && (!step.profiles || step.profiles.includes(options.profile)) && (step.scope === "target" || request.releaseExecutor === true) && (!step.when || Object.entries(step.when).every(([name, expected]) => phaseEnvironment[name] === expected)))
|
|
5367
5399
|
};
|
|
5368
5400
|
})
|
|
5369
5401
|
].filter((pipeline) => pipeline.steps.length > 0);
|
|
@@ -5400,7 +5432,7 @@ function createDeploymentManager(options) {
|
|
|
5400
5432
|
return revision.toLowerCase();
|
|
5401
5433
|
},
|
|
5402
5434
|
deploy(request = {}) {
|
|
5403
|
-
const revisionKey = request.revision ? `${request.revision.toLowerCase()}:${request.
|
|
5435
|
+
const revisionKey = request.revision ? `${request.revision.toLowerCase()}:${request.releaseExecutor === true ? "release" : "target"}` : undefined;
|
|
5404
5436
|
if (revisionKey) {
|
|
5405
5437
|
const existing = activeRevisions.get(revisionKey);
|
|
5406
5438
|
if (existing)
|
|
@@ -5592,7 +5624,7 @@ async function deployClaim(options, claim) {
|
|
|
5592
5624
|
schedule();
|
|
5593
5625
|
let result;
|
|
5594
5626
|
try {
|
|
5595
|
-
result = await manager.deploy({ revision: claim.revision,
|
|
5627
|
+
result = await manager.deploy({ revision: claim.revision, releaseExecutor: claim.releaseExecutor }).result;
|
|
5596
5628
|
} finally {
|
|
5597
5629
|
stopped = true;
|
|
5598
5630
|
clearTimer(timer);
|
|
@@ -7875,7 +7907,7 @@ function requestAgentUpdate(request, socketPath = DEFAULT_AGENT_UPDATE_SOCKET, t
|
|
|
7875
7907
|
import { readFileSync as readFileSync7 } from "fs";
|
|
7876
7908
|
|
|
7877
7909
|
// src/version.ts
|
|
7878
|
-
var VERSION2 = "0.1.
|
|
7910
|
+
var VERSION2 = "0.1.30";
|
|
7879
7911
|
|
|
7880
7912
|
// src/agent-heartbeat.ts
|
|
7881
7913
|
var unquote = (value) => value.replace(/^['"]|['"]$/g, "");
|
|
@@ -8164,7 +8196,7 @@ if (import.meta.main) {
|
|
|
8164
8196
|
" FZ_SEED_PATH legacy/dev seed file (default: " + DEFAULT_SEED_PATH + ")",
|
|
8165
8197
|
" FZ_NODE_KEY override the node key (default: derived from the seed)",
|
|
8166
8198
|
"",
|
|
8167
|
-
" deploy [--revision=<full-sha>] [--
|
|
8199
|
+
" deploy [--revision=<full-sha>] [--release-executor]",
|
|
8168
8200
|
" identity print this sealed seed's public identity",
|
|
8169
8201
|
" enrol consume a systemd-loaded compute capability",
|
|
8170
8202
|
" metal-helper --profile=/etc/forgezero/metal.json",
|
|
@@ -8221,9 +8253,9 @@ if (import.meta.main) {
|
|
|
8221
8253
|
const profilePath = args.find((arg) => arg.startsWith("--profile="))?.slice("--profile=".length);
|
|
8222
8254
|
if (!profilePath)
|
|
8223
8255
|
throw new Error("metal-helper requires --profile=/absolute/path.json");
|
|
8224
|
-
const
|
|
8256
|
+
const profile2 = JSON.parse(readFileSync8(profilePath, "utf8"));
|
|
8225
8257
|
const helper = startMetalHelper({
|
|
8226
|
-
profile,
|
|
8258
|
+
profile: profile2,
|
|
8227
8259
|
socketPath: process.env.FZ_METAL_HELPER_SOCKET ?? DEFAULT_METAL_HELPER_SOCKET
|
|
8228
8260
|
});
|
|
8229
8261
|
console.log(`[metal-helper] listening on ${process.env.FZ_METAL_HELPER_SOCKET ?? DEFAULT_METAL_HELPER_SOCKET}`);
|
|
@@ -8339,8 +8371,8 @@ if (import.meta.main) {
|
|
|
8339
8371
|
const profilePath = args.find((arg) => arg.startsWith("--profile="))?.slice("--profile=".length);
|
|
8340
8372
|
if (!profilePath)
|
|
8341
8373
|
throw new Error("metal-isolation requires --profile=/absolute/path.json");
|
|
8342
|
-
const
|
|
8343
|
-
await applyMetalIsolation(
|
|
8374
|
+
const profile2 = JSON.parse(readFileSync8(profilePath, "utf8"));
|
|
8375
|
+
await applyMetalIsolation(profile2);
|
|
8344
8376
|
console.log("[metal-isolation] host and guest cgroup boundaries active");
|
|
8345
8377
|
process.exit(0);
|
|
8346
8378
|
}
|
|
@@ -8468,7 +8500,7 @@ if (import.meta.main) {
|
|
|
8468
8500
|
op: "deploy",
|
|
8469
8501
|
request: {
|
|
8470
8502
|
revision: revisionArg?.slice("--revision=".length),
|
|
8471
|
-
|
|
8503
|
+
releaseExecutor: args.includes("--release-executor")
|
|
8472
8504
|
}
|
|
8473
8505
|
} : command2 === "cancel" ? { op: "cancel", id: idArg?.slice("--id=".length) ?? "" } : ["pause-key", "resume-key", "stop-key", "start-key"].includes(command2) ? {
|
|
8474
8506
|
op: command2,
|
|
@@ -8575,19 +8607,19 @@ if (import.meta.main) {
|
|
|
8575
8607
|
} : systemdDeploymentSecrets;
|
|
8576
8608
|
const repository = process.env.FZ_DEPLOY_REPO;
|
|
8577
8609
|
const branch = process.env.FZ_DEPLOY_BRANCH;
|
|
8578
|
-
const
|
|
8610
|
+
const profile = process.env.FZ_DEPLOY_PROFILE;
|
|
8579
8611
|
const root = process.env.FZ_DEPLOY_ROOT;
|
|
8580
8612
|
const pullEnabled = process.env.FZ_DEPLOY_PULL === "true" && Boolean(process.env.FZ_API);
|
|
8581
8613
|
if (pullEnabled && !binding) {
|
|
8582
8614
|
throw new Error("agent: outbound guest deployment requires a persisted tenant enrolment binding");
|
|
8583
8615
|
}
|
|
8584
|
-
if (root && (repository && branch &&
|
|
8616
|
+
if (root && (repository && branch && profile || pullEnabled)) {
|
|
8585
8617
|
const managers = new Map;
|
|
8586
8618
|
const managerOptions = (source, key) => ({
|
|
8587
8619
|
key,
|
|
8588
8620
|
repository: source.repository,
|
|
8589
8621
|
branch: source.branch,
|
|
8590
|
-
|
|
8622
|
+
profile: source.profile,
|
|
8591
8623
|
sourceAuth: source.auth,
|
|
8592
8624
|
root,
|
|
8593
8625
|
publicApiUrl: process.env.FZ_PUBLIC_API_URL,
|
|
@@ -8604,7 +8636,7 @@ if (import.meta.main) {
|
|
|
8604
8636
|
ensureSoftware: (requirements) => requestSoftware(requirements, process.env.FZ_SOFTWARE_HELPER_SOCKET ?? DEFAULT_SOFTWARE_HELPER_SOCKET),
|
|
8605
8637
|
projectExec: process.env.FZ_DEPLOY_RUNNER_SOCKET ? (input) => requestDeploymentCommand(input, process.env.FZ_DEPLOY_RUNNER_SOCKET) : undefined
|
|
8606
8638
|
});
|
|
8607
|
-
const staticManager = repository && branch &&
|
|
8639
|
+
const staticManager = repository && branch && profile ? createDeploymentManager(managerOptions({ repository, branch, profile }, process.env.FZ_DEPLOY_KEY ?? `${repository}:${branch}:${profile}`)) : undefined;
|
|
8608
8640
|
if (staticManager)
|
|
8609
8641
|
managers.set("__static__", staticManager);
|
|
8610
8642
|
const control = staticManager ? startControlServer(staticManager, process.env.FZ_CONTROL_SOCKET ?? DEFAULT_CONTROL_SOCKET) : undefined;
|
|
@@ -8620,7 +8652,7 @@ if (import.meta.main) {
|
|
|
8620
8652
|
claim.pipelineKey,
|
|
8621
8653
|
claim.source.repository,
|
|
8622
8654
|
claim.source.branch,
|
|
8623
|
-
claim.source.
|
|
8655
|
+
claim.source.profile,
|
|
8624
8656
|
JSON.stringify(claim.source.auth ?? null)
|
|
8625
8657
|
].join("\x00");
|
|
8626
8658
|
const existing = managers.get(cacheKey);
|
|
@@ -8818,6 +8850,8 @@ export {
|
|
|
8818
8850
|
SUPPORTED_GUEST_IMAGE,
|
|
8819
8851
|
SOFTWARE_HELPER_UNIT_PATH,
|
|
8820
8852
|
SOFTWARE_HELPER_GROUP,
|
|
8853
|
+
SOFTWARE_CATALOG,
|
|
8854
|
+
OS_CATALOG,
|
|
8821
8855
|
MAX_AGENT_TARBALL_BYTES,
|
|
8822
8856
|
DeploymentError,
|
|
8823
8857
|
DEFAULT_SOFTWARE_HELPER_SOCKET,
|