Complete SCUM controlled deployment lifecycle
This commit is contained in:
@@ -33,6 +33,7 @@
|
||||
"process.stop",
|
||||
"process.restart",
|
||||
"process.status",
|
||||
"deployment.scum.v1",
|
||||
"files.list",
|
||||
"files.read",
|
||||
"files.patch",
|
||||
@@ -572,6 +573,41 @@
|
||||
]
|
||||
}
|
||||
],
|
||||
"serverDeployments": [
|
||||
{
|
||||
"key": "scum-steamcmd-windows",
|
||||
"version": "1.0.0",
|
||||
"supportedTargets": [
|
||||
{ "os": "windows", "arch": "amd64" }
|
||||
],
|
||||
"steamAppId": "3792580",
|
||||
"executableKey": "scum/server-executable",
|
||||
"installRootKey": "server/install-root",
|
||||
"configKey": "scum/server-settings",
|
||||
"configFormat": "ini",
|
||||
"configMappings": [
|
||||
{ "fieldKey": "serverName", "configKey": "server-settings.server-name", "valueType": "text", "required": true },
|
||||
{ "fieldKey": "gamePort", "configKey": "server-settings.game-port", "valueType": "port", "required": true },
|
||||
{ "fieldKey": "queryPort", "configKey": "server-settings.query-port", "valueType": "port", "required": true },
|
||||
{ "fieldKey": "maxPlayers", "configKey": "server-settings.max-players", "valueType": "integer", "required": true }
|
||||
],
|
||||
"discoveryMarkers": [
|
||||
{ "key": "scum-executable", "kind": "file.exists", "targetKey": "scum/server-executable", "required": true },
|
||||
{ "key": "scum-version", "kind": "command.version", "targetKey": "scum/server-executable", "required": true },
|
||||
{ "key": "scum-steam-app", "kind": "steam.app", "targetKey": "server/install-root", "expected": "3792580", "required": true },
|
||||
{ "key": "scum-config", "kind": "file.exists", "targetKey": "scum/server-settings", "expected": "ServerSettings.ini", "required": true },
|
||||
{ "key": "scum-game-port", "kind": "port.open", "targetKey": "game-port", "required": true },
|
||||
{ "key": "scum-query-port", "kind": "port.open", "targetKey": "query-port", "required": true }
|
||||
],
|
||||
"verificationChecks": [
|
||||
{ "key": "executable", "kind": "executable.present", "targetKey": "scum/server-executable", "required": true },
|
||||
{ "key": "version", "kind": "version.matches", "targetKey": "scum/server-executable", "required": true },
|
||||
{ "key": "game-port", "kind": "port.bound", "targetKey": "game-port", "required": true },
|
||||
{ "key": "config", "kind": "config.readable", "targetKey": "scum/server-settings", "required": true },
|
||||
{ "key": "process", "kind": "process.healthy", "targetKey": "scum/server-executable", "required": true }
|
||||
]
|
||||
}
|
||||
],
|
||||
"logSources": [
|
||||
{
|
||||
"key": "scum-chat-events",
|
||||
|
||||
@@ -109,6 +109,12 @@
|
||||
"items": { "$ref": "#/$defs/runtimeInstallPlan" },
|
||||
"uniqueItems": true
|
||||
},
|
||||
"serverDeployments": {
|
||||
"type": "array",
|
||||
"items": { "$ref": "#/$defs/runtimeServerDeploymentProfile" },
|
||||
"uniqueItems": true,
|
||||
"maxItems": 8
|
||||
},
|
||||
"logSources": {
|
||||
"type": "array",
|
||||
"items": { "$ref": "#/$defs/runtimeLogSource" },
|
||||
@@ -329,6 +335,8 @@
|
||||
"process.stop",
|
||||
"process.restart",
|
||||
"process.status",
|
||||
"deployment.plan.v1",
|
||||
"deployment.scum.v1",
|
||||
"config.write",
|
||||
"files.list",
|
||||
"files.read",
|
||||
@@ -518,6 +526,58 @@
|
||||
"steps": { "type": "array", "items": { "$ref": "#/$defs/runtimeInstallStep" }, "minItems": 1, "maxItems": 64 }
|
||||
}
|
||||
},
|
||||
"runtimeServerConfigMapping": {
|
||||
"type": "object",
|
||||
"required": ["fieldKey", "configKey", "valueType"],
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"fieldKey": { "type": "string", "pattern": "^[A-Za-z][A-Za-z0-9._/-]*$", "maxLength": 80 },
|
||||
"configKey": { "$ref": "#/$defs/logicalKey" },
|
||||
"valueType": { "enum": ["text", "integer", "number", "boolean", "port"] },
|
||||
"required": { "type": "boolean" }
|
||||
}
|
||||
},
|
||||
"runtimeServerDiscoveryMarker": {
|
||||
"type": "object",
|
||||
"required": ["key", "kind", "targetKey"],
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"key": { "$ref": "#/$defs/logicalKey" },
|
||||
"kind": { "enum": ["file.exists", "command.version", "port.open", "steam.app"] },
|
||||
"targetKey": { "$ref": "#/$defs/logicalKey" },
|
||||
"expected": { "type": "string", "maxLength": 120 },
|
||||
"required": { "type": "boolean" }
|
||||
}
|
||||
},
|
||||
"runtimeServerVerificationCheck": {
|
||||
"type": "object",
|
||||
"required": ["key", "kind", "targetKey"],
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"key": { "$ref": "#/$defs/logicalKey" },
|
||||
"kind": { "enum": ["executable.present", "version.matches", "port.bound", "config.readable", "process.healthy"] },
|
||||
"targetKey": { "$ref": "#/$defs/logicalKey" },
|
||||
"required": { "type": "boolean" }
|
||||
}
|
||||
},
|
||||
"runtimeServerDeploymentProfile": {
|
||||
"type": "object",
|
||||
"required": ["key", "version", "supportedTargets", "steamAppId", "executableKey", "installRootKey", "configKey", "configFormat", "configMappings", "discoveryMarkers", "verificationChecks"],
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"key": { "$ref": "#/$defs/logicalKey" },
|
||||
"version": { "type": "string", "pattern": "^[0-9]+\\.[0-9]+\\.[0-9]+(?:-[0-9A-Za-z.-]+)?$" },
|
||||
"supportedTargets": { "type": "array", "items": { "$ref": "#/$defs/runtimeTarget" }, "minItems": 1, "uniqueItems": true },
|
||||
"steamAppId": { "type": "string", "pattern": "^[0-9]{1,12}$" },
|
||||
"executableKey": { "$ref": "#/$defs/logicalKey" },
|
||||
"installRootKey": { "$ref": "#/$defs/logicalKey" },
|
||||
"configKey": { "$ref": "#/$defs/logicalKey" },
|
||||
"configFormat": { "enum": ["ini", "json", "yaml", "properties"] },
|
||||
"configMappings": { "type": "array", "items": { "$ref": "#/$defs/runtimeServerConfigMapping" }, "minItems": 1, "maxItems": 32, "uniqueItems": true },
|
||||
"discoveryMarkers": { "type": "array", "items": { "$ref": "#/$defs/runtimeServerDiscoveryMarker" }, "minItems": 1, "maxItems": 32, "uniqueItems": true },
|
||||
"verificationChecks": { "type": "array", "items": { "$ref": "#/$defs/runtimeServerVerificationCheck" }, "minItems": 5, "maxItems": 16, "uniqueItems": true }
|
||||
}
|
||||
},
|
||||
"runtimeLogSource": {
|
||||
"type": "object",
|
||||
"required": ["key", "kind", "streamKey"],
|
||||
|
||||
@@ -379,6 +379,29 @@ function validateDependencyPlans(manifest: unknown): string[] {
|
||||
return errors;
|
||||
}
|
||||
|
||||
function validateServerDeploymentProfiles(manifest: unknown): string[] {
|
||||
if (typeof manifest !== "object" || manifest === null || !("server" in manifest)) return [];
|
||||
const record = manifest as { server?: { createFields?: Array<{ key?: string }> }; runtimeProfiles?: { serverDeployments?: Array<any> } };
|
||||
const declaredFields = new Set((record.server?.createFields ?? []).map((field) => field.key).filter((key): key is string => Boolean(key)));
|
||||
const errors: string[] = [];
|
||||
for (const [index, profile] of (record.runtimeProfiles?.serverDeployments ?? []).entries()) {
|
||||
const location = `manifest.runtimeProfiles.serverDeployments[${index}]`;
|
||||
const mappingKeys = new Set<string>();
|
||||
for (const [mappingIndex, mapping] of (profile.configMappings ?? []).entries()) {
|
||||
const mappingLocation = `${location}.configMappings[${mappingIndex}]`;
|
||||
if (!declaredFields.has(mapping.fieldKey)) errors.push(`${mappingLocation}.fieldKey: must reference a declared server.createFields key`);
|
||||
if (mappingKeys.has(mapping.fieldKey)) errors.push(`${mappingLocation}.fieldKey: duplicate mapping`);
|
||||
mappingKeys.add(mapping.fieldKey);
|
||||
}
|
||||
const requiredChecks = new Set(["executable.present", "version.matches", "port.bound", "config.readable", "process.healthy"]);
|
||||
for (const check of profile.verificationChecks ?? []) {
|
||||
if (check.required) requiredChecks.delete(check.kind);
|
||||
}
|
||||
for (const missing of requiredChecks) errors.push(`${location}.verificationChecks: required check ${missing} is missing`);
|
||||
}
|
||||
return errors;
|
||||
}
|
||||
|
||||
function validateClientManagerProfiles(manifest: unknown): string[] {
|
||||
if (typeof manifest !== "object" || manifest === null) {
|
||||
return [];
|
||||
@@ -1124,6 +1147,7 @@ export function validateManifestFile(manifestPath: string): string[] {
|
||||
errors.push(...scanUnsafeValues(manifest, "manifest"));
|
||||
errors.push(...validateCreateFieldDeclarations(manifest));
|
||||
errors.push(...validateDependencyPlans(manifest));
|
||||
errors.push(...validateServerDeploymentProfiles(manifest));
|
||||
errors.push(...validateClientManagerProfiles(manifest));
|
||||
errors.push(...validateDLLExtensionProfiles(manifest));
|
||||
errors.push(...validateGameClientBridgeCatalog(manifest));
|
||||
|
||||
@@ -173,6 +173,21 @@ describe("plugin manifest validation", () => {
|
||||
expect(validateManifestFile("examples/scum-server-plugin/manifest.json")).toEqual([]);
|
||||
});
|
||||
|
||||
it("declares a frozen SCUM install/adopt template with explicit mapping and verification checks", () => {
|
||||
const manifest = JSON.parse(fs.readFileSync(path.join(pluginsRoot, "examples/scum-server-plugin/manifest.json"), "utf8")) as any;
|
||||
const template = manifest.runtimeProfiles.serverDeployments[0];
|
||||
expect(template).toMatchObject({ key: "scum-steamcmd-windows", version: "1.0.0", steamAppId: "3792580", configFormat: "ini" });
|
||||
expect(template.configMappings.map((mapping: any) => mapping.fieldKey)).toEqual(["serverName", "gamePort", "queryPort", "maxPlayers"]);
|
||||
expect(template.verificationChecks.filter((check: any) => check.required)).toHaveLength(5);
|
||||
});
|
||||
|
||||
it("rejects an SCUM template mapping an undeclared field", () => {
|
||||
const errors = validateTemporaryScumCompanionManifest((manifest) => {
|
||||
manifest.runtimeProfiles.serverDeployments[0].configMappings[0].fieldKey = "hostCommand";
|
||||
});
|
||||
expect(errors.some((error) => error.includes("configMappings") && error.includes("fieldKey"))).toBe(true);
|
||||
});
|
||||
|
||||
it("rejects unsupported or unsafe inline create-field declarations", () => {
|
||||
const malformed = validateTemporaryScumCompanionManifest((manifest) => {
|
||||
manifest.server.createFields[0].type = "path";
|
||||
|
||||
Reference in New Issue
Block a user