Files
KArtSell.Aegis/docs/Design/kbx-foundation-v36/scripts/generate-configuration-contracts.mjs
T

89 lines
8.0 KiB
JavaScript

import fs from 'node:fs'
import crypto from 'node:crypto'
const sourcePath='contracts/configuration/kbx.configuration.json'
const text=fs.readFileSync(sourcePath,'utf8')
const source=JSON.parse(text)
const sha=crypto.createHash('sha256').update(text).digest('hex')
const seenKeys=new Set(),seenEnv=new Set()
for(const s of source.settings??[]){
if(seenKeys.has(s.key))throw new Error(`duplicate configuration key ${s.key}`)
if(seenEnv.has(s.env))throw new Error(`duplicate environment variable ${s.env}`)
seenKeys.add(s.key);seenEnv.add(s.env)
}
const seenProfiles=new Set()
for(const e of source.environments??[]){if(seenProfiles.has(e.id))throw new Error(`duplicate environment profile ${e.id}`);seenProfiles.add(e.id)}
const manifest={
schemaVersion:source.schemaVersion,
contractVersion:source.contractVersion,
sourceSha256:sha,
principles:source.principles,
sourcePrecedence:source.sourcePrecedence,
settingCount:source.settings.length,
environmentCount:source.environments.length,
settings:source.settings,
environments:source.environments
}
fs.mkdirSync('generated',{recursive:true})
fs.writeFileSync('generated/configuration-manifest.json',JSON.stringify(manifest,null,2)+'\n')
const settingCatalog=Object.fromEntries(source.settings.map(s=>[s.key,s]))
const envCatalog=Object.fromEntries(source.environments.map(e=>[e.id,e]))
fs.mkdirSync('packages/kbx-contracts/src/generated',{recursive:true})
fs.writeFileSync('packages/kbx-contracts/src/generated/configurationCatalog.ts',`// generated from ${sourcePath}; do not edit.\nimport type { KbxConfigurationSettingDefinition, KbxEnvironmentProfileDefinition } from '../configuration'\nexport const kbxConfigurationSourceSha256='${sha}' as const\nexport const kbxConfigurationCatalog=${JSON.stringify(settingCatalog,null,2)} as const satisfies Record<string,KbxConfigurationSettingDefinition>\nexport const kbxEnvironmentProfiles=${JSON.stringify(envCatalog,null,2)} as const satisfies Record<string,KbxEnvironmentProfileDefinition>\nexport type KbxConfigurationKey=keyof typeof kbxConfigurationCatalog\nexport type KbxEnvironmentProfile=keyof typeof kbxEnvironmentProfiles\n`)
const esc=s=>String(s).replaceAll('\\','\\\\').replaceAll('"','\\"')
const csDefault=v=>v===undefined?'null':`"${esc(String(v))}"`
const csAllowed=v=>`new string[]{${(v??[]).map(x=>`"${esc(x)}"`).join(',')}}`
const csNullableInt=v=>v===undefined?'null':String(v)
const csSettings=source.settings.map(s=>` ["${esc(s.key)}"] = new("${esc(s.key)}","${esc(s.env)}","${esc(s.category)}","${esc(s.type)}",${s.secret?'true':'false'},${s.restartRequired?'true':'false'},${csDefault(s.default)},${csAllowed(s.allowedValues)},${csAllowed(s.requiredIn)},${s.requiredWhen?`"${esc(s.requiredWhen.key)}"`:'null'},${s.requiredWhen?`"${esc(String(s.requiredWhen.equals))}"`:'null'},${csNullableInt(s.minimum)},${csNullableInt(s.maximum)},${csAllowed(s.allowedSources)})`).join(',\n')
const csProfiles=source.environments.map(e=>` ["${esc(e.id)}"] = new("${esc(e.id)}",${e.artifactPromotion?'true':'false'},"${esc(e.migrationStrategy)}","${esc(e.providerNetwork)}",${e.requireHttps?'true':'false'},${e.providerEndpointOverrideAllowed?'true':'false'},${csAllowed(e.requiredGates)})`).join(',\n')
fs.mkdirSync('backend/Shared/Configuration/Generated',{recursive:true})
fs.writeFileSync('backend/Shared/Configuration/Generated/KbxConfigurationCatalog.g.cs',`// generated from ${sourcePath}; do not edit.\nnamespace Kbx.Shared.Configuration.Generated;\npublic sealed record KbxConfigurationSettingDefinition(string Key,string Env,string Category,string Type,bool Secret,bool RestartRequired,string? DefaultValue,string[] AllowedValues,string[] RequiredIn,string? RequiredWhenKey,string? RequiredWhenEquals,int? Minimum,int? Maximum,string[] AllowedSources);\npublic sealed record KbxEnvironmentProfileDefinition(string Id,bool ArtifactPromotion,string MigrationStrategy,string ProviderNetwork,bool RequireHttps,bool ProviderEndpointOverrideAllowed,string[] RequiredGates);\npublic static class KbxConfigurationCatalog {\n public const string SourceSha256="${sha}";\n public static readonly IReadOnlyDictionary<string,KbxConfigurationSettingDefinition> Settings=new Dictionary<string,KbxConfigurationSettingDefinition>(StringComparer.Ordinal) {\n${csSettings}\n };\n public static readonly IReadOnlyDictionary<string,KbxEnvironmentProfileDefinition> Environments=new Dictionary<string,KbxEnvironmentProfileDefinition>(StringComparer.Ordinal) {\n${csProfiles}\n };\n}\n`)
// Generate a secret-free env template. Secret values are intentionally blank.
const envLines=['# Generated KBX environment variable template. Do not commit real secret values.']
for(const s of source.settings){
const suffix=s.secret?' # secret: inject from environment/secret store':''
const value=s.secret?'':(s.default===undefined?'':String(s.default))
envLines.push(`${s.env}=${value}${suffix}`)
}
fs.mkdirSync('deploy/kbx',{recursive:true})
fs.writeFileSync('deploy/kbx/.env.kbx.example',envLines.join('\n')+'\n')
const publicDefaults={}
for(const s of source.settings){if(!s.secret&&s.default!==undefined)publicDefaults[s.key]=s.default}
fs.writeFileSync('deploy/kbx/nonsecret-defaults.generated.json',JSON.stringify(publicDefaults,null,2)+'\n')
fs.writeFileSync('deploy/kbx/environment-matrix.generated.json',JSON.stringify({sourceSha256:sha,environments:source.environments},null,2)+'\n')
fs.mkdirSync('deploy/kbx/environments',{recursive:true})
const profileOverrides={
Development:{'Kbx:Runtime:Environment':'Development','Kbx:Database:MigrationsMode':'startup-apply','Kbx:Security:RequireHttps':false},
Test:{'Kbx:Runtime:Environment':'Test','Kbx:Database:MigrationsMode':'startup-apply','Kbx:Security:RequireHttps':false,'ExternalProviders:Krx:Enabled':false,'ExternalProviders:OpenDart:Enabled':false,'ExternalProviders:Kis:Enabled':false},
Staging:{'Kbx:Runtime:Environment':'Staging','Kbx:Database:MigrationsMode':'predeploy','Kbx:Security:RequireHttps':true},
Production:{'Kbx:Runtime:Environment':'Production','Kbx:Database:MigrationsMode':'predeploy','Kbx:Security:RequireHttps':true,'ExternalProviders:Kis:Environment':'production'}
}
for(const e of source.environments){
const lines=[`# Generated ${e.id} example. Secret values intentionally blank.`]
const overrides=profileOverrides[e.id]??{}
for(const s of source.settings){
let value=s.secret?'':(Object.hasOwn(overrides,s.key)?String(overrides[s.key]):(s.default===undefined?'':String(s.default)))
lines.push(`${s.env}=${value}${s.secret?' # secret':''}`)
}
fs.writeFileSync(`deploy/kbx/environments/${e.id.toLowerCase()}.env.example`,lines.join('\n')+'\n')
}
fs.writeFileSync('deploy/kbx/release-artifact.contract.json',JSON.stringify({
schemaVersion:'1.0',configurationSourceSha256:sha,immutableArtifact:true,environmentSpecificConfigurationExternal:true,
promotionOrder:['Staging','Production'],
requiredEvidence:['generated/release-impact.json','generated/configuration-manifest.json','generated/test-scenario-manifest.json','generated/api-manifest.json','migration-dry-run-report','configuration-validation-report']
},null,2)+'\n')
const md=['# KBX Configuration Reference','',`Source SHA-256: \`${sha}\``,'',`Settings: **${source.settings.length}** · Environments: **${source.environments.length}**`,'','| Key | Env | Category | Type | Secret | Restart |','|---|---|---|---|---:|---:|']
for(const s of source.settings)md.push(`| \`${s.key}\` | \`${s.env}\` | ${s.category} | ${s.type} | ${s.secret?'yes':'no'} | ${s.restartRequired?'yes':'no'} |`)
md.push('','## Environment profiles','')
for(const e of source.environments)md.push(`### ${e.id}\n- Migration: \`${e.migrationStrategy}\`\n- Provider network: \`${e.providerNetwork}\`\n- HTTPS required: \`${e.requireHttps}\`\n- Endpoint override allowed: \`${e.providerEndpointOverrideAllowed}\`\n- Gates: ${e.requiredGates.map(x=>`\`${x}\``).join(', ')}`)
fs.writeFileSync('docs/configuration-reference.generated.md',md.join('\n')+'\n')
console.log(`configuration contracts generated: settings=${source.settings.length}, environments=${source.environments.length}, SHA ${sha.slice(0,12)}`)