// Pure helpers for the application promotion pipeline: producing the next // version's deployment plan and advancing assignment rings. No I/O, so both are // unit-tested; the controller persists/pushes the results. // Bump the trailing numeric component of a version string (1.2.3 -> 1.2.4, // 126 -> 127). Falls back to appending ".1" when no number is present. export function bumpVersion(version = '') { const text = String(version).trim(); if (!text) return '1.0.0'; const match = text.match(/^(.*?)(\d+)(\D*)$/); if (!match) return `${text}.1`; const [, head, num, tail] = match; return `${head}${Number(num) + 1}${tail}`; } // Build a fresh deployment payload for the next version, based on an existing // plan. The new plan is unpublished (no graphAppId), draft status, and keeps the // application link so the catalog groups both versions. export function cloneForNextVersion(deployment, { version, applicationId } = {}) { if (!deployment) throw new Error('A source deployment is required'); const nextVersion = version || bumpVersion(extractVersion(deployment.name)); const baseName = deployment.name.replace(/\s+\d[\d.]*$/, '').trim() || deployment.name; return { name: `${baseName} ${nextVersion}`.trim(), profileId: deployment.profileId || null, scriptId: deployment.scriptId || null, applicationId: applicationId || deployment.applicationId || null, sourceFolder: deployment.sourceFolder || '', intunewinFile: '', commandStyle: deployment.commandStyle, installBehavior: deployment.installBehavior, restartBehavior: deployment.restartBehavior, uiMode: deployment.uiMode, requirements: { ...(deployment.requirements || {}) }, installCommand: deployment.installCommand, uninstallCommand: deployment.uninstallCommand, detectionType: deployment.detectionType, detectionRule: deployment.detectionRule, returnCodes: deployment.returnCodes || [], assignments: deployment.assignments || [], relationships: deployment.relationships || [], status: 'draft', graphAppId: '', graphConnectionId: deployment.graphConnectionId || null, notes: `Version ${nextVersion} created from ${deployment.name}.`, visibility: deployment.visibility, groupId: deployment.groupId || null }; } function extractVersion(name = '') { const match = String(name).match(/(\d[\d.]*)\s*$/); return match ? match[1] : ''; } // Advance a named assignment ring to a new intent (e.g. promote "Broad" from // available to required). Returns a new assignments array; unknown rings are a // no-op so callers can detect "nothing changed". export function setRingIntent(assignments = [], ring, intent) { let changed = false; const next = assignments.map((assignment) => { if (assignment.ring?.toLowerCase() === String(ring).toLowerCase() && assignment.intent !== intent) { changed = true; return { ...assignment, intent }; } return assignment; }); return { assignments: next, changed }; }