89 lines
8.0 KiB
JavaScript
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)}`)
|